目录
[1 Linux中查看进程运行状态的指令、查看内存使用情况的指令、 tar解压文件的参数。](#1 Linux中查看进程运行状态的指令、查看内存使用情况的指令、 tar解压文件的参数。)
[2 文件权限怎么修改](#2 文件权限怎么修改)
[3 说说常用的Linux命令](#3 说说常用的Linux命令)
[4 说说如何以root权限运行某个程序。](#4 说说如何以root权限运行某个程序。)
[5 说说软链接和硬链接的区别。](#5 说说软链接和硬链接的区别。)
[6 说说静态库和动态库怎么制作及如何使用,区别是什么。](#6 说说静态库和动态库怎么制作及如何使用,区别是什么。)
[7 简述GDB常见的调试命令,什么是条件断点,多进程下如何调试。](#7 简述GDB常见的调试命令,什么是条件断点,多进程下如何调试。)
[8 说说什么是大端小端,如何判断大端小端?](#8 说说什么是大端小端,如何判断大端小端?)
[9 说说进程调度算法有哪些?](#9 说说进程调度算法有哪些?)
[10 简述操作系统如何申请以及管理内存的?](#10 简述操作系统如何申请以及管理内存的?)
[11 简述Linux系统态与用户态,什么时候会进入系统态?](#11 简述Linux系统态与用户态,什么时候会进入系统态?)
[12 简述LRU算法及其实现方式。](#12 简述LRU算法及其实现方式。)
[13 一个线程占多大内存?](#13 一个线程占多大内存?)
[14 什么是页表,为什么要有?](#14 什么是页表,为什么要有?)
[15 简述操作系统中的缺页中断。](#15 简述操作系统中的缺页中断。)
[16 说说虚拟内存分布,什么时候会由用户态陷入内核态?](#16 说说虚拟内存分布,什么时候会由用户态陷入内核态?)
[17 简述一下虚拟内存和物理内存,为什么要用虚拟内存,好处是什么?](#17 简述一下虚拟内存和物理内存,为什么要用虚拟内存,好处是什么?)
[18 虚拟地址到物理地址怎么映射的?](#18 虚拟地址到物理地址怎么映射的?)
[19 说说堆栈溢出是什么,会怎么样?](#19 说说堆栈溢出是什么,会怎么样?)
[20 简述操作系统中malloc的实现原理](#20 简述操作系统中malloc的实现原理)
[22 32位系统能访问4GB以上的内存吗?](#22 32位系统能访问4GB以上的内存吗?)
[23 请你说说并发和并行](#23 请你说说并发和并行)
[24 说说进程、线程、协程是什么,区别是什么?](#24 说说进程、线程、协程是什么,区别是什么?)
[25 请你说说Linux的fork的作用](#25 请你说说Linux的fork的作用)
[26 请你说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程](#26 请你说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程)
[28 说说进程通信的方式有哪些?](#28 说说进程通信的方式有哪些?)
[29 说说进程同步的方式?](#29 说说进程同步的方式?)
[30 说说Linux进程调度算法及策略有哪些?](#30 说说Linux进程调度算法及策略有哪些?)
[31 说说进程有多少种状态?](#31 说说进程有多少种状态?)
[33 简述mmap的原理和使用场景](#33 简述mmap的原理和使用场景)
[34 互斥量能不能在进程中使用?](#34 互斥量能不能在进程中使用?)
[35 协程是轻量级线程,轻量级表现在哪里?](#35 协程是轻量级线程,轻量级表现在哪里?)
[36 说说常见信号有哪些,表示什么含义?](#36 说说常见信号有哪些,表示什么含义?)
[37 说说线程间通信的方式有哪些?](#37 说说线程间通信的方式有哪些?)
[41 单核机器上写多线程程序,是否要考虑加锁,为什么?](#41 单核机器上写多线程程序,是否要考虑加锁,为什么?)
[42 说说多线程和多进程的不同?](#42 说说多线程和多进程的不同?)
[43 简述互斥锁的机制,互斥锁与读写的区别?](#43 简述互斥锁的机制,互斥锁与读写的区别?)
[44 说说什么是信号量,有什么作用?](#44 说说什么是信号量,有什么作用?)
[46 简述自旋锁和互斥锁的使用场景](#46 简述自旋锁和互斥锁的使用场景)
[48 多线程和单线程有什么区别,多线程编程要注意什么,多线程加锁](#48 多线程和单线程有什么区别,多线程编程要注意什么,多线程加锁)
[49 说说sleep和wait的区别?](#49 说说sleep和wait的区别?)
[51 进程和线程相比,为什么慢?](#51 进程和线程相比,为什么慢?)
[52 简述Linux零拷贝的原理?](#52 简述Linux零拷贝的原理?)
操作系统篇
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****文件权限怎么修改
参考回答
Linux 文件的基本权限就有九个,分别是 owner/group/others 三种身份各有自己的 read/write/execute
权限
修改权限指令: chmod
答案解析
举例:文件的权限字符为 -rwxrwxrwx 时,这九个权限是三个三个一组。其中,我们可以使用数字来代表各个权限。
各权限的分数对照如下:
每种身份 (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
3说说常用的Linux****命令
参考回答
- cd 命令:用于切换当前目录
- ls 命令:查看当前文件与目录
- grep 命令:该命令常用于分析一行的信息,若当中有我们所需要的信息,就将该行显示出来,该命令通常与管道命令一起使用,用于对一些命令的输出进行筛选加工。
- cp 命令:复制命令
- mv 命令:移动文件或文件夹命令 6. rm 命令:删除文件或文件夹命令
- ps 命令:查看进程情况
- kill 命令:向进程发送终止信号
- tar 命令:对文件进行打包,调用 gzip 或 bzip 对文件进行压缩或解压
- cat 命令:查看文件内容,与 less 、 more 功能相似
- top 命令:可以查看操作系统的信息,如进程、 CPU 占用率、内存信息等
- pwd 命令:命令用于显示工作目录。
4说说如何以root****权限运行某个程序。
参考回答
sudo chown root app (文件名)
sudo chmod u + s app (文件名)
输入上面两条指令后即可
5****说说软链接和硬链接的区别。
参考回答
- 定义不同
软链接又叫符号链接,这个文件包含了另一个文件的路径名。可以是任意文件或目录,可以链接不
同文件系统的文件。
硬链接就是一个文件的一个或多个文件名。把文件名和计算机文件系统使用的节点号链接起来。因
此我们可以用多个文件名与同一个文件进行链接,这些文件名可以在同一目录或不同目录。 - 限制不同
硬链接只能对已存在的文件进行创建,不能交叉文件系统进行硬链接的创建;
软链接可对不存在的文件或目录创建软链接;可交叉文件系统; - 创建方式不同
硬链接不能对目录进行创建,只可对文件创建;
软链接可对文件或目录创建; - 影响不同
删除一个硬链接文件并不影响其他有相同 inode 号的文件。
删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即
dangling link ,若被指向路径文件被重新创建,死链接可恢复为正常的软链接
6****说说静态库和动态库怎么制作及如何使用,区别是什么。
静态库的制作:
cpp
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 为后缀。
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 调试父进程
8****说说什么是大端小端,如何判断大端小端?
参考回答
小端模式 : 低 的有效字节存储在 低的 存储器地址。小端一般为主机字节序;常用的 X86 结构是小端模式。很多的ARM , DSP 都为小端模式。
大端模式 : 高 的有效字节存储在 低的 存储器地址。大端为网络字节序; KEIL C51 则为大端模式。
有些 ARM 处理器还可以由硬件来选择是大端模式还是小端模式。
如何判断:我们可以根据 联合体 来判断系统是大端还是小端。因为联合体变量总是从 低地址 存储 。
cppint 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() 将其转换成大端
法存放的数值 ;
9****说说进程调度算法有哪些?
参考回答
- 先来先服务调度算法
- 短作业 ( 进程 ) 优先调度算法
- 高优先级优先调度算法
- 时间片轮转法
- 多级反馈队列调度算法
答案解析 - 先来先服务调度算法:每次调度都是从后备作业(进程)队列中选择一个或多个 最先进入该队列的 作业 (进程),将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。
- 短作业 ( 进程 ) 优先调度算法:短作业优先 (SJF) 的调度算法是从后备队列中选择一个或若干个 估计运 行时间最短的作业(进程) ,将它们调入内存运行。
- 高优先级优先调度算法:当把该算法用于作业调度时,系统将从后备队列中选择若干个 优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程
- 时间片轮转法:每次调度时, 把 CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几 ms 到几百 ms 。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来 停止该进程的执行,并将它送往就绪队列的末尾 ;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。
- 多级反馈队列调度算法:综合前面多种调度算法。
在这些调度算法中,有抢占式和非抢占式的区别。 - 非抢占式优先权算法
在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下
去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先
权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统
中。 - 抢占式优先权调度算法
在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要
又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程 ( 原优先权最高的进程 ) 的
执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统
中出现一个新的就绪进程 i 时,就将其优先权 Pi 与正在执行的进程 j 的优先权 Pj 进行比较。如果
Pi ≤ Pj ,原进程 Pj 便继续执行;但如果是 Pi>Pj ,则立即停止 Pj 的执行,做进程切换,使 i 进程投入执
行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格
的实时系统中,以及对性能要求较高的批处理和分时系统中。
区别: 非抢占式( Nonpreemptive ): 让进程运行直到结束或阻塞的调度方式,容易实现,适合专用系统 ,不适合通用系统。抢占式(Preemptive): 允许将逻辑上可继续运行的在运行过程暂停的调度方式可防止单一进程长时间 独占, CPU 系统开销大 (降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序)。
10****简述操作系统如何申请以及管理内存的?
参考回答
操作系统如何管理内存:
- 物理内存 :物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。
寄存器:速度最快、量少、价格贵。
高速缓存:次之。
主存:再次之。
磁盘:速度最慢、量多、价格便宜。
操作系统会对物理内存进行管理,有一个部分称为 内存管理器 (memory manager) ,它的主要工
作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收
内存。 - 虚拟内存 : 操作系统为每一个进程分配一个独立的地址空间 ,但是虚拟内存。 虚拟内存与物理内存存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换 。
操作系统如何申请内存:
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成: *brk 和 mmap。
11简述Linux****系统态与用户态,什么时候会进入系统态?
- 内核态与用户态 : 内核态 (系统态)与 用户态 是操作系统的两种运行级别。内核态拥有最高权限,可以访问所有系统指令;用户态则只能访问一部分指令。
- 什么时候进入内核态 :共有三种方式: a 、 系统调用 。 b 、 异常 。 c 、 设备中断 。 其中,系统调用是主动的,另外两种是被动的。
- 为什么区分内核态与用户态 :在 CPU 的所有指令中, 有一些指令是非常危险的,如果错用,将导致整个系统崩溃。 比如:清内存、设置时钟等。所以区分内核态与用户态主要是出于安全的考虑。
12简述LRU****算法及其实现方式。
参考回答
- LRU 算法 : LRU 算法用于 缓存淘汰 。思路是 将缓存中最近最少使用的对象删除掉
- 实现方式 :利用 链表和 hashmap 。
当需要插入新的数据项的时候,如果新数据项在链表中 存在 (一般称为命中),则 把该节点移到链
表头部 ,如果 不存在 ,则 新建一个节点,放到链表头部 ,若 缓存满了,则把链表最后一个节点删除
即可 。 在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回 -1 。这样一来在链表尾部的节点就是最近最久未访问的数据项。
答案解析
cpp
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);
*/
13****一个线程占多大内存?
参考回答
一个 linux 的线程大概占 8M 内存 。
linux 的栈是通过缺页来分配内存的,不是所有栈地址空间都分配了内存。因此, 8M 是最大消耗,实际的内存消耗只会略大于实际需要的内存( 内部损耗,每个在 4k 以内 ) 。
14****什么是页表,为什么要有?
参考回答
页表是虚拟内存的概念。 操作系统虚拟内存到物理内存的映射表,就被称为页表。
原因 :不可能每一个虚拟内存的 Byte 都对应到物理内存的地址。这张表将大得真正的物理地址也放不下,于是操作系统引入了页(Page )的概念。进行分页,这样可以 减小虚拟内存页对应物理内存页的映射表大小 。
答案解析
如果将每一个虚拟内存的 Byte 都对应到物理内存的地址,每个条目最少需要 8 字节( 32 位虚拟地址 ->32位物理地址),在 4G 内存的情况下,就需要 32GB 的空间来存放对照表,那么这张表就大得真正的物理地址也放不下了,于是操作系统引入了页(Page )的概念。 在系统启动时,操作系统将整个物理内存以 4K 为单位,划分为各个页。之后进行内存分配时,都以页为单位,那么虚拟内存页对应物理内存页的映射表就大大减小了,4G 内存,只需要 8M 的映射表即可,一些进程没有使用到的虚拟内存,也并不需要保存映射关系,而且Linux 还为大内存设计了多级页表,可以进一页减少了内存消耗。
15****简述操作系统中的缺页中断。
参考回答
- 缺页异常 : malloc 和 mmap 函数在分配内存时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异
常,引发缺页中断 。 - 缺页中断 :缺页异常后将产生一个缺页中断,此时操作系统会根据页表中的 外存地址 在外存中找到所缺的一页,将其调入内存 。
答案解析
两者区别。
缺页中断与一般中断一样,需要经历四个步骤: 保护 CPU 现场、分析中断原因、转入缺页中断处理程序、恢复CPU 现场,继续执行。 缺页中断与一般中断区别:
(1)在指令执行期间产生和处理缺页中断信号
(2)一条指令在执行期间,可能产生多次缺页中断
(3)缺页中断返回的是执行产生中断的一条指令,而一般中断返回的是执行下一条指令。
16****说说虚拟内存分布,什么时候会由用户态陷入内核态?
参考回答
- 虚拟内存分布 :
用户空间 :
(1) 代码段 .text : 存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
(2) 数据段 .data : 存放程序中 已初始化 的 全局变量和静态变量 的一块内存区域。
(3) BSS 段 .bss :存放程序中 未初始化的全局变量和静态变量的一块内存区域。
(4) 可执行程序在运行时又会多出两个区域: 堆区和栈区 。
堆区:动态申请内存用。堆从低地址向高地址增长。
栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
(5)最后还有一个 文件映射区 ,位于堆和栈之间。
内核空间: DMA 区、常规区、高位区。 - 什么时候进入内核态 :共有三种方式: a 、 系统调用 。 b 、 异常 。 c 、 设备中断 。其中,系统调用是主动的,另外两种是被动的。
17简述一下虚拟内存和物理内存,为什么要用虚拟内存,好处是什么?
参考回答
- 物理内存 :物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。
寄存器:速度最快、量少、价格贵。
高速缓存:次之。
主存:再次之
磁盘:速度最慢、量多、价格便宜。
操作系统会对物理内存进行管理,有一个部分称为 内存管理器 (memory manager) ,它的主要工
作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收
内存。
2. 虚拟内存 :操作系统为 每一个进程分配一个独立的地址空间 ,但是虚拟内存。虚拟内存与物理内存存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换 。
3. 为什么要用虚拟内存 :因为早期的内存分配方法存在以下问题:
(1) 进程地址空间不隔离。会导致数据被随意修改 。
(2) 内存使用效率低。
(3) 程序运行的地址不确定。操作系统随机为进程分配内存空间 ,所以程序运行的地址是不确定
的。
4. 使用虚拟内存的 好处 :
(1) 扩大地址空间 。每个进程独占一个 4G 空间,虽然 真实物理内存没那么多 。
(2) 内存保护:防止不同进程对物理内存的争夺和践踏,可以对特定内存地址提供写保护,防止
恶意篡改。
(3)可以 实现内存共享,方便进程通信 。
(4)可以 避免内存碎片,虽然物理内存可能不连续,但映射到虚拟内存上可以连续 。
5. 使用虚拟内存的 缺点 :
(1)虚拟内存 需要额外构建数据结构,占用空间 。
(2)虚拟地址到物理地址的转换, 增加了执行时间 。
(3) 页面换入换出耗时 。
(4)一页如果只有一部分数据,浪费内存。
18****虚拟地址到物理地址怎么映射的?
参考回答
操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表。 页表中的每一项都记录了这个页的基地址。
三级页表转换方法:(两步)
- 逻辑地址转线性地址 : 段起始地址 + 段内偏移地址 = 线性地址
- 线性地址转物理地址 :
(1)每一个 32 位的线性地址被划分为三部分:页目录索引( DIRECTORY , 10 位)、页表索引
( TABLE , 10 位)、页内偏移( OFFSET , 12 位)
(2)从 cr3 中取出进程的页目录地址(操作系统调用进程时,这个地址被装入寄存器中)
页目录地址 + 页目录索引 = 页表地址
页表地址 + 页表索引 = 页地址
页地址 + 页内偏移 = 物理地址
按照以上两步法,就完成了一个三级页表从虚拟地址到物理地址的转换。
19****说说堆栈溢出是什么,会怎么样?
参考回答
堆栈溢出就是不顾堆栈中分配的局部数据块大小, 向该数据块写入了过多的数据,导致数据越界 。常指调用堆栈溢出,本质上一种数据结构的满溢情况。堆栈溢出可以理解为两个方面:堆溢出和栈溢出。
- 堆溢出:比如不断的 new 一个对象,一直创建新的对象,而不进行释放,最终导致内存不足。将会报错:OutOfMemory Error 。
- 栈溢出:一次函数调用中,栈中将被依次压入:参数,返回地址等,而方法如果递归比较深或进去死循环,就会导致栈溢出。将会报错:StackOverflow Error 。
20简述操作系统中malloc****的实现原理
参考回答
malloc 底层实现: 当开辟的空间小于 128K 时,调用 brk ()函数;当开辟的空间大于 128K 时,调用mmap()。 malloc 采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。
21 说说进程空间从高位到低位都有些什么?
参考回答
如上图, 从高地址到低地址,一个程序由命令行参数和环境变量、栈、文件映射区、堆、 BSS 段、数据 段、代码段组成。
- 命令行参数和环境变量
- 栈区: 存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
- 文件映射区 ,位于堆和栈之间。
- 堆区: 动态申请内存用。堆从低地址向高地址增长。
- BSS 段 :存放程序中未初始化的全局变量和静态变量的一块内存区域。
- 数据段 :存放程序中已初始化的全局变量和静态变量的一块内存区域。
- 代码段: 存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
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的内存。
23****请你说说并发和并行
参考回答
- 并发 :对于单个 CPU ,在一个时刻只有一个进程在运行,但是线程的切换时间则减少到纳秒数量级,多个任务不停来回快速切换。
- 并行 :对于多个 CPU ,多个进程同时运行。
- 区别 。通俗来讲,它们虽然都说是 " 多个进程同时运行 " ,但是它们的 " 同时 " 不是一个概念。并行
的 " 同时 " 是同一时刻可以多个任务在运行 ( 处于 running) ,并发的 " 同时 " 是经过不同线程快速切换,
使得看上去多个任务同时都在运行的现象。
24****说说进程、线程、协程是什么,区别是什么?
参考回答
- 进程 :程序是指令、数据及其组织形式的描述,而进程则是程序的运行实例,包括程序计数器、寄存器和变量的当前值。
- 线程 :微进程,一个进程里更小粒度的执行单元。一个进程里包含多个线程并发执行任务。
- 协程 :协程是微线程,在子程序内部执行,可在子程序内部中断,转而执行别的子程序,在适当的时候再返回来接着执行。
区别 : - 线程与进程的区别 :
(1)一个线程从属于一个进程;一个进程可以包含多个线程。
(2) 一个线程挂掉,对应的进程挂掉 ;一个进程挂掉,不会影响其他进程。
(3) 进程是系统资源调度的最小单位 ; 线程 CPU 调度的最小单位 。
(4)进程系统开销显著大于线程开销;线程需要的系统资源更少。
(5) 进程在执行时拥有独立的内存单元,多个线程共享进程的内存 ,如代码段、数据段、扩展
段;但每个线程拥有自己的栈段和寄存器组。
(6) 进程切换时需要刷新 TLB 并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。
(7)通信方式不一样。
(8)进程适应于多核、多机分布;线程适用于多核 - 线程与协程的区别:
(1) 协程执行效率极高 。协程 直接操作栈基本没有内核切换的开销 ,所以上下文的切换非常快,
切换开销比线程更小。
(2) 协程不需要多线程的锁机制,因为多个协程从属于一个线程,不存在同时写变量冲突,效率
比线程高。
(3)一个线程可以有多个协程。
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() 函数会返回两次,一次在父进程中返回,一次在子进程中返回,两次返回的值不一样,如上面的三种情况。
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 )
27 请你说说什么是守护进程,如何实现?
参考回答
- 守护进程 :守护进程是 运行在后台的一种生存期长的特殊进程。它独立于控制终端,处理一些系统级别任务。
- 如何实现 :
(1)创建子进程,终止父进程。方法是调用 fork() 产生一个子进程,然后使父进程退出。
(2)调用 setsid() 创建一个新会话。
(3) 将当前目录更改为根目录 。使用 fork() 创建的子进程也继承了父进程的当前工作目录。
(4)重设文件权限掩码。文件权限掩码是指屏蔽掉文件权限中的对应位。
(5)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。
实现代码如下:
cpp
#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;
}
28****说说进程通信的方式有哪些?
参考回答
进程间通信主要包括 管道 、 系统 IPC (包括消息队列、信号量、信号、共享内存)、 套接字 socket 。
- 管道 :包括无名管道和命名管道,无名管道半双工,只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件;命名管道可以允许无亲缘关系进程间的通信。
- 系统 IPC
消息队列 :消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列
及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
信号量 semaphore :是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现
进程间的互斥与同步。
信号 :用于通知接收进程某个事件的发生。
内存共享 :使多个进程访问同一块内存空间。
29****说说进程同步的方式?
参考回答
- 信号量 semaphore :是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现 进程间的互斥与同步。P 操作 ( 递减操作 ) 可以用于阻塞一个进程, V 操作 ( 增加操作 ) 可以用于解除阻 塞一个进程。
- 管道 :一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行, 调用管程的任何其他进程都被阻塞,以等待管程可用。
- 消息队列 :消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
30说说Linux****进程调度算法及策略有哪些?
参考回答
- 先来先服务调度算法
- 短作业 ( 进程 ) 优先调度算法
- 高优先级优先调度算法
- 时间片轮转法
- 多级反馈队列调度算法
答案解析 - 先来先服务调度算法:每次调度都是从后备作业(进程)队列中选择一个或多个最先进入该队列的 作业(进程),将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。
- 短作业 ( 进程 ) 优先调度算法:短作业优先 (SJF) 的调度算法是从后备队列中选择一个或若干个估计运 行时间最短的作业(进程),将它们调入内存运行。
- 高优先级优先调度算法:当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高 的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程
- 时间片轮转法:每次调度时,把 CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几 ms 到几百 ms 。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来
停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进
程,同时也让它执行一个时间片。 - 多级反馈队列调度算法:综合前面多种调度算法。
在这些调度算法中,有抢占式和非抢占式的区别。 - 非抢占式优先权算法
在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下
去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先
权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统
中。 - 抢占式优先权调度算法
在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要
又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程 ( 原优先权最高的进程 ) 的
执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统
中出现一个新的就绪进程 i 时,就将其优先权 Pi 与正在执行的进程 j 的优先权 Pj 进行比较。如果
Pi ≤ Pj ,原进程 Pj 便继续执行;但如果是 Pi>Pj ,则立即停止 Pj 的执行,做进程切换,使 i 进程投入执
行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格
的实时系统中,以及对性能要求较高的批处理和分时系统中。
非抢占式( Nonpreemptive ):让进程运行直到结束或阻塞的调度方式,容易实现,适合专用系统,不 适合通用系统。
抢占式(Preemptive ):允许将逻辑上可继续运行的在运行过程暂停的调度方式可防止单一进程长时间 独占,CPU 系统开销大(降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序)
31****说说进程有多少种状态?
参考回答
进程有五种状态: 创建、就绪、执行、阻塞、终止 。一个进程创建后,被放入队列处于就绪状态,等待 操作系统调度执行,执行过程中可能切换到阻塞状态(并发),任务完成后,进程销毁终止。
答案解析
创建状态
一个应用程序从系统上启动,首先就是进入 创建状态 ,需要获取系统资源创建进程管理块( PCB :
Process Control Block )完成资源分配。
就绪状态
在 创建状态 完成之后,进程已经准备好,处于 就绪状态 ,但是还未获得处理器资源,无法运行。
运行状态
获取处理器资源,被系统调度, 当具有时间片 开始进入 运行状态 。如果进程的时间片用完了就进入 就绪 状态 。
阻塞状态
在 运行状态 期间,如果进行了阻塞的操作,如耗时的 I/O 操作,此时进程暂时无法操作就进入到了 阻塞状 态 ,在这些操作完成后就进入 就绪状态 。等待再次获取处理器资源,被系统调度, 当具有时间片 就进入 运行状态 。
终止状态
进程结束或者被系统终止,进入 终止状态
相互转换如图
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 创建子进程 , 那么子进程也有两个文件描述符指向同一管道。
- 父进程关闭管道读端 , 子进程关闭管道写端。父进程可以往管道里写 , 子进程可以从管道里读 , 管道是 用环形队列实现的, 数据从写端流入从读端流出 , 这样就实现了进程间通信。
答案解析
给出实现的代码,实现父子进程间的管道通信
cpp
#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;
}
33简述mmap****的原理和使用场景
参考回答
原理 : mmap 是一种内存映射文件的方法 ,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read, write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图:
使用场景 :
- 对同一块区域频繁读写操作;
- 可用于实现用户空间和内核空间的高效交互
- 可提供进程间共享内存及相互通信
- 可实现高效的大规模数据传输。
34****互斥量能不能在进程中使用?
参考回答
能 。
不同的进程之间, 存在资源竞争或并发使用的问题 ,所以需要 互斥量 。
进程中也需要 互斥量 ,因为一个进程中可以包含多个线程,线程与线程之间需要通过互斥的手段进行同步,避免导致共享数据修改引起冲突。可以使用互斥锁 ,属于互斥量的一种。
35****协程是轻量级线程,轻量级表现在哪里?
参考回答
- 协程调用跟切换比线程效率高 :协程执行效率极高。协程不需要多线程的锁机制,可以不加锁的访问全局变量,所以上下文的切换非常快。
- 协程占用内存少 :执行协程只需要极少的栈内存(大概是 4 ~ 5KB ),而默认情况下,线程栈的大小为1MB 。
- 切换开销更少 :协程直接操作栈基本没有内核切换的开销,所以切换开销比线程少。
36****说说常见信号有哪些,表示什么含义?
参考回答
编号为1 ~ 31 的信号为传统 UNIX 支持的信号,是不可靠信号 ( 非实时的 ) 。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。编号为1 ~ 31 的信号如下:
而常见信号如下 :
其中最重要的就是 "1"、"9"、"15"、"17"这几个信号。
37****说说线程间通信的方式有哪些?
参考回答
线程间的通信方式包括 临界区、互斥量、信号量、条件变量、读写锁 :
- 临界区:每个线程中访问临界资源的那段代码称为临界区( Critical Section )(临界资源是一次仅允许一个线程使用的共享资源)。每次只准许一个线程进入临界区,进入后不允许其他线程进入。
不论是硬件临界资源,还是软件临界资源,多个线程必须互斥地对它进行访问。 - 互斥量:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
- 信号量:计数器,允许多个线程同时访问同一个资源。
- 条件变量:通过条件变量通知操作的方式来保持多线程同步。
- 读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。
38 说说线程同步方式有哪些?
参考回答
线程间的同步方式包括 互斥锁、信号量、条件变量、读写锁 :
- 互斥锁 :采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
- 信号量 :计数器,允许多个线程同时访问同一个资源。
- 条件变量 :通过条件变量通知操作的方式来保持多线程同步。
- 读写锁 :读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。
39 说说什么是死锁,产生的条件,如何解决?
参考回答 - 死锁 : 是指多个进程在执行过程中, 因争夺资源而造成了互相等待 。此时系统产生了死锁。比如两只羊过独木桥,若两只羊互不相让,争着过桥,就产生死锁。
- 产生的条件 :死锁发生有 四个必要条件 :
(1) 互斥条件 :进程对所分配到的资源不允许其他进程访问,若其他进程访问,只能等待,直到
进程使用完成后释放该资源;
(2) 请求保持条件 :进程获得一定资源后,又对其他资源发出请求,但该资源被其他进程占有,
此时请求阻塞,而且该进程不会释放自己已经占有的资源;
(3) 不可剥夺条件 :进程已获得的资源,只能自己释放,不可剥夺;
(4) 环路等待条件 :若干进程之间形成一种头尾相接的循环等待资源关系。 - 如何解决 :
(1)资源一次性分配,从而解决请求保持的问题
(2)可剥夺资源:当进程新的资源未得到满足时,释放已有的资源;
(3)资源有序分配:资源按序号递增,进程请求按递增请求,释放则相反。
答案解析
举个例子,比如:如果此时有两个线程 T1 和 T2 ,它们分别占有 R1 和 R2 资源
此时, T1 请求 R2 资源的同时, T2 请求 R1 资源。
这个时候 T2 说:你把 R1 给我,我就给你 R2。
T1 说:不行,你要先给我 R2 ,我才能给你 R1
那么就这样,死锁产生了。如下图:
40 有了进程,为什么还要有线程?
参考回答 - 原因
进程在早期的多任务操作系统中是基本的 执行单元 。每次进程切换,都要先保存进程资源然后再恢
复,这称为上下文切换。 但是进程频繁切换将引起额外开销,从而严重影响系统的性能。 为了减少
进程切换的开销,人们把两个任务放到一个进程中,每个任务用一个更小 粒度 的执行单元来实现并
发执行,这就是 线程 。 - 线程与进程对比
(1) 进程间的信息难以共享。 由于除去只读代码段外,父子进程并未共享内存,因此必须采用一
些进程间通信方式,在进程间进行信息交换。
但 多个线程共享 进程的内存,如代码段、数据段、扩展段,线程间进行信息交换十分方便。
(2)调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页
表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。
但创建线程比创建进程通常要快 10 倍甚至更多。 线程间是共享虚拟地址空间的,无需采用写时复
制来复制内存,也无需复制页表。
41****单核机器上写多线程程序,是否要考虑加锁,为什么?
参考回答
在单核机器上写多线程程序,仍然需要线程锁。
原因 :因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会 导致共享数据修改引起冲突。
42****说说多线程和多进程的不同?
参考回答
(1)一个线程从属于一个进程;一个进程可以包含多个线程。
(2)一个线程挂掉,对应的进程挂掉,多线程也挂掉;一个进程挂掉,不会影响其他进程,多进程稳
定。
(3)进程系统开销显著大于线程开销;线程需要的系统资源更少。
(4)多个进程在执行时拥有各自独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展
段;但每个线程拥有自己的栈段和寄存器组。
(5)多进程切换时需要刷新 TLB 并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只
需要切换硬件上下文和内核栈。
(6)通信方式不一样。
(7)多进程适应于多核、多机分布;多线程适用于多核
43****简述互斥锁的机制,互斥锁与读写的区别?
参考回答
- 互斥锁机制 : mutex ,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败
时,线程会进入睡眠,等待锁释放时被唤醒。 - 互斥锁和读写锁 :
(1) 读写锁区分读者和写者,而互斥锁不区分
(2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写
者,但是允许多个读者同时读对象。
答案解析
原理详解 :
互斥锁其实就是一个 bool 型变量,为 true 时表示锁可获取,为 false 时表示已上锁。这里说的是 互斥锁 , 其实是泛指linux 中所有的锁机制。 我们采用互斥锁保护临界区,从而防止竞争条件。也就是说,一个线程在进入临界区时应得到锁;它在退出临界区时释放锁。函数 acquire() 获取锁,而函数 release() 释放锁,如图 :
每个互斥锁有一个布尔变量 available ,它的值表示锁是否可用。如果锁是可用的,那么调用 acquire() 会成功,并且锁不再可用。当一个线程试图获取不可用的锁时,它会阻塞,直到锁被释放。
按如下定义 acquire() :
cpp
acquire() {
while (!available);
/* busy wait */
available = false;
}
按如下定义 release() :
release () {
available = true ;
}
44****说说什么是信号量,有什么作用?
参考回答
- 概念 : 信号量本质上是一个计数器,用于多进程对共享数据对象的读取,它主要是用来保护共享资源( 信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
- 原理 :由于信号量只能进行两种操作等待和发送信号,即 P(sv) 和 V(sv) ,具体的行为如下:
(
1 ) P(sv) 操作:如果 sv 的值大于零,就给它减 1 ;如果它的值为零,就挂起该进程的执行(信号量
的值为正,进程获得该资源的使用权,进程将信号量减 1 ,表示它使用了一个资源单位)。
(
2 ) V(sv) 操作:如果有其他进程因等待 sv 而被挂起,就让它恢复运行,如果没有进程因等待 sv 而
挂起,就给它加 1 (若此时信号量的值为 0 ,则进程进入挂起状态,直到信号量的值大于 0 ,若进程
被唤醒则返回至第一步)。 - 作用 :用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资
源),使得资源在一个时刻只有一个进程独享。
45 进程、线程的中断切换的过程是怎样的?
参考回答
上下文切换指的是内核(操作系统的核心)在 CPU 上对进程或者线程进行切换。 - 进程上下文切换
(1)保护被中断进程的处理器现场信息
(2)修改被中断进程的进程控制块有关信息,如进程状态等
(3)把被中断进程的进程控制块加入有关队列
(4)选择下一个占有处理器运行的进程
(5)根据被选中进程设置操作系统用到的地址转换和存储保护信息
切换页目录以使用新的地址空间
切换内核栈和硬件上下文(包括分配的内存,数据段,堆栈段等)
(6)根据被选中进程恢复处理器现场 - 线程上下文切换
(1)保护被中断线程的处理器现场信息
(2)修改被中断线程的线程控制块有关信息,如线程状态等
(3)把被中断线程的线程控制块加入有关队列
(4)选择下一个占有处理器运行的线程
(5)根据被选中线程设置操作系统用到的存储保护信息
切换内核栈和硬件上下文(切换堆栈,以及各寄存器)
(6)根据被选中线程恢复处理器现场
46****简述自旋锁和互斥锁的使用场景
参考回答
- 互斥锁 用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑
(1)临界区有 IO 操作
(2)临界区代码复杂或者循环量大
(3)临界区竞争非常激烈
(4)单核处理器 - 自旋锁就 主要用在临界区持锁时间非常短且 CPU 资源不紧张的情况下。
47 请你说说线程有哪些状态,相互之间怎么转换?
参考回答
类似进程,有以下五种状态: - 新建状态 (New)
- 就绪状态 (Runnable)
- 运行状态 (Running)
- 阻塞状态 (Blocked)
- 死亡状态 (Dead)
48****多线程和单线程有什么区别,多线程编程要注意什么,多线程加锁
需要注意什么?
参考回答
(1)多线程从属于一个进程,单线程也从属于一个进程;一个线程挂掉都会导致从属的进程挂
掉。
(2)一个进程里有多个线程,可以并发执行多个任务;一个进程里只有一个线程,就只能执行一
个任务。
(3)多线程并发执行多任务,需要切换内核栈与硬件上下文,有切换的开销;单线程不需要切
换,没有切换的开销。
(4)多线程并发执行多任务,需要考虑同步的问题;单线程不需要考虑同步的问题。
2. 多线程编程需要考虑 同步 的问题。线程间的同步方式包括 互斥锁、信号量、条件变量、读写锁 。
3. 多线程加锁,主要需要注意 死锁 的问题。破坏死锁的必要条件从而避免死锁。
49说说sleep和wait****的区别?
参考回答
- sleep
sleep 是一个 延时函数 , 让进程或线程进入休眠。休眠完毕后继续运行 。
在 linux 下面, sleep 函数的参数是秒,而 windows 下面 sleep 的函数参数是毫秒。
windows 下面 sleep 的函数参数是毫秒。
例如:
#include <windows.h> // 首先应该先导入头文件
Sleep ( 500 ) ; // 注意第一个字母是大写。
// 就是到这里停半秒,然后继续向下执行。
- wait
wait 是父进程回收子进程 PCB 资源的一个系统调用。进程一旦调用了 wait 函数,就立即阻塞自己
本身 ,然后由 wait 函数自动分析当前进程的某个子进程是否已经退出,当找到一个已经变成僵尸的
子进程, wait 就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进
程, wait 就会一直阻塞,直到有一个出现为止。函数原型如下:
子进程的结束状态值会由参数 status 返回,而子进程的进程识别码也会一起返回。如果不需要结束
状态值,则参数 status 可以设成 NULL 。 - 区别 :
(1) sleep 是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。
(2) wait 是父进程回收子进程 PCB ( Process Control Block )资源的一个系统调用。
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 个普通线程来处理多余的任务。
以上是线程池的工作流程。
51****进程和线程相比,为什么慢?
参考回答
- 进程系统开销显著大于线程开销;线程需要的系统资源更少。
- 进程切换开销比线程大。多进程切换时需要刷新 TLB 并获取新的地址空间,然后切换硬件上下文和 内核栈;多线程切换时只需要切换硬件上下文和内核栈。
- 进程通信比线程通信开销大。进程通信需要借助管道、队列、共享内存,需要额外申请空间,通信 繁琐;而线程共享进程的内存,如代码段、数据段、扩展段,通信快捷简单,同步开销更小。
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 零拷贝来实现。