其他后端所需技术栈个人总结链接汇总:
1.1 Linux 中查看进程运行状态的指令、查看内存使用情况的指令、tar 解压文件的参数。
- 查看进程运行状态: ps 命令。“ps -aux | grep PID”,用来查看某 PID 进程状态。
- 查看内存使用情况: free 命令。“free -m”
- tar 解压文件的参数:
注意:查看进程状态、查看内存使用情况的指令均可使用 top 指令。# 五个命令中必选一个 -c:建立压缩 -x:解压 -t:查看内容 -r:向压缩归档文件末尾追加文件 -u:更新原压缩包中的文件 # 这几个参数是可选的 -z:有 gzip 属性的 -j:有 bz2 属性的 -z:有 compress 属性的 -v:显示所有过程 -o:将文件解开到标准输出
1.2 文件权限怎么修改
- 文件权限描述: Linux 中的基本权限只有九个,分别是
owner/group/others
三种身份各有自己的read/write/execute
权限 - 修改权限指令:chmod
- chmod 语法:
chmod [-R] xyz 文件或目录 选项与参数: xyz: 就是数字模型的权限属性,为 rwx 属性数值的相加。 -R:进行递归(recursive)的持续变更,亦即连同次目录下的所有文件都会变更 例: chmod 770 dy.c # 即修改 dy.c 文件的权限为 770
1.3 说说常用的 LInux 命令
- cd:切换当前目录
- ls:查看当前文件与目录
- grep:改名
- cp:复制
- mv:移动文件或文件夹
- rm:删除文件或文件夹
- ps:查看进程情况
- netstat:查看网络相关信息
- kill:向进程发送终止信号
- tar:对文件进行压缩或解压
- cat:查看文件内容,与less、more功能类似
- top:可以查看操作系统的信息,如进程、CPU 占用率、内存信息等
- pwd:显示当前工作目录
- ./a.cpp &:使 a.cpp 在后台运行
1.4 如何以 root 权限运行某个程序
sudo chown root app(文件名)
sudo chmod u+s app(文件名)
1.5 软链接和硬链接的区别
软链接 | 硬链接 | |
---|---|---|
定义 | 又称符号链接,这个文件包含了另一个文件的路径名 | 硬链接就是一个文件的一个或多个文件名 |
限制 | 可以对不存在的文件或目录创建软链接,可交叉文件系统 | 只能对已存在的文件进行创建,不能交叉文件系统 |
创建方式 | 可以对文件或目录创建 | 不能对目录进行创建,只可对文件创建 |
影响 | 删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软链接被称为死链接。 | 删除一个硬链接文件并不影响其他有相同 inode 号的文件 |
1.6 静态库和动态库怎么制作及如何使用,区别是什么?
制作和使用:
静态库 | 动态库 | |
---|---|---|
编译 | g++ -c a.c b.c | g++ -c -fpic a.c b.c |
封装成库 | ar rcs libxxx.a a.o b.o | g++ -shared a.o b.o -o libxxx.so |
生成可执行文件 | g++ -o main -I ./include/(.h头文件目录) -L ./lib/(静态库所在目录) -l js(静态库名称) | g++ -o main -I ./include/(.h头文件目录) -L ./lib/(动态库所在目录) -l js(动态库名称) |
优点 | 1、静态库被打包到应用程序中加载速度快。2、发布程序无需提供静态库,移植方便 | 1、可以实现进程间资源共享(共享库)。2、更新、部署、发布简单。3、可以控制何时加载动态库 |
缺点 | 1、消耗系统资源,浪费内存。2、更新、部署、发布麻烦 | 1、加载速度比静态库慢。2、发布程序时需要提供依赖的动态库 |
动态链接库把动态库所在绝对位置加入到 /etc/ld.so.cache
sudo vim ld.so.conf // 将绝对位置加入
sudo ldconfig // 保存修改
区别:
- 静态链接:
- 描述: 静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中
- 生成文件类型: Windows 下以 .lib 为后缀,Linux 下以 .a 为后缀
- 优点: (1):加载速度快。(2):发布程序无需提供静态库,移植方便。
- 缺点: (1):消耗系统资源,浪费内存。(2):更新、部署、发布麻烦。
- 动态链接:
- 描述: 动态链接,在链接的时候没有直接把调用的函数代码链接进去(只包含函数的重定位信息),而是在执行的过程中,通过函数的重定位信息找到要链接的函数。
- 生成文件类型: Windows 下以 .dll 为后缀,Linux 下以 .so 为后缀。
- 优点: (1):可以实现进程间资源共享(共享库)。(2):更新、部署、发布简单。(3):可以控制何时加载动态库。
- 缺点: (1):加载速度比静态库慢。(2):发布程序时需要提供依赖的动态库。
1.7 简述 GDB 常见调试命令,什么是条件断点,多进程下如何调试
GDB 调试描述: GDB 调试的是可执行文件,在编译时加入 -g,告诉 gcc/g++ 在编译时加入调试信息,这样 GDB 才能调试这个被编译的文件。例:g++ -g test.c -o test
GDB 命令格式:
-
启动和退出
gdb 可执行程序 // 启动
quit // 退出
list // 查看当前文件代码
-
断电操作
b 行号、b 文件名:行号 // 设置断点
b // 查看断点
d 断点编号 // 删除断点
b 10 if i==5 // 设置条件断点
-
调试命令
run // 运行GDB程序,遇到断点才停
c // 继续运行,到下一个断点停
n // 向下执行一行代码
s // 向下单步调试
p/print 变量名 // 打印变量值
ptype 变量名 // 打印变量类型
until // 跳出循环
-
多进程调试
show/set fellow-fork-mode [parent | child] // 展示/设置GDB调试父进程还是子进程,**最重要**
show/set detack-on-fork [on | off] // 展示/设置其他进程是否被GDB挂起
info inferiors // 查看调试的进程
inferior id // 切换当前调试的进程
detach inferiors id // 使进程脱离 GDB 调试
1.8 说说什么是大端小端,如何判断大端小端?
- 概念:
- 小端模式:低的有效字节存储在低的存储器地址。主机字节序一般采用小端
- 大端模式:高的有效字节存储在低的存储器地址。网络字节序一般采用大端
- 如何判断: 可以采用联合体来判断
int fun1(){ union test{ char c; int i; }; test t; t.i = 1; // 如果是大端,则 t.c 为 0x00,即 t.c != 1,反之是小端 return (t.c == 1) }
- 经测,ubuntu 采用的是小端
1.8.1 在进行网络通信时是否需要进行字节序转换?
相同字节序的平台在进行网络通信时可以不进行字节序交换,但是跨平台进行网络通信时必须进行字节序转换。
1.9 进程调度算法有哪些?
- 先来先服务调度算法
- 短作业优先调度算法
- 高优先级优先调度算法
- 时间片轮转算法
- 多级反馈队列调度算法
1.9.1 非抢占式和抢占式
非抢占式: 让进城运行直到结束或阻塞的调度方式,容易实现,适合专用系统,不适合通用系统
抢占式: 允许将逻辑上可继续运行的进程在运行过程中暂停的调度方式,可防止单一进程长时间独占 CPU。(降低途径:硬件实现进程切换,或扩充主存以储存大部分程序)
1.10 简述操作系统如何申请以及管理内存的?
- 如何管理内存:
- 物理内存: 有四个层次,分别是寄存器、高速缓存、主存(内存)、磁盘。
- 寄存器:速度最快、容量小、价格贵
- 高速缓存:次之
- 主存:再次之
- 磁盘:速度最慢、容量大、价格便宜
- 虚拟内存: 操作系统为每一个进程分配一个独立的地址空间(在 Linux 中的大小是 4G),就是虚拟内存。虚拟内存与物理内存之间存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换。
- 物理内存: 有四个层次,分别是寄存器、高速缓存、主存(内存)、磁盘。
- 如何申请内存:
由两个系统调用完成,*brk 和 mmap(内存映射)
1.11 说说堆栈溢出是什么?会怎么样?
向数据块写入过多的数据,导致数据越界。主要分为两个方面:堆溢出和栈溢出
- 堆溢出: 不断的 new 一个对象而不释放,最终导致内存不足,将会报错:OutOfMemory Error。
- 栈溢出: 如果函数递归比较深或者进入死循环,就会导致栈溢出。将会报错:StackOverflow Error。
1.12 简述 Linux 系统态与用户态,什么时候会进入系统态
内核态与用户态: 内核态与用户态是操作系统的两种运行级别,内核态拥有最高权限,可以访问所有指令;用户态则只能访问一部分指令。
对应虚拟内存: 用户处于内核态的时候,既能访问用户区的数据(栈、堆),又能访问内核区的数据(进程控制块)。
什么时候进入内核区:
- 系统调用(主动)
- 设备中断(被动)
- 异常(被动)
为什么区分内核态和用户态: CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,只允许操作系统及其相关模块操作(也就是内核态),就能尽量避免这种问题。
1.13 简述 LRU 算法及其实现方式
LRU 算法: 俗称最近最久未使用算法,主要用于缓存淘汰,每次都是将最近最少使用的对象删除掉。
实现方式:双向链表和哈希表
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(); // 映射头部
}
};
1.14 32 位系统能访问 4GB 以上的内存吗?
正常情况是不可以的, 在 32 位的系统中,总共是 4G。其他原因还需要占用一部分内存,所以内存只能识别 3G 多。
使用 PAE 技术就可以实现 32 位系统能访问 4GB 以上的内存。
PAE(Physical Address Extension): 通过将地址扩展到 36 位。这样系统就能容纳 64GB 的内存。
1.15 说说并行和并发
并行: 多个进程同一时间同时执行。
并发: 多个进程同一时间段内执行,但是同一时间只有一个进程在执行。
------------------虚拟内存相关问题---------------------------------
2.1 虚拟内存的目的、特征?
传统物理内存: 寄存器、高速缓存、主存和磁盘
- 传统存储管理方式的缺点:
- 一次性:作业数据必须一次全部调入内存
- 驻留性:作业数据在整个运行期间都会常驻内存
- 虚拟内存的主要目的: 为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。(可以看虚拟性)
- 虚拟内存的概述: 程序不需全部装入即可运行,运行时根据需要动态调入数据,若内存不够,还需换出一些数据。
- 虚拟内存的特征:
- 多次性: 无须在作业运行时一次性全部装入内存,而是允许被分成多次调入内存。
- 对换性: 无须在作业运行时一直常驻内存,而是允许在作业运行过程中,将作业换入换出。
- 虚拟性: 从逻辑上扩充了内存的容量,使用户看到的内存容量远大于实际的容量。
- 虚拟内存的所需功能:
- 请求调页功能(用来解决一次性问题): 访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存。
- 页面置换功能(用来解决驻留性问题): 将内存中暂时用不到的信息换出到外存。
- 实现技术:
- 请求分页存储管理
- 请求分段存储管理
- 请求段页式存储管理(三次访存),现在的系统大多采用的就是这种方式。一个进程对应一个段表,可能对应多个页表。
2.2 虚拟内存常用的页面置换算法
- 最佳置换算法(OPT)
- 先进先出置换算法(FIFO)
- 最近最久未使用置换算法(LRU)
- 时钟置换算法(CLOCK)
- 改进型的时钟置换算法
2.3 抖动你知道是什么吗?它也叫颠簸现象
概述: 刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出内存,这种频繁的页面调度行为称为抖动,或颠簸。
主要原因: 产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够)
解决方案: 为进程分配的物理块太少,会使进程发生抖动现象。为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率。为了研究应该为每个进程分配多少个物理块,Denning 提出了"进程工作集"的概念
2.4 在发生内存交换时,那些进程应该被优先考虑?
- 可优先换出阻塞进程
- 可换出优先级低的进程
- 为了防止优先级低的进程在被调入内存后很快又被换出,有的系统还会考虑进程在内存的驻留时间…
注意: PCB 会常驻内存,不会被换出内存
2.5 内存交换你知道有哪些需要注意的关键点吗?
- 交换需要备份存储,通常是快速磁盘,它必须足够大,并且提供对这些内存映像的直接访问。
- 为了有效使用 CPU,需要每个进程的执行时间比交换时间长,而影响交换时间的主要是转移时间,转移时间与所交换的空间内存成正比。
- 如果换出进程,比如确保该进程的内存空间成正比。
- 交换空间通常作为磁盘的一整块,且独立于文件系统,因此使用就可能很快。
- 交换通常在有许多进程运行且内存空间吃紧时开始启动,而系统负荷降低就暂停。
- 普通交换使用不多,但交换的策略的某些变种在许多系统中(如 UNIX 系统)仍然发挥作用
2.6 动态分配内存空间常见算法
- 首次适配
- 最佳适配
- 最坏适配
2.7 什么是页表,为什么要有?
概念: 页表是虚拟内存的概念:操作系统虚拟内存到物理内存的映射表
原因: 如果每个字节 Byte 都对应到物理内存,映射表就太大了,通过引入页的概念,进行分页,可以减少虚拟内存对应物理内存页的映射表大小。
2.8 简述操作系统中的缺页中断
缺页异常: 当访问没有在虚拟内存中的内容时,处理器会触发缺页异常,产生缺页中断。
缺页中断: 操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
2.9 缺页中断与一般中断的异同
相同点: 需要经历四个步骤:
- 保护 CPU 现场
- 分析中断原因
- 载入缺页中断处理程序
- 恢复 CPU 现场
不同点:
- 在指令执行期间产生和处理缺页中断信号
- 一条指令在执行期间,可能产生多次缺页中断
- 缺页中断返回的是执行产生中断的这一条指令,而一般中断返回的是执行下一条指令。
--------------------进程相关问题---------------------------------
3.1 进程、线程和协程
概念:
- 进程: 进程是程序的运行实例。
- 线程: 微进程,一个进程里更小的执行单元。
- 协程: 微线程,子程序内部执行,可在子程序内部中断,转而执行别的子程序,在适当的时候再返回来接着执行。
区别:
- 线程与进程的区别:
- 进程是资源调度的最小单位,线程是 CPU 调度的最小单位
- 一个进程包含多个线程
- 一个进程挂掉不会影响其他的进程,一个线程挂掉,对应的进程挂掉
- 进程系统开销和需要的系统资源较大
- 多个线程共享进程的一部分内存(代码段),每个线程拥有自己的栈区和寄存器区。
- 线程间通信比较方便
- 进程切换时需要刷新 TLB(设备缓冲区)并获取新的地址空间,然后切换硬件上下文和内核栈。线程不需要第一步。
- 线程与协程的区别:
- 一个线程可以有多个协程。
- 协程执行效率极高,它可以直接操作栈,基本没有内核切换的开销。
- 协程不需要多线程的锁机制,因为多个协程从属于一个线程,不存在同时写变量冲突,效率比线程高。
3.2 有了进程,为什么还要有线程?
原因: 进程频繁切换会引起很多的额外开销,会严重影响系统的性能。
线程与进程对比:
- 通信: 进程间通讯不容易实现,线程间由于有共享区域,较容易实现通信。
- 创建代价: 进程创建资源较多,并且线程创建速度通常比进程快 10 倍甚至更多。
- 切换代价: 多进程切换时需要刷新 TLB 并获取新的地址空间。
3.3 进程、线程的中断切换的过程是怎样的?
上下文切换指的是内核(操作系统的核心)在 CPU 上对进程或者线程进行切换
- 进程上下文切换:
- 保护被中断进程的处理器现场信息
- 修改被中断进程的进程控制块有关信息,如进程状态等
- 把被中断进程的进程控制块加入有关队列
- 选择下一个占有处理器运行的进程
- 根据被选中进程设置操作系统用到的地址转换和存储保护信息
- 切换页目录以使用新的地址空间
- 切换内核栈和硬件上下文(包括分配的内存、数据段、堆栈段等)
- 根据被选中进程恢复处理器现场
- 线程上下文切换:
- 保护被中断线程的处理器现场信息
- 修改被中断线程的线程控制块有关信息,如线程状态等。
- 把被中断线程的线程控制块加入有关队列
- 选择下一个占有处理器运行的线程
- 根据被选中线程设置操作系统用到的存储保护信息
- 切换内核栈和硬件上下文(切换堆栈,以及各寄存器)
- 根据被选中线程恢复处理器现场。
3.4 协程是轻量级线程,轻量级表现在那里?
- 协程调用和切换比线程效率高: 协程执行效率极高,协程不需要多线程的锁机制,可以不加锁的访问全局变量,所以上下文的切换非常快。
- 协程占用内存少: 协程所需栈内存(4~5KB),线程栈的大小为 1MB。
- 切换开销更少: 协程直接操作栈基本没有内核切换的开销,所以切换开销比线程少。
3.5 一个线程占多大内存?
Linux 中大概占 8M 内存,它是通过缺页来分配内存的。
扩充: 一个进程的多个线程,它们在用户区非共享区域主要是栈和寄存器段
3.6 说一说 Linux 的 fork 的作用
fork 函数是由来创建一个子进程,对于父进程,返回的是所创子进程的 PID,对于子进程,返回值是 0,创建失败则会返回 -1。通常 Linux 采用的是写时拷贝
3.7 说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程?
概念:
- 孤儿进程: 父进程结束了,而它的子进程没有结束,这个时候子进程就是孤儿进程。
- 僵尸进程: 子进程结束了,父进程还没有结束,并且没有去释放子进程所占用的资源,这个时候子进程就是僵尸进程。
解决方法:
- 使用 SIGCHLD 信号处理函数,因为每当子进程结束的时候,内核就会给父进程发送 SIGCHLD 信号,我们可以通过信号处理函数去释放子进程的资源。(wait 函数)
3.8 说说什么是守护进程,如何实现?
守护进程: 是 Linux 中的后台服务进程,它独立于控制终端,处理一些系统级别任务。
如何实现:
- 创建子进程,然后退出父进程。
- 子进程调用 setsid() 开启一个新会话
- 将当前目录更改为根目录
- 重设文件权限掩码
- 关闭不需要的文件描述符
3.9 Linux 进程调度算法及策略有哪些?
- 先来先服务算法
- 短作业优先
- 高优先级优先调度算法
- 时间片轮转算法
- 多级反馈队列算法
3.10 说一说进程有多少种状态?
总共有五种状态:创建、就绪、运行、阻塞、终止
3.11 说说什么是死锁,产生的条件,如何解决?
概念: 指多个进程在执行过程中,因争夺资源而造成互相等待。
产生的条件:
- 互斥条件: 进程分配的资源,不允许其他进程访问。
- 请求保持条件: 进程在请求其他资源的时候,不会释放自己已经占有的资源。
- 不可剥夺条件: 进程所拥有的资源,只能自己释放,不能被其他进程剥夺。
- 循环等待条件: 进程之间形成一种头尾相连的循环等待资源关系。
如何解决:
- 资源一次性分配
- 可剥夺资源
- 资源有序分配
- 银行家算法(避免死锁)
3.12 进程同步的方式?
总共包括三种:临界区(管道)、信号量和消息队列
- 临界区(管道): 同一时间只有一个进程可以访问临界区中的资源。
- 信号量(semaphore): 是一个计数器,可以用来控制多个进程对共享资源的访问。它能够用于实现进程间的互斥与同步。
- 消息队列: 消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除。
3.13 说说进程通信的方式有哪些?
进程间通信主要包括:管道、系统 IPC(消息队列、信号量、信号、共享内存、内存映射)、套接字 Socket
- 管道: 包括匿名管道(
pipe()
)和有名管道(mkfifo()
)- 匿名管道是一个在内存中的缓冲区,只能用于有关系的进程间,是半双工的。
- 有名管道作为一个特殊文件存在,但内容存放在内存中。它可以用于没有关系的进程间。
- 消息队列: 消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除。
- 信号量(semaphore): 是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。信号量也属于临界资源。
- 信号: 用于通知接收进程某个事件的发生。可以通过 sigaction 函数来设置信号处理函数。
- 内存共享: 使多个进程访问同一块内存空间。
- 使用
shmget
创建或者获取一个共享内存块,使用shmat
将当前进程与指定共享内存块相关联。 - 使用
shmdt
姐却当前进程与指定共享内存的关联。使用shmctl
删除共享内存,但只有当所用共享这一内存都执行后,共享内存块才会被销毁。 - 使用
ipcs -m
可以查看每个共享内存的相关信息。
- 使用
- 内存映射: 每个进程都有一个磁盘文件的映射,进程可以通过修改内存来完成通信。
- 使用
mmap
建立内存映射,使用munmap
释放内存映射。
- 使用
- Socket: 可以实现不同主机之间的通信。
3.13.1 进程通信中的管道实现原理是什么?
操作系统在内核中开辟出一块缓冲区用于通信。管道是一种两个进程间进行单向通信的机制。它是半双工通信(只能由一个进程流向另一个进程),如果要实现进程间的全双工通信,就要用两个管道。剩余的见 3.13 中的管道。
3.13.2 简述 mmap 的原理和使用场景
原理: 它是内存映射的方法。即将一个文件或者其他对象映射到进程的地址空间,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上。
3.13.3 说说常见信号有那些,表示什么含义?
信号代号 | 信号名称 | 作用 |
---|---|---|
1 | SIGHUP | 该信号让进程立即关闭.然后重新读取配置文件之后重启 |
2 | SIGINT | 程序中止信号,用于中止前台进程。相当于输出 Ctrl+C 快捷键 |
19 | SIGSTOP | 该信号可以暂停前台进程,相当于输入 Ctrl+Z 快捷键。本信号不能被阻断 |
9 | SIGKILL | 用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。般用于强制中止进程 |
14 | SIGALRM | 时钟定时信号,计算的是实际的时间或时钟时间。alarm 函数使用该信号 |
15 | SIGTERM | 正常结束进程的信号,kill 命令的默认信号。如果进程已经发生了问题,那么这 个信号是无法正常中止进程的,这时我们才会尝试 SIGKILL 信号,也就是信号 9 |
17 | SIGCHLD | 子进程结束时, 父进程会收到这个信号。 |
3.13.4 互斥量能不能在进程中使用?
能
不同的进程之间,存在资源竞争或并发使用的问题,所以需要互斥量。线程同步使用的互斥锁是属于互斥量的一种。
3.14 说说线程间通信的方式有那些?
主要包括五种:临界区、互斥锁、读写锁、信号量和条件变量
- 临界区: 同一时间只允许一个线程进入临界区。
- 互斥锁: 采用互斥对象机制,只有拥有互斥对象的线程才可以访问。
- 信号量: 计数器,允许多个线程同时访问同一个资源。
- 条件变量: 通过条件变量通知操作的方式来保持多线程同步。
- 读写锁: 读写锁与互斥锁类似。但是读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率比互斥锁高。
3.15 线程同步方式有哪些?
- 互斥锁: 采用互斥对象机制,只有拥有互斥对象的线程才可以访问。
- 信号量: 计数器,允许多个线程同时访问同一个资源。
- 条件变量: 通过条件变量通知操作的方式来保持多线程同步。
- 读写锁: 读写锁与互斥锁类似。但是读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率比互斥锁高。
3.16 简述互斥锁的机制,互斥锁与读写锁的区别?
互斥锁机制: mutex,用于保证在任何时刻,都只能有一个线程访问该对象。
互斥锁与读写锁:
- 读写锁分读者和写者,而互斥锁不分。
- 互斥锁同一时间只允许一个线程访问该对象。读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
3.17 简述自旋锁和互斥锁的使用场景
两者区别: 请求资源如果被占用,互斥锁会进入睡眠状态,而自旋锁会循环申请。
使用场景:
- 互斥锁用于临界区持锁时间比较长的操作。
- 临界区有 IO 操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
- 单核处理器
- 自旋锁主要用于临界区持锁时间非常短且 CPU 资源不紧张的情况下。
3.18 说说线程有那些状态,相互之间怎么转换?
- 新建状态
- 就绪状态
- 运行状态
- 阻塞状态
- 终止状态
3.19 单核机器上写多线程程序,是否要考虑加锁,为什么?
需要
原因: 线程锁能实现线程的同步和通信。如果两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。
3.20 多线程和单线程有什么区别,多线程编程需要注意什么,多线程加锁需要注意什么?
- 区别:
- 多线程从属于一个进程,单线程也从属于一个进程。
- 多线程可以并发执行多个任务,单线程只能执行一个任务。
- 多线程并发执行多任务,需要切换内核栈和硬件上下文,有切换的开销。单线程不需要切换,没有切换的开销。
- 多线程并发执行多任务,需要考虑同步的问题;单线程不需要考虑同步的问题。
- 多线程编程需要注意同步问题。
- 多线程加锁,主要需要注意死锁的问题。
----------------------------------------------------------------------
4.1 说说 sleep 和 wait 的区别?
概念:
- sleep: 是一个延时函数,让进程或线程等一段时间后进入休眠状态。
- wait: 是父进程回收子进程 PCB 资源的一个系统调用。进程一旦调用了 wait 函数,就立即阻塞自己本身,当某个子进程退出就会释放子进程资源。
区别:
- sleep 是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。
- wait 是父进程回收子进程 PCB(Process Control Block) 资源的一个系统调用。
4.2 说说线程池的设计思路,线程池中线程的数量由什么确定?
设计思路: 实现线程池由以下几个步骤:
- 设置一个生产者消费者队列,作为临界资源。
- 初始化 n 个线程,并让其运行起来,加锁去队列里取任务运行。
- 当任务队列为空时,所有线程阻塞.
- 当生产者队列来了一个队列后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。
线程池中线程数量:
- 线程数量和那些因素有关:CPU、IO、并发、并行
如果是 CPU 密集型应用,则线程池大小设置为:CPU 数目 + 1 如果是 IO 密集型应用,则线程池大小设置为:2*CPU 数目 + 1 最佳线程数目 = (线程等待时间与线程 CPU 时间之比 + 1) * CPU 数目
为什么要创建线程池?(作用)
- 线程池可以避免频繁创建线程和销毁线程的开销,同时线程池也是为了提升系统效率。
- 线程池由于线程数量确定,所以也可以控制应用所占内存容量。
4.3 简述 Linux 零拷贝的原理
传统拷贝:
- 磁盘 --> 文件缓冲区
- 文件缓冲区 --> 应用缓冲区
- 应用缓冲区 --> Socket 缓冲区
- Socket 缓冲区 --> 网卡
零拷贝两种技术
mmap: 可以对数据操作
- 磁盘 --> 文件缓冲区
- 文件缓冲区 --> Socket 缓冲区
- Socket 缓冲区 --> 网卡
Sendfile: 不可以对数据做操作
- 磁盘 --> Socket 缓冲区
- Socket 缓冲区 --> 网卡
零拷贝概念: 就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。(可以延申 C++ 种的 push_back 和 emplace_back)
零拷贝的好处:
- 节省了 CPU 周期,空出的 CPU 可以完成更多其他的任务。
- 减少了内存区域之间数据拷贝,节省内存带宽
- 减少用户态和内核态之间数据拷贝,提升数据传输效率
- 应用零拷贝技术,减少用户态和内核态之间的上下文切换
Senfile()具体过程:
- 发起 sendfile() 系统调用,操作系统由用户态空间切换到内核态空间(第一次上下文切换)
- 通过 DMA 引擎将数据从磁盘拷贝到内核态空间的输入的 Socket 缓冲区中(第一次拷贝)
- 将数据从内核空间拷贝到与之关联的 Socket 缓冲区(第二次拷贝)
- 将 Socket 缓冲区的数据拷贝到协议引擎中(第三次拷贝)
- sendfile() 系统调用结束,操作系统由用户态空间切换到内核态空间(第二次上下文切换)
4.4 说一下 I/O 多路复用,select 和 epoll,epoll 水平触发和边缘触发的区别?
I/O 多路复用: I/O 多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用进程进行相应的读写操作。
select 和 epoll 的区别:
- 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。而 epoll 保证了每个 fd 在整个过程中只会拷贝一次。
- select 能同时监听的文件描述符数量是有限的,默认是 1024,而 epoll 没有这个限制。
epoll 水平触发和边缘触发的区别:
LT模式(水平触发)下:只要这个 fd 还有数据可读,每次 epoll_wait 都会返回它的事件。
ET模式(边缘触发)下:它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论 fd 中是否有数据可读。
4.5 说说 Reactor、Proactor 模式
Reactor 模式用于同步 I/O,而 Proactor 模式用于异步 I/O 操作。
Reactor:
- 主线程:只负责监听文件描述符是否有事件发生,不做任何其他实质性的工作。
- 工作线程:读写数据,接受新的连接,以及处理客户请求。
Proactor:
- 主线程:主线程负责监听文件描述符是否有事件发生,当有事件发生的时候向内核注册 Socket 读完成事件。
- 工作线程:仅仅负责业务逻辑
主要区别:
在 Reactor 中工作线程需要处理数据的读写,并进行逻辑处理。而 Proactor 中工作线程只负责逻辑请求,并不负责数据的读写。
Proactor 更高效
4.6 简述同步与异步的区别,阻塞与非阻塞的区别?
I/O 操作主要分为两个阶段:数据就绪 和数据读写
同步与异步的区别: (可以扩充进程间的同步和异步),针对的是数据读写阶段(需要将数据从内核区(TCP 缓冲区)拷贝到用户区(buf 数组)),同步是工作线程负责这步,异步是内核负责这步操作,当内核处理完后会通知工作线程完成了。
- 同步: 是所有的操作都做完,才返回给用户结果。即写完数据库之后,再响应用户,用户体验不好。
- 异步: 不用等所有操作都做完,就响应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较好。
同步和异步的区别就在于, 异步是内核将数据拷贝到用户区,不需要用户再自己接受数据,直接使用就行了,而同步是内核通知用户数据到了,然后用户自己调用相应函数去接收数据。
阻塞与非阻塞的区别: 它们针对的是数据就绪阶段
- 阻塞: 阻塞等待,直到 IO 事件就绪,才去执行下一步动作。
- 非阻塞: 非阻塞等待,每隔一段时间就去检查 IO 事件是否就绪。没有就绪就可以做其他事情。
4.7 介绍一下 5 种 IO 模型
- 阻塞 IO: 阻塞等待,直到 IO 事件就绪,才去执行下一步动作。
- 非阻塞 IO: 非阻塞等待,每隔一段时间就去检查 IO 事件是否就绪,没有就绪就可以做其他事情。
- 信号驱动 IO(非阻塞同步): Linux 用套接字口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当 IO 事件就绪,进程收到 SIGIO 信号,然后处理 IO 事件。
- IO 多路复用: Linux 用 select/poll 函数实现 IO 复用模型,这两个函数才会使进程阻塞,但是和阻塞 IO 所不同的是这两个函数可以同时阻塞多个 IO 操作。而且可以同时对多个读操作、写操作的 IO 函数进行检查。直到有数据可读或可写时,才真正调用 IO 操作函数。单个进程可以同时处理多个网络 IO 请求。
- 异步 IO(非阻塞异步): Linux 中,可以调用 aio_read 函数告诉内核描述子缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。用户可以直接去使用数据。
4.8 BIO、NIO有什么区别?
BIO(Blocking I/O):阻塞 IO。 调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
NIO(New I/O):同时支持阻塞与非阻塞模式, NIO 的做法是叫一个线程不断的轮询每个 IO 的状态,看看是否有 IO 的状态发生了改变,从而进行下一步的操作。