C++岗位面试真题宝典 -- 操作系统篇
2.1 Linux中查看进程运行状态的指令、查看内存使用情况的指令、tar解压文件的参数。
参考回答
-
查看进程运行状态的指令:ps命令。“ps -aux | grep PID”,用来查看某PID进程状态
-
查看内存使用情况的指令:free命令。“free -m”,命令查看内存使用情况。
-
tar解压文件的参数:
五个命令中必选一个 -c: 建立压缩档案 -x:解压 -t:查看内容 -r:向压缩归档文件末尾追加文件 -u:更新原压缩包中的文件 这几个参数是可选的 -z:有gzip属性的 -j:有bz2属性的 -Z:有compress属性的 -v:显示所有过程 -O:将文件解开到标准输出
答案解析
//ps使用示例
//显示当前所有进程
ps -A
//与grep联用查找某进程
ps -aux | grep apache
//查看进程运行状态、查看内存使用情况的指令均可使用top指令。
top
2.2 文件权限怎么修改
参考回答
Linux文件的基本权限就有九个,分别是owner/group/others三种身份各有自己的read/write/execute权限
修改权限指令:chmod
答案解析
举例:文件的权限字符为 -rwxrwxrwx 时,这九个权限是三个三个一组。其中,我们可以使用数字来代表各个权限。
各权限的分数对照如下:
r | w | x |
---|---|---|
4 | 2 | 1 |
每种身份(owner/group/others)各自的三个权限(r/w/x)分数是需要累加的,
例如当权限为: [-rwxrwx---] ,则分数是:
owner = rwx = 4+2+1 = 7
group = rwx = 4+2+1 = 7
others= --- = 0+0+0 = 0
所以我们设定权限的变更时,该文件的权限数字就是770!变更权限的指令chmod的语法是这样的:
[root@www ~]# chmod [-R] xyz 文件或目录
选项与参数:
xyz : 就是刚刚提到的数字类型的权限属性,为 rwx 属性数值的相加。
-R : 进行递归(recursive)的持续变更,亦即连同次目录下的所有文件都会变更
# chmod 770 douya.c //即修改douya.c文件的权限为770
2.3 说说常用的Linux命令
参考回答
- cd命令:用于切换当前目录
- ls命令:查看当前文件与目录
- grep命令:该命令常用于分析一行的信息,若当中有我们所需要的信息,就将该行显示出来,该命令通常与管道命令一起使用,用于对一些命令的输出进行筛选加工。
- cp命令:复制命令
- mv命令:移动文件或文件夹命令
- rm命令:删除文件或文件夹命令
- ps命令:查看进程情况
- kill命令:向进程发送终止信号
- tar命令:对文件进行打包,调用gzip或bzip对文件进行压缩或解压
- cat命令:查看文件内容,与less、more功能相似
- top命令:可以查看操作系统的信息,如进程、CPU占用率、内存信息等
- pwd命令:命令用于显示工作目录。
2.4 说说如何以root权限运行某个程序。
参考回答
sudo chown root app(文件名)
sudo chmod u+s app(文件名)
输入上面两条指令后即可
2.5 说说软链接和硬链接的区别。
参考回答
-
定义不同
软链接又叫符号链接,这个文件包含了另一个文件的路径名。可以是任意文件或目录,可以链接不同文件系统的文件。
硬链接就是一个文件的一个或多个文件名。把文件名和计算机文件系统使用的节点号链接起来。因此我们可以用多个文件名与同一个文件进行链接,这些文件名可以在同一目录或不同目录。
-
限制不同
硬链接只能对已存在的文件进行创建,不能交叉文件系统进行硬链接的创建;
软链接可对不存在的文件或目录创建软链接;可交叉文件系统;
-
创建方式不同
硬链接不能对目录进行创建,只可对文件创建;
软链接可对文件或目录创建;
-
影响不同
删除一个硬链接文件并不影响其他有相同 inode 号的文件。
删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。
2.6 说说静态库和动态库怎么制作及如何使用,区别是什么。
参考回答
静态库的制作:
gcc hello.c -c //这样就生成hello.o目标文件
ar rcs libhello.a hello.o//生成libhello.a静态库
静态库的使用:
gcc main.c -lhello -o staticLibrary//main.c和hello静态库链接,生成staticLibrary执行文件
/*
main.c: 是指main主函数
-lhello: 是我们生成的.a 文件砍头去尾(lib不要 .a也不要)前面加-l
-L: 是指告诉gcc编译器先从-L指定的路径去找静态库,默认是从/usr/lib/ 或者 /usr/local/lib/ 去找。
./: 是指当前路径的意思
staticLibrary: 是最后想生成的文件名(这里可随意起名字)
*/
动态库的制作:
gcc -shared -fpic hello.c -o libhello.so
-shared 指定生成动态库
-fpic :fPIC选项作用于编译阶段,在生成目标文件时就得使用该选项,以生成位置无关的代码。
动态库的使用:
gcc main.c -lhello -L ./ -o dynamicDepot
/*
main.c: 是指main主函数
-lhello: 是我们生成的.so 文件砍头去尾(lib不要 .so也不要)前面加-l
-L: 是指告诉gcc编译器先从-L指定的路径去找静态库,默认是从/usr/lib/ 或者 /usr/local/lib/ 去找。
./: 是指当前路径的意思
dynamicDepot: 是最后想生成的文件名(这里可随意起名字)
*/
区别:
- 静态库代码装载的速度快,执行速度略比动态库快。
- 动态库更加节省内存,可执行文件体积比静态库小很多。
- 静态库是在编译时加载,动态库是在运行时加载。
- 生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。
2.7 简述GDB常见的调试命令,什么是条件断点,多进程下如何调试。
参考回答
GDB调试:gdb调试的是可执行文件,在gcc编译时加入 -g ,告诉gcc在编译时加入调试信息,这样gdb才能调试这个被编译的文件 gcc -g tesst.c -o test
GDB命令格式:
-
quit:退出gdb,结束调试
-
list:查看程序源代码
list 5,10:显示5到10行的代码
list test.c:5, 10: 显示源文件5到10行的代码,在调试多个文件时使用
list get_sum: 显示get_sum函数周围的代码
list test,c get_sum: 显示源文件get_sum函数周围的代码,在调试多个文件时使用
-
reverse-search:字符串用来从当前行向前查找第一个匹配的字符串
-
run:程序开始执行
-
help list/all:查看帮助信息
-
break:设置断点
break 7:在第七行设置断点
break get_sum:以函数名设置断点
break 行号或者函数名 if 条件:以条件表达式设置断点
-
watch 条件表达式:条件表达式发生改变时程序就会停下来
-
next:继续执行下一条语句 ,会把函数当作一条语句执行
-
step:继续执行下一条语句,会跟踪进入函数,一次一条的执行函数内的代码
条件断点:break if 条件 以条件表达式设置断点
多进程下如何调试:用set follow-fork-mode child 调试子进程
或者set follow-fork-mode parent 调试父进程
2.8 说说什么是大端小端,如何判断大端小端?
参考回答
小端模式:低的有效字节存储在低的存储器地址。小端一般为主机字节序;常用的X86结构是小端模式。很多的ARM,DSP都为小端模式。
大端模式:高的有效字节存储在低的存储器地址。大端为网络字节序;KEIL C51则为大端模式。
有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
如何判断:我们可以根据联合体来判断系统是大端还是小端。因为联合体变量总是从低地址存储。
int fun1(){
union test{
char c;
int i;
};
test t; t.i = 1;
//如果是大端,则t.c为0x00,则t.c != 1,反之是小端
return (t.c == 1);
}
答案解析
-
在进行网络通信时是否需要进行字节序转换?
相同字节序的平台在进行网络通信时可以不进行字节序转换,但是跨平台进行网络数据通信时必须进行字节序转换。
原因如下:网络协议规定接收到得第一个字节是高字节,存放到低地址,所以发送时会首先去低地址取数据的高字节。小端模式的多字节数据在存放时,低地址存放的是低字节,而被发送方网络协议函数发送时会首先去低地址取数据(想要取高字节,真正取得是低字节),接收方网络协议函数接收时会将接收到的第一个字节存放到低地址(想要接收高字节,真正接收的是低字节),所以最后双方都正确的收发了数据。而相同平台进行通信时,如果双方都进行转换最后虽然能够正确收发数据,但是所做的转换是没有意义的,造成资源的浪费。而不同平台进行通信时必须进行转换,不转换会造成错误的收发数据,字节序转换函数会根据当前平台的存储模式做出相应正确的转换,如果当前平台是大端,则直接返回不进行转换,如果当前平台是小端,会将接收到得网络字节序进行转换。
-
网络字节序
网络上传输的数据都是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节的时候,它将这个字节作为高位字节还是低位字节处理,是一个比较有意义的问题; UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端法存放的; 所以说,网络字节序是大端字节序; 比如,我们经过网络发送整型数值0x12345678时,在80X86平台中,它是以小端发存放的,在发送之前需要使用系统提供的字节序转换函数htonl()将其转换成大端法存放的数值;
2.9 说说进程调度算法有哪些?
参考回答
- 先来先服务调度算法
- 短作业(进程)优先调度算法
- 高优先级优先调度算法
- 时间片轮转法
- 多级反馈队列调度算法
答案解析
- 先来先服务调度算法:每次调度都是从后备作业(进程)队列中选择一个或多个最先进入该队列的作业(进程),将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。
- 短作业(进程)优先调度算法:短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业(进程),将它们调入内存运行。
- 高优先级优先调度算法:当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程
- 时间片轮转法:每次调度时,把CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。
- 多级反馈队列调度算法:综合前面多种调度算法。
在这些调度算法中,有抢占式和非抢占式的区别。
- 非抢占式优先权算法
在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。 - 抢占式优先权调度算法
在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
区别:
非抢占式(Nonpreemptive):让进程运行直到结束或阻塞的调度方式,容易实现,适合专用系统,不适合通用系统。
抢占式(Preemptive):允许将逻辑上可继续运行的在运行过程暂停的调度方式可防止单一进程长时间独占,CPU系统开销大(降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序)
2.10 简述操作系统如何申请以及管理内存的?
参考回答
操作系统如何管理内存:
-
物理内存:物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。
寄存器:速度最快、量少、价格贵。
高速缓存:次之。
主存:再次之。
磁盘:速度最慢、量多、价格便宜。
操作系统会对物理内存进行管理,有一个部分称为内存管理器(memory manager),它的主要工作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收内存。
-
虚拟内存:操作系统为每一个进程分配一个独立的地址空间,但是虚拟内存。虚拟内存与物理内存存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换。
操作系统如何申请内存:
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:**brk和mmap*
2.11 简述Linux系统态与用户态,什么时候会进入系统态?
参考回答
- 内核态与用户态:内核态(系统态)与用户态是操作系统的两种运行级别。内核态拥有最高权限,可以访问所有系统指令;用户态则只能访问一部分指令。
- 什么时候进入内核态:共有三种方式:a、系统调用。b、异常。c、设备中断。其中,系统调用是主动的,另外两种是被动的。
- 为什么区分内核态与用户态:在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。所以区分内核态与用户态主要是出于安全的考虑。
2.12 简述LRU算法及其实现方式。
参考回答
-
LRU算法:LRU算法用于缓存淘汰。思路是将缓存中最近最少使用的对象删除掉
-
实现方式:利用链表和hashmap。
当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。
在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。
答案解析
给出C++实现的代码
class LRUCache {
list<pair<int, int>> cache;//创建双向链表
unordered_map<int, list<pair<int, int>>::iterator> map;//创建哈希表
int cap;
public:
LRUCache(int capacity) {
cap = capacity;
}
int get(int key) {
if (map.count(key) > 0){
auto temp = *map[key];
cache.erase(map[key]);
map.erase(key);
cache.push_front(temp);
map[key] = cache.begin();//映射头部
return temp.second;
}
return -1;
}
void put(int key, int value) {
if (map.count(key) > 0){
cache.erase(map[key]);
map.erase(key);
}
else if (cap == cache.size()){
auto temp = cache.back();
map.erase(temp.first);
cache.pop_back();
}
cache.push_front(pair<int, int>(key, value));
map[key] = cache.begin();//映射头部
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
2.13 一个线程占多大内存?
参考回答
一个linux的线程大概占8M内存。
答案解析
linux的栈是通过缺页来分配内存的,不是所有栈地址空间都分配了内存。因此,8M是最大消耗,实际的内存消耗只会略大于实际需要的内存(内部损耗,每个在4k以内)。
2.14 什么是页表,为什么要有?
参考回答
页表是虚拟内存的概念。操作系统虚拟内存到物理内存的映射表,就被称为页表。
原因:不可能每一个虚拟内存的 Byte 都对应到物理内存的地址。这张表将大得真正的物理地址也放不下,于是操作系统引入了页(Page)的概念。进行分页,这样可以减小虚拟内存页对应物理内存页的映射表大小。
答案解析
如果将每一个虚拟内存的 Byte 都对应到物理内存的地址,每个条目最少需要 8字节(32位虚拟地址->32位物理地址),在 4G 内存的情况下,就需要 32GB 的空间来存放对照表,那么这张表就大得真正的物理地址也放不下了,于是操作系统引入了页(Page)的概念。
在系统启动时,操作系统将整个物理内存以 4K 为单位,划分为各个页。之后进行内存分配时,都以页为单位,那么虚拟内存页对应物理内存页的映射表就大大减小了,4G 内存,只需要 8M 的映射表即可,一些进程没有使用到的虚拟内存,也并不需要保存映射关系,而且Linux 还为大内存设计了多级页表,可以进一页减少了内存消耗。
2.15 简述操作系统中的缺页中断。
参考回答
- 缺页异常:malloc和mmap函数在分配内存时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常,引发缺页中断。
- 缺页中断:缺页异常后将产生一个缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
答案解析
两者区别。
缺页中断与一般中断一样,需要经历四个步骤:保护CPU现场、分析中断原因、转入缺页中断处理程序、恢复CPU现场,继续执行。
缺页中断与一般中断区别:
(1)在指令执行期间产生和处理缺页中断信号
(2)一条指令在执行期间,可能产生多次缺页中断
(3)缺页中断返回的是执行产生中断的一条指令,而一般中断返回的是执行下一条指令。
2.16 说说虚拟内存分布,什么时候会由用户态陷入内核态?
参考回答
- 虚拟内存分布:
用户空间:
(1)代码段.text:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
(2)数据段.data:存放程序中已初始化的全局变量和静态变量的一块内存区域。
(3)BSS 段.bss:存放程序中未初始化的全局变量和静态变量的一块内存区域。
(4)可执行程序在运行时又会多出两个区域:堆区和栈区。
堆区:动态申请内存用。堆从低地址向高地址增长。
栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
(5)最后还有一个文件映射区,位于堆和栈之间。
内核空间:DMA区、常规区、高位区。
- 什么时候进入内核态:共有三种方式:a、系统调用。b、异常。c、设备中断。其中,系统调用是主动的,另外两种是被动的。
2.17 简述一下虚拟内存和物理内存,为什么要用虚拟内存,好处是什么?
参考回答
-
物理内存:物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。
寄存器:速度最快、量少、价格贵。
高速缓存:次之。
主存:再次之。
磁盘:速度最慢、量多、价格便宜。
操作系统会对物理内存进行管理,有一个部分称为内存管理器(memory manager),它的主要工作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收内存。
-
虚拟内存:操作系统为每一个进程分配一个独立的地址空间,但是虚拟内存。虚拟内存与物理内存存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换。
-
为什么要用虚拟内存:因为早期的内存分配方法存在以下问题:
(1)进程地址空间不隔离。会导致数据被随意修改。
(2)内存使用效率低。
(3)程序运行的地址不确定。操作系统随机为进程分配内存空间,所以程序运行的地址是不确定的。
-
使用虚拟内存的好处:
(1)扩大地址空间。每个进程独占一个4G空间,虽然真实物理内存没那么多。
(2)内存保护:防止不同进程对物理内存的争夺和践踏,可以对特定内存地址提供写保护,防止恶意篡改。
(3)可以实现内存共享,方便进程通信。
(4)可以避免内存碎片,虽然物理内存可能不连续,但映射到虚拟内存上可以连续。
-
使用虚拟内存的缺点:
(1)虚拟内存需要额外构建数据结构,占用空间。
(2)虚拟地址到物理地址的转换,增加了执行时间。
(3)页面换入换出耗时。
(4)一页如果只有一部分数据,浪费内存。
2.18 虚拟地址到物理地址怎么映射的?
参考回答
操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表。页表中的每一项都记录了这个页的基地址。
三级页表转换方法:(两步)
-
逻辑地址转线性地址:段起始地址+段内偏移地址=线性地址
-
线性地址转物理地址:
(1)每一个32位的线性地址被划分为三部分:页目录索引(DIRECTORY,10位)、页表索引(TABLE,10位)、页内偏移(OFFSET,12位)
(2)从cr3中取出进程的页目录地址(操作系统调用进程时,这个地址被装入寄存器中)页目录地址 + 页目录索引 = 页表地址 页表地址 + 页表索引 = 页地址 页地址 + 页内偏移 = 物理地址
按照以上两步法,就完成了一个三级页表从虚拟地址到物理地址的转换。
2.19 说说堆栈溢出是什么,会怎么样?
参考回答
堆栈溢出就是不顾堆栈中分配的局部数据块大小,向该数据块写入了过多的数据,导致数据越界。常指调用堆栈溢出,本质上一种数据结构的满溢情况。堆栈溢出可以理解为两个方面:堆溢出和栈溢出。
- 堆溢出:比如不断的new 一个对象,一直创建新的对象,而不进行释放,最终导致内存不足。将会报错:OutOfMemory Error。
- 栈溢出:一次函数调用中,栈中将被依次压入:参数,返回地址等,而方法如果递归比较深或进去死循环,就会导致栈溢出。将会报错:StackOverflow Error。
2.20 简述操作系统中malloc的实现原理
参考回答
malloc底层实现:当开辟的空间小于 128K 时,调用 brk()函数;当开辟的空间大于 128K 时,调用mmap()。malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。
2.21 说说进程空间从高位到低位都有些什么?
参考回答
如上图,从高地址到低地址,一个程序由命令行参数和环境变量、栈、文件映射区、堆、BSS段、数据段、代码段组成。
- 命令行参数和环境变量
- 栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
- 文件映射区,位于堆和栈之间。
- 堆区:动态申请内存用。堆从低地址向高地址增长。
- BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
- 数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
- 代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
2.22 32位系统能访问4GB以上的内存吗?
参考回答
正常情况下是不可以的。原因是计算机使用二进制,每位数只有0或1两个状态,32位正好是2的32次方,正好是4G,所以大于4G就没办法表示了,而在32位的系统中,因其它原因还需要占用一部分空间,所以内存只能识别3G多。要使用4G以上就只能换64位的操作系统了。
但是使用PAE技术就可以实现 32位系统能访问4GB以上的内存。
答案解析
Physical Address Extension(PAE)技术最初是为了弥补32位地址在PC服务器应用上的不足而推出的。我们知道,传统的IA32架构只有32位地址总线,只能让系统容纳不超过4GB的内存,这么大的内存,对于普通的桌面应用应该说是足够用了。可是,对于服务器应用来说,还是显得不足,因为服务器上可能承载了很多同时运行的应用。PAE技术将地址扩展到了36位,这样,系统就能够容纳2^36=64GB的内存。
2.23 请你说说并发和并行
参考回答
- 并发:对于单个CPU,在一个时刻只有一个进程在运行,但是线程的切换时间则减少到纳秒数量级,多个任务不停来回快速切换。
- 并行:对于多个CPU,多个进程同时运行。
- 区别。通俗来讲,它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念。并行的"同时"是同一时刻可以多个任务在运行(处于running),并发的"同时"是经过不同线程快速切换,使得看上去多个任务同时都在运行的现象。
2.24 说说进程、线程、协程是什么,区别是什么?
参考回答
- 进程:程序是指令、数据及其组织形式的描述,而进程则是程序的运行实例,包括程序计数器、寄存器和变量的当前值。
- 线程:微进程,一个进程里更小粒度的执行单元。一个进程里包含多个线程并发执行任务。
- 协程:协程是微线程,在子程序内部执行,可在子程序内部中断,转而执行别的子程序,在适当的时候再返回来接着执行。
区别:
-
线程与进程的区别:
(1)一个线程从属于一个进程;一个进程可以包含多个线程。
(2)一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程。
(3)进程是系统资源调度的最小单位;线程CPU调度的最小单位。
(4)进程系统开销显著大于线程开销;线程需要的系统资源更少。
(5)进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。
(6)进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。
(7)通信方式不一样。
(8)进程适应于多核、多机分布;线程适用于多核
-
线程与协程的区别:
(1)协程执行效率极高。协程直接操作栈基本没有内核切换的开销,所以上下文的切换非常快,切换开销比线程更小。
(2)协程不需要多线程的锁机制,因为多个协程从属于一个线程,不存在同时写变量冲突,效率比线程高。
(3)一个线程可以有多个协程。
2.25 请你说说Linux的fork的作用
参考回答
fork函数用来创建一个子进程。对于父进程,fork()函数返回新创建的子进程的PID。对于子进程,fork()函数调用成功会返回0。如果创建出错,fork()函数返回-1。
答案解析
fork()函数,其原型如下:
#include <unistd.h>
pid_t fork(void);
fork()函数不需要参数,返回值是一个进程标识符PID。返回值有以下三种情况:
(1) 对于父进程,fork()函数返回新创建的子进程的PID。
(2) 对于子进程,fork()函数调用成功会返回0。
(3) 如果创建出错,fork()函数返回-1。
fork()函数创建一个新进程后,会为这个新进程分配进程空间,将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。这时候,子进程和父进程一模一样,都接受系统的调度。因为两个进程都停留在fork()函数中,最后fork()函数会返回两次,一次在父进程中返回,一次在子进程中返回,两次返回的值不一样,如上面的三种情况。
2.26 请你说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程
参考回答
-
孤儿进程:是指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并且由init进程对它们完整状态收集工作。
-
僵尸进程:是指一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait()或者waitpid()系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程。
-
如何解决僵尸进程:
(1)一般,为了防止产生僵尸进程,在fork子进程之后我们都要及时使用wait系统调用;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。
(2)使用kill命令。
打开终端并输入下面命令:
ps aux | grep Z
会列出进程表中所有僵尸进程的详细内容。
然后输入命令:
kill -s SIGCHLD pid(父进程pid)
2.27 请你说说什么是守护进程,如何实现?
参考回答
-
守护进程:守护进程是运行在后台的一种生存期长的特殊进程。它独立于控制终端,处理一些系统级别任务。
-
如何实现:
(1)创建子进程,终止父进程。方法是调用fork() 产生一个子进程,然后使父进程退出。
(2)调用setsid() 创建一个新会话。
(3)将当前目录更改为根目录。使用fork() 创建的子进程也继承了父进程的当前工作目录。
(4)重设文件权限掩码。文件权限掩码是指屏蔽掉文件权限中的对应位。
(5)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。
答案解析
实现代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#define MAXFILE 65535
int main(){
//第一步:创建进程
int pid = fork();
if (pid > 0)
exit(0);//结束父进程
else if (pid < 0){
printf("fork error!\n");
exit(1);//fork失败,退出
}
//第二步:子进程成为新的会话组长和进程组长,并与控制终端分离
setsid();
//第三步:改变工作目录到
chdir("/");
//第四步:重设文件创建掩模
umask(0);
//第五步:关闭打开的文件描述符
for (int i=0; i<MAXFILE; ++i)
close(i);
sleep(2);
}
return 0;
}
2.28 说说进程通信的方式有哪些?
参考回答
进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存)、套接字socket。
-
管道:包括无名管道和命名管道,无名管道半双工,只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件;命名管道可以允许无亲缘关系进程间的通信。
-
系统IPC
消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
信号量semaphore:是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。
信号:用于通知接收进程某个事件的发生。
内存共享:使多个进程访问同一块内存空间。
-
套接字socket:用于不同主机直接的通信。
2.29 说说进程同步的方式?
参考回答
- 信号量semaphore:是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。P操作(递减操作)可以用于阻塞一个进程,V操作(增加操作)可以用于解除阻塞一个进程。
- 管道:一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。
- 消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
2.30 说说Linux进程调度算法及策略有哪些?
参考回答
- 先来先服务调度算法
- 短作业(进程)优先调度算法
- 高优先级优先调度算法
- 时间片轮转法
- 多级反馈队列调度算法
答案解析
- 先来先服务调度算法:每次调度都是从后备作业(进程)队列中选择一个或多个最先进入该队列的作业(进程),将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。
- 短作业(进程)优先调度算法:短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业(进程),将它们调入内存运行。
- 高优先级优先调度算法:当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程
- 时间片轮转法:每次调度时,把CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。
- 多级反馈队列调度算法:综合前面多种调度算法。
在这些调度算法中,有抢占式和非抢占式的区别。
- 非抢占式优先权算法
在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。 - 抢占式优先权调度算法
在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
区别:
非抢占式(Nonpreemptive):让进程运行直到结束或阻塞的调度方式,容易实现,适合专用系统,不适合通用系统。
抢占式(Preemptive):允许将逻辑上可继续运行的在运行过程暂停的调度方式可防止单一进程长时间独占,CPU系统开销大(降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序)
2.31 说说进程有多少种状态?
参考回答
进程有五种状态:创建、就绪、执行、阻塞、终止。一个进程创建后,被放入队列处于就绪状态,等待操作系统调度执行,执行过程中可能切换到阻塞状态(并发),任务完成后,进程销毁终止。
答案解析
创建状态
一个应用程序从系统上启动,首先就是进入创建状态,需要获取系统资源创建进程管理块(PCB:Process Control Block)完成资源分配。
就绪状态
在创建状态完成之后,进程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。
运行状态
获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果进程的时间片用完了就进入就绪状态。
阻塞状态
在运行状态期间,如果进行了阻塞的操作,如耗时的I/O操作,此时进程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态。
终止状态
进程结束或者被系统终止,进入终止状态
相互转换如图:
2.32 进程通信中的管道实现原理是什么?
参考回答
操作系统在内核中开辟一块缓冲区(称为管道)用于通信。管道是一种两个进程间进行单向通信的机制。因为这种单向性,管道又称为半双工管道,所以其使用是有一定的局限性的。半双工是指数据只能由一个进程流向另一个进程(一个管道负责读,一个管道负责写);如果是全双工通信,需要建立两个管道。管道分为无名管道和命名管道,无名管道只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件,管道本质是一种文件;命名管道可以允许无亲缘关系进程间的通信。
管道原型如下:
#include <unistd.h>
int pipe(int fd[2]);
pipe()函数创建的管道处于一个进程中间,因此一个进程在由 pipe()创建管道后,一般再使用fork() 建立一个子进程,然后通过管道实现父子进程间的通信。管道两端可分别用描述字fd[0]以及fd[1]来描述。注意管道的两端的任务是固定的,即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另 一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将发生错误。一般文件的 I/O 函数都可以用于管道,如close()、read()、write()等。
具体步骤如下:
- 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
- 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
- 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
答案解析
给出实现的代码,实现父子进程间的管道通信
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define INPUT 0
#define OUTPUT 1
int main(){
//创建管道
int fd[2];
pipe(fd);
//创建子进程
pid_t pid = fork();
if (pid < 0){
printf("fork error!\n");
exit(-1);
}
else if (pid == 0){//执行子进程
printf("Child process is starting...\n");
//子进程向父进程写数据,关闭管道的读端
close(fd[INPUT]);
write(fd[OUTPUT], "hello douya!", strlen("hello douya!"));
exit(0);
}
else{//执行父进程
printf ("Parent process is starting......\n");
//父进程从管道读取子进程写的数据 ,关闭管道的写端
close(fd[OUTPUT]);
char buf[255];
int output = read(fd[INPUT], buf, sizeof(buf));
printf("%d bytes of data from child process: %s\n", output, buf);
}
return 0;
}
2.33 简述mmap的原理和使用场景
参考回答
原理:mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read, write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图:
使用场景:
- 对同一块区域频繁读写操作;
- 可用于实现用户空间和内核空间的高效交互
- 可提供进程间共享内存及相互通信
- 可实现高效的大规模数据传输。
2.34 互斥量能不能在进程中使用?
参考回答
能。
不同的进程之间,存在资源竞争或并发使用的问题,所以需要互斥量。
进程中也需要互斥量,因为一个进程中可以包含多个线程,线程与线程之间需要通过互斥的手段进行同步,避免导致共享数据修改引起冲突。可以使用互斥锁,属于互斥量的一种。
2.35 协程是轻量级线程,轻量级表现在哪里?
参考回答
- 协程调用跟切换比线程效率高:协程执行效率极高。协程不需要多线程的锁机制,可以不加锁的访问全局变量,所以上下文的切换非常快。
- 协程占用内存少:执行协程只需要极少的栈内存(大概是4~5KB),而默认情况下,线程栈的大小为1MB。
- 切换开销更少:协程直接操作栈基本没有内核切换的开销,所以切换开销比线程少。
2.36 说说常见信号有哪些,表示什么含义?
参考回答
编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。编号为1 ~ 31的信号如下:
信号代号 | 信号名称 | 说 明 |
---|---|---|
1 | SIGHUP | 该信号让进程立即关闭.然后重新读取配置文件之后重启 |
2 | SIGINT | 程序中止信号,用于中止前台进程。相当于输出 Ctrl+C 快捷键 |
3 | SIGQUIT | 和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。 |
4 | SIGILL | 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。 |
5 | SIGTRAP | 由断点指令或其它trap指令产生. 由debugger使用。 |
6 | SIGABRT | 调用abort函数生成的信号。 |
7 | SIGBUS | 非法地址, 包括内存地址对齐(alignment)出错。 |
8 | SIGFPE | 在发生致命的算术运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等其他所有的算术运算错误 |
9 | SIGKILL | 用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。般用于强制中止进程 |
10 | SIGUSR1 | 留给用户使用 |
11 | SIGSEGV | 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据. |
12 | SIGUSR2 | 留给用户使用 |
13 | SIGPIPE | 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。 |
14 | SIGALRM | 时钟定时信号,计算的是实际的时间或时钟时间。alarm 函数使用该信号 |
15 | SIGTERM | 正常结束进程的信号,kill 命令的默认信号。如果进程已经发生了问题,那么这 个信号是无法正常中止进程的,这时我们才会尝试 SIGKILL 信号,也就是信号 9 |
17 | SIGCHLD | 子进程结束时, 父进程会收到这个信号。 |
18 | SIGCONT | 该信号可以让暂停的进程恢复执行。本信号不能被阻断 |
19 | SIGSTOP | 该信号可以暂停前台进程,相当于输入 Ctrl+Z 快捷键。本信号不能被阻断 |
20 | SIGTSTP | 停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号 |
21 | SIGTTIN | 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行. |
22 | SIGTTOU | 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到. |
23 | SIGURG | 有"紧急"数据或out-of-band数据到达socket时产生. |
24 | SIGXCPU | 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。 |
25 | SIGXFSZ | 当进程企图扩大文件以至于超过文件大小资源限制。 |
26 | SIGVTALRM | 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间. |
27 | SIGPROF | 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间. |
28 | SIGWINCH | 窗口大小改变时发出. |
29 | SIGIO | 文件描述符准备就绪, 可以开始进行输入/输出操作. |
30 | SIGPWR | Power failure |
31 | SIGSYS | 非法的系统调用。 |
而常见信号如下:
信号代号 | 信号名称 | 说 明 |
---|---|---|
1 | SIGHUP | 该信号让进程立即关闭.然后重新读取配置文件之后重启 |
2 | SIGINT | 程序中止信号,用于中止前台进程。相当于输出 Ctrl+C 快捷键 |
8 | SIGFPE | 在发生致命的算术运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等其他所有的算术运算错误 |
9 | SIGKILL | 用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。般用于强制中止进程 |
14 | SIGALRM | 时钟定时信号,计算的是实际的时间或时钟时间。alarm 函数使用该信号 |
15 | SIGTERM | 正常结束进程的信号,kill 命令的默认信号。如果进程已经发生了问题,那么这 个信号是无法正常中止进程的,这时我们才会尝试 SIGKILL 信号,也就是信号 9 |
17 | SIGCHLD | 子进程结束时, 父进程会收到这个信号。 |
18 | SIGCONT | 该信号可以让暂停的进程恢复执行。本信号不能被阻断 |
19 | SIGSTOP | 该信号可以暂停前台进程,相当于输入 Ctrl+Z 快捷键。本信号不能被阻断 |
其中最重要的就是 "1"、"9"、"15"、"17"这几个信号。
2.37 说说线程间通信的方式有哪些?
参考回答
线程间的通信方式包括临界区、互斥量、信号量、条件变量、读写锁:
- 临界区:每个线程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个线程使用的共享资源)。每次只准许一个线程进入临界区,进入后不允许其他线程进入。不论是硬件临界资源,还是软件临界资源,多个线程必须互斥地对它进行访问。
- 互斥量:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
- 信号量:计数器,允许多个线程同时访问同一个资源。
- 条件变量:通过条件变量通知操作的方式来保持多线程同步。
- 读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。
2.38 说说线程同步方式有哪些?
参考回答
线程间的同步方式包括互斥锁、信号量、条件变量、读写锁:
- 互斥锁:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
- 信号量:计数器,允许多个线程同时访问同一个资源。
- 条件变量:通过条件变量通知操作的方式来保持多线程同步。
- 读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。
2.39 说说什么是死锁,产生的条件,如何解决?
参考回答
-
死锁: 是指多个进程在执行过程中,因争夺资源而造成了互相等待。此时系统产生了死锁。比如两只羊过独木桥,若两只羊互不相让,争着过桥,就产生死锁。
-
产生的条件:死锁发生有四个必要条件:
(1)互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问,只能等待,直到进程使用完成后释放该资源;(2)请求保持条件:进程获得一定资源后,又对其他资源发出请求,但该资源被其他进程占有,此时请求阻塞,而且该进程不会释放自己已经占有的资源;
(3)不可剥夺条件:进程已获得的资源,只能自己释放,不可剥夺;
(4)环路等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
-
如何解决:
(1)资源一次性分配,从而解决请求保持的问题
(2)可剥夺资源:当进程新的资源未得到满足时,释放已有的资源;
(3)资源有序分配:资源按序号递增,进程请求按递增请求,释放则相反。
答案解析
举个例子,比如:如果此时有两个线程T1和T2,它们分别占有R1和R2资源
此时,T1请求R2资源的同时,T2请求R1资源。
这个时候T2说:你把R1给我,我就给你R2
T1说:不行,你要先给我R2,我才能给你R1
那么就这样,死锁产生了。如下图:
2.40 有了进程,为什么还要有线程?
参考回答
-
原因
进程在早期的多任务操作系统中是基本的执行单元。每次进程切换,都要先保存进程资源然后再恢复,这称为上下文切换。但是进程频繁切换将引起额外开销,从而严重影响系统的性能。为了减少进程切换的开销,人们把两个任务放到一个进程中,每个任务用一个更小粒度的执行单元来实现并发执行,这就是线程。
-
线程与进程对比
(1)进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。
但多个线程共享进程的内存,如代码段、数据段、扩展段,线程间进行信息交换十分方便。
(2)调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。
但创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。
2.41 单核机器上写多线程程序,是否要考虑加锁,为什么?
参考回答
在单核机器上写多线程程序,仍然需要线程锁。
原因:因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。
2.42 说说多线程和多进程的不同?
参考回答
(1)一个线程从属于一个进程;一个进程可以包含多个线程。
(2)一个线程挂掉,对应的进程挂掉,多线程也挂掉;一个进程挂掉,不会影响其他进程,多进程稳定。
(3)进程系统开销显著大于线程开销;线程需要的系统资源更少。
(4)多个进程在执行时拥有各自独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。
(5)多进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只需要切换硬件上下文和内核栈。
(6)通信方式不一样。
(7)多进程适应于多核、多机分布;多线程适用于多核
2.43 简述互斥锁的机制,互斥锁与读写的区别?
参考回答
-
互斥锁机制:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
-
互斥锁和读写锁:
(1) 读写锁区分读者和写者,而互斥锁不区分
(2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
答案解析
原理详解:
互斥锁其实就是一个bool型变量,为true时表示锁可获取,为false时表示已上锁。这里说的是互斥锁,其实是泛指linux中所有的锁机制。
我们采用互斥锁保护临界区,从而防止竞争条件。也就是说,一个线程在进入临界区时应得到锁;它在退出临界区时释放锁。函数 acquire() 获取锁,而函数 release() 释放锁,如图 :
每个互斥锁有一个布尔变量 available,它的值表示锁是否可用。如果锁是可用的,那么调用 acquire() 会成功,并且锁不再可用。当一个线程试图获取不可用的锁时,它会阻塞,直到锁被释放。
按如下定义 acquire():
acquire() {
while (!available);
/* busy wait */
available = false;
}
按如下定义release():
release() {
available = true;
}
2.44 说说什么是信号量,有什么作用?
参考回答
-
概念:信号量本质上是一个计数器,用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
-
原理:由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),具体的行为如下:
(1)P(sv)操作:如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行(信号量的值为正,进程获得该资源的使用权,进程将信号量减1,表示它使用了一个资源单位)。
(2)V(sv)操作:如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1(若此时信号量的值为0,则进程进入挂起状态,直到信号量的值大于0,若进程被唤醒则返回至第一步)。
-
作用:用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
2.45 进程、线程的中断切换的过程是怎样的?
参考回答
上下文切换指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换。
-
进程上下文切换
(1)保护被中断进程的处理器现场信息
(2)修改被中断进程的进程控制块有关信息,如进程状态等
(3)把被中断进程的进程控制块加入有关队列
(4)选择下一个占有处理器运行的进程
(5)根据被选中进程设置操作系统用到的地址转换和存储保护信息
切换页目录以使用新的地址空间
切换内核栈和硬件上下文(包括分配的内存,数据段,堆栈段等)
(6)根据被选中进程恢复处理器现场
-
线程上下文切换
(1)保护被中断线程的处理器现场信息
(2)修改被中断线程的线程控制块有关信息,如线程状态等
(3)把被中断线程的线程控制块加入有关队列
(4)选择下一个占有处理器运行的线程
(5)根据被选中线程设置操作系统用到的存储保护信息
切换内核栈和硬件上下文(切换堆栈,以及各寄存器)
(6)根据被选中线程恢复处理器现场
2.46 简述自旋锁和互斥锁的使用场景
参考回答
- 互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑
(1)临界区有IO操作
(2)临界区代码复杂或者循环量大
(3)临界区竞争非常激烈
(4)单核处理器
- 自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下。
2.47 请你说说线程有哪些状态,相互之间怎么转换?
参考回答
类似进程,有以下五种状态:
- 新建状态(New)
- 就绪状态(Runnable)
- 运行状态(Running)
- 阻塞状态(Blocked)
- 死亡状态(Dead)
转换方式如下:
创建状态
一个应用程序从系统上启动,首先就是进入创建状态,获取系统资源。
就绪状态
在创建状态完成之后,线程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。
运行状态
获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果线程的时间片用完了就进入就绪状态。
阻塞状态
在运行状态期间,如果进行了阻塞的操作,如耗时的I/O操作,此时线程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态。
终止状态
线程结束或者被系统终止,进入终止状态
相互转换如图:
2.48 多线程和单线程有什么区别,多线程编程要注意什么,多线程加锁需要注意什么?
参考回答
-
区别:
(1)多线程从属于一个进程,单线程也从属于一个进程;一个线程挂掉都会导致从属的进程挂掉。
(2)一个进程里有多个线程,可以并发执行多个任务;一个进程里只有一个线程,就只能执行一个任务。
(3)多线程并发执行多任务,需要切换内核栈与硬件上下文,有切换的开销;单线程不需要切换,没有切换的开销。
(4)多线程并发执行多任务,需要考虑同步的问题;单线程不需要考虑同步的问题。
-
多线程编程需要考虑同步的问题。线程间的同步方式包括互斥锁、信号量、条件变量、读写锁。
-
多线程加锁,主要需要注意死锁的问题。破坏死锁的必要条件从而避免死锁。
答案解析
-
死锁: 是指多个进程在执行过程中,因争夺资源而造成了互相等待。此时系统产生了死锁。比如两只羊过独木桥,若两只羊互不相让,争着过桥,就产生死锁。
-
产生的条件:死锁发生有四个必要条件:
(1)互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问,只能等待,直到进程使用完成后释放该资源;(2)请求保持条件:进程获得一定资源后,又对其他资源发出请求,但该资源被其他进程占有,此时请求阻塞,而且该进程不会释放自己已经占有的资源;
(3)不可剥夺条件:进程已获得的资源,只能自己释放,不可剥夺;
(4)环路等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
-
如何解决:
(1)资源一次性分配,从而解决请求保持的问题
(2)可剥夺资源:当进程新的资源未得到满足时,释放已有的资源;
(3)资源有序分配:资源按序号递增,进程请求按递增请求,释放则相反。
答案解析
举个例子,比如:如果此时有两个线程T1和T2,它们分别占有R1和R2资源
此时,T1请求R2资源的同时,T2请求R1资源。
这个时候T2说:你把R1给我,我就给你R2
T1说:不行,你要先给我R2,我才能给你R1
那么就这样,死锁产生了。如下图:
2.49 说说sleep和wait的区别?
参考回答
-
sleep
sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。
在linux下面,sleep函数的参数是秒,而windows下面sleep的函数参数是毫秒。
windows下面sleep的函数参数是毫秒。
例如:
#include <windows.h>// 首先应该先导入头文件 Sleep (500) ; //注意第一个字母是大写。 //就是到这里停半秒,然后继续向下执行。
在 Linux C语言中 sleep的单位是秒
例如:
#include <unistd.h>// 首先应该先导入头文件 sleep(5); //停5秒 //就是到这里停5秒,然后继续向下执行。
-
wait
wait是父进程回收子进程PCB资源的一个系统调用。进程一旦调用了wait函数,就立即阻塞自己本身,然后由wait函数自动分析当前进程的某个子进程是否已经退出,当找到一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞,直到有一个出现为止。函数原型如下:
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int* status);
子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一起返回。如果不需要结束状态值,则参数status可以设成 NULL。
-
区别:
(1)sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。(2)wait是父进程回收子进程PCB(Process Control Block)资源的一个系统调用。
2.50 说说线程池的设计思路,线程池中线程的数量由什么确定?
参考回答
-
设计思路:
实现线程池有以下几个步骤:
(1)设置一个生产者消费者队列,作为临界资源。(2)初始化n个线程,并让其运行起来,加锁去队列里取任务运行
(3)当任务队列为空时,所有线程阻塞。
(4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。
-
线程池中线程数量:
线程数量和哪些因素有关:CPU,IO、并行、并发
如果是CPU密集型应用,则线程池大小设置为:CPU数目+1 如果是IO密集型应用,则线程池大小设置为:2*CPU数目+1 最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
所以线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
答案解析
-
为什么要创建线程池:
创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。同时线程池也是为了提升系统效率。
-
线程池的核心线程与普通线程:
任务队列可以存放100个任务,此时为空,线程池里有10个核心线程,若突然来了10个任务,那么刚好10个核心线程直接处理;若又来了90个任务,此时核心线程来不及处理,那么有80个任务先入队列,再创建核心线程处理任务;若又来了120个任务,此时任务队列已满,不得已,就得创建20个普通线程来处理多余的任务。
以上是线程池的工作流程。
2.51 进程和线程相比,为什么慢?
参考回答
- 进程系统开销显著大于线程开销;线程需要的系统资源更少。
- 进程切换开销比线程大。多进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只需要切换硬件上下文和内核栈。
- 进程通信比线程通信开销大。进程通信需要借助管道、队列、共享内存,需要额外申请空间,通信繁琐;而线程共享进程的内存,如代码段、数据段、扩展段,通信快捷简单,同步开销更小。
2.52 简述Linux零拷贝的原理?
参考回答
-
什么是零拷贝:
所谓「零拷贝」描述的是计算机操作系统当中,CPU不执行将数据从一个内存区域,拷贝到另外一个内存区域的任务。通过网络传输文件时,这样通常可以节省 CPU 周期和内存带宽。
-
零拷贝的好处:
(1)节省了 CPU 周期,空出的 CPU 可以完成更多其他的任务
(2)减少了内存区域之间数据拷贝,节省内存带宽
(3)减少用户态和内核态之间数据拷贝,提升数据传输效率
(4)应用零拷贝技术,减少用户态和内核态之间的上下文切换
-
零拷贝原理:
在传统 IO 中,用户态空间与内核态空间之间的复制是完全不必要的,因为用户态空间仅仅起到了一种数据转存媒介的作用,除此之外没有做任何事情。
(1)Linux 提供了 sendfile() 用来减少我们的数据拷贝和上下文切换次数。
过程如图:
a. 发起 sendfile() 系统调用,操作系统由用户态空间切换到内核态空间(第一次上下文切换)
b. 通过 DMA 引擎将数据从磁盘拷贝到内核态空间的输入的 socket 缓冲区中(第一次拷贝)
c. 将数据从内核空间拷贝到与之关联的 socket 缓冲区(第二次拷贝)
d. 将 socket 缓冲区的数据拷贝到协议引擎中(第三次拷贝)
e. sendfile() 系统调用结束,操作系统由用户态空间切换到内核态空间(第二次上下文切换)
根据以上过程,一共有 2 次的上下文切换,3 次的 I/O 拷贝。我们看到从用户空间到内核空间并没有出现数据拷贝,从操作系统角度来看,这个就是零拷贝。内核空间出现了复制的原因: 通常的硬件在通过DMA访问时期望的是连续的内存空间。
(2)mmap 数据零拷贝原理
如果需要对数据做操作,Linux 提供了mmap 零拷贝来实现。
2.53 简述epoll和select的区别,epoll为什么高效?
参考回答
-
区别:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;而epoll保证了每个fd在整个过程中只会拷贝一次。
(2)每次调用select都需要在内核遍历传递进来的所有fd;而epoll只需要轮询一次fd集合,同时查看就绪链表中有没有就绪的fd就可以了。
(3)select支持的文件描述符数量太小了,默认是1024;而epoll没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。
-
epoll为什么高效:
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而epoll只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。
2.54 说说多路IO复用技术有哪些,区别是什么?
参考回答
-
select,poll,epoll都是IO多路复用的机制,I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。
-
区别:
(1)poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
(2)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
(3)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而epoll只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。
2.55 简述socket中select,epoll的使用场景和区别,epoll水平触发与边缘触发的区别?
参考回答
-
select,epoll的使用场景:都是IO多路复用的机制,应用于高并发的网络编程的场景。I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。
-
select,epoll的区别:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;而epoll保证了每个fd在整个过程中只会拷贝一次。
(2)每次调用select都需要在内核遍历传递进来的所有fd;而epoll只需要轮询一次fd集合,同时查看就绪链表中有没有就绪的fd就可以了。
(3)select支持的文件描述符数量太小了,默认是1024;而epoll没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。
-
epoll水平触发与边缘触发的区别
LT模式(水平触发)下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;
而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。
2.56 说说Reactor、Proactor模式。
参考回答
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
-
Reactor模式:Reactor模式应用于同步I/O的场景。Reactor中读操作的具体步骤如下:
读取操作:
(1)应用程序注册读就需事件和相关联的事件处理器
(2)事件分离器等待事件的发生
(3)当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
(4)事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
-
Proactor模式:Proactor模式应用于异步I/O的场景。Proactor中读操作的具体步骤如下:
(1)应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
(2)事件分离器等待读取操作完成事件
(3)在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
(4)事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
-
区别:从上面可以看出,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要用户再自己接收数据,直接使用就可以了,操作系统会将数据从内核拷贝到用户区。
答案解析
IO模型的类型。
(1)阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
(2)非阻塞IO:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。
(3)信号驱动IO:Linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO信号,然后处理IO事件。
(4)IO多路复用:Linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检查。知道有数据可读或可写时,才真正调用IO操作函数。
(5)异步IO:Linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。用户可以直接去使用数据。
前四种模型--阻塞IO、非阻塞IO、多路复用IO和信号驱动IO都属于同步模式,因为其中真正的IO操作(函数)都将会阻塞进程,只有异步IO模型真正实现了IO操作的异步性。
2.57 简述同步与异步的区别,阻塞与非阻塞的区别?
参考回答
-
同步与异步的区别:
同步:是所有的操作都做完,才返回给用户结果。即写完数据库之后,再响应用户,用户体验不好。
异步:不用等所有操作都做完,就响应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较好。
-
阻塞与非阻塞的区别:
阻塞:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
非阻塞:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。
2.58 BIO、NIO有什么区别?
参考回答
BIO(Blocking I/O):阻塞IO。调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
NIO(New I/O):同时支持阻塞与非阻塞模式,NIO的做法是叫一个线程不断的轮询每个IO的状态,看看是否有IO的状态发生了改变,从而进行下一步的操作。
2.59 请介绍一下5种IO模型
参考回答
- 阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
- 非阻塞IO:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。
- 信号驱动IO:Linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO信号,然后处理IO事件。
- IO多路复用:Linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检查。知道有数据可读或可写时,才真正调用IO操作函数。
- 异步IO:Linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。用户可以直接去使用数据。
答案解析
前四种模型--阻塞IO、非阻塞IO、多路复用IO和信号驱动IO都属于同步模式,因为其中真正的IO操作(函数)都将会阻塞进程,只有异步IO模型真正实现了IO操作的异步性。
异步和同步的区别就在于,异步是内核将数据拷贝到用户区,不需要用户再自己接收数据,直接使用就可以了,而同步是内核通知用户数据到了,然后用户自己调用相应函数去接收数据。
2.60 请说一下socket网络编程中客户端和服务端用到哪些函数?
参考回答
-
服务器端函数:
(1)socket创建一个套接字
(2)bind绑定ip和port
(3)listen使套接字变为可以被动链接
(4)accept等待客户端的链接
(5)write/read接收发送数据
(6)close关闭连接
-
客户端函数:
(1)创建一个socket,用函数socket()
(2)bind绑定ip和port
(3)连接服务器,用函数connect()
(4)收发数据,用函数send()和recv(),或read()和write()
(5)close关闭连接、
2.61 简述网络七层参考模型,每一层的作用?
参考回答
OSI七层模型 | 功能 | 对应的网络协议 | TCP/IP四层概念模型 |
---|---|---|---|
应用层 | 文件传输,文件管理,电子邮件的信息处理 | HTTP、TFTP, FTP, NFS, WAIS、SMTP | 应用层 |
表示层 | 确保一个系统的应用层发送的消息可以被另一个系统的应用层读取,编码转换,数据解析,管理数据的解密和加密。 | Telnet, Rlogin, SNMP, Gopher | 应用层 |
会话层 | 负责在网络中的两节点建立,维持和终止通信。 | SMTP, DNS | 应用层 |
传输层 | 定义一些传输数据的协议和端口。 | TCP, UDP | 传输层 |
网络层 | 控制子网的运行,如逻辑编址,分组传输,路由选择 | IP, ICMP, ARP, RARP, AKP, UUCP | 网络层 |
数据链路层 | 主要是对物理层传输的比特流包装,检测保证数据传输的可靠性,将物理层接收的数据进行MAC(媒体访问控制)地址的封装和解封装 | FDDI, Ethernet, Arpanet, PDN, SLIP, PPP,STP。HDLC,SDLC,帧中继 | 数据链路层 |
物理层 | 定义物理设备的标准,主要对物理连接方式,电气特性,机械特性等制定统一标准。 | IEEE 802.1A, IEEE 802.2到IEEE 802. | 数据链路层 |