1. 程序执行的基本过程
- 第一步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作 「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到「指令寄存器」。
- 第二步,CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执行;
- 第三步,CPU 执行完指令后,「程序计数器」的值自增,表示指向下一条指令。这个自增的大小,由 CPU 的位宽决定,比如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此「程序计数器」的值会自增 4;
2. 如何让程序跑的更快
-
提升数据缓存的命中率
-
CPU Cache 一次性能加载数据的大小,可以在 Linux 里通过 coherency_line_size 配置查看它的大小,通常是 64 个字节
-
遇到这种遍历数组的情况时,按照内存布局顺序访问
-
-
提升指令缓存的命中率
-
分支预测器
-
如果分支预测可以预测到接下来要执行 if 里的指令,还是 else 指令的话,就可以「提前」把这些指令放在指令缓存中,这样 CPU 可以直接从 Cache 读取到指令,于是执行速度就会很快。
-
-
提升多核 CPU 的缓存命中率
-
线程绑定在某一个 CPU 核心上
-
sched_setaffinity
-
3. 缓存一致性问题
- 如果这时旁边的 B 号核心尝试从内存读取 i 变量的值,则读到的将会是错误的值,因为刚才 A 号核心更新 i 值还没写入到内存中,内存中的值还依然是 0。这个就是所谓的缓存一致性问题,A 号核心和 B 号核心的 缓存,在这个时候是不一致,从而会导致执行结果的错误。
- 第一点,某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播 (Wreite Propagation);
- 写传播的原则就是当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心。最常⻅实现的方式是总线嗅探(Bus Snooping)。
- 第二点,某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串形化(Transaction Serialization)。
- MESI协议
- Modified,已修改
- Exclusive,独占
- Shared,共享
- Invalidated,已失效
4. 伪共享问题
-
多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象称为伪共享(False Sharing)。
-
避免伪共享的方法
-
在 Linux 内核中存在 __cacheline_aligned_in_smp 宏定义
-
Java 并发框架 Disruptor 使用「字节填充 + 继承」的方式, 来避免伪共享的问题。
由于「前后」各填充了 7 个不会被读写的 long 类型变量,所以无论怎么加载 Cache Line,这整个 Cache Line 里都没有会发生更新操作的数据,于是只要数据被频繁地读取访问,就自然没有数据被换出 Cache 的可能,也因此不会产生伪共享的问题。
-
5. 软中断
-
中断是系统用来响应硬件设备请求的一种机制,操作系统收到硬件的中断请求,会打断正在执行的进程,然后调用内核中的中断处理程序来响应请求。
-
Linux 系统为了解决中断处理程序执行过⻓和中断丢失的问题,将中断过程分成了两个阶段,分别是 「上半部和下半部分」。
-
上半部用来快速处理中断,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。
上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行;
-
下半部用来延迟处理上半部未完成的工作,一般以「内核线程」的方式运行。
下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较⻓的事 情,特点是延迟执行;
-
-
在 Linux 系统里,我们可以通过查看 /proc/softirqs 的内容来知晓「软中断」的运行情况,以及 /proc/interrupts 的内容来知晓「硬中断」的运行情况。
6. 分段分页问题与解决
- 分段机制下的虚拟地址由两部分组成,段选择子和段内偏移量。
- 段选择子就保存在段寄存器里面。段选择子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。
- 分段的问题
- 第一个就是内存碎片的问题。
- 第二个就是内存交换的效率低的问题。
- 分⻚是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。
- 在分⻚机制下,虚拟地址分为两部分,⻚号和⻚内偏移。⻚号作为⻚表的索引,⻚表包含物理⻚每⻚所在物理内存的基地址,这个基地址与⻚内偏移的组合就形成了物理内存地址。
- 分页内存地址转换的三个步骤:
- 把虚拟内存地址,切分成⻚号和偏移量;
- 根据⻚号,从⻚表里面,查询对应的物理⻚号;
- 直接拿物理⻚号,加上前面的偏移量,就得到了物理内存地址。
- 分页的问题
- 有空间上的缺陷。
- 多级⻚表(Multi-Level Page Table)
- 全局⻚目录项 PGD(Page Global Directory);
- 上层⻚目录项 PUD(Page Upper Directory);
- 中间⻚目录项 PMD(Page Middle Directory);
- ⻚表项 PTE(Page Table Entry);
- 段⻚式内存管理
- 段号、段内⻚号和⻚内位移
7. 并行与并发的区别
8. 进程控制过程
-
创建进程
-
为新进程分配一个唯一的进程标识号,并申请一个空白的 PCB,PCB 是有限的,若申请失败则创建失败;
-
为进程分配资源,此处如果资源不足,进程就会进入等待状态,以等待资源;
-
初始化 PCB;
-
如果进程的调度队列能够接纳新进程,那就将进程插入到就绪队列,等待被调度运行;
-
-
终止进程
进程可以有 3 种终止方式:正常结束、异常结束以及外界干预(信号 kill 掉)。 终止进程的过程如下:
- 查找需要终止的进程的 PCB;
- 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程;
- 如果其还有子进程,则应将其所有子进程终止;
- 将该进程所拥有的全部资源都归还给父进程或操作系统;
- 将其从 PCB 所在队列中删除;
-
阻塞进程
- 找到将要被阻塞进程标识号对应的 PCB;
- 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;
- 将该 PCB 插入到阻塞队列中去;
-
唤醒进程
- 在该事件的阻塞队列中找到相应进程的 PCB;
- 将其从阻塞队列中移出,并置其状态为就绪状态;
- 把该 PCB 插入到就绪队列中,等待调度程序调度
9. 进程上下文切换的场景
进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;
-
时间片耗尽。
为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配 给各个进程。这样,当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列 选择另外一个进程运行;
-
系统资源不足。
进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
-
睡眠。
当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
-
高优先级。
当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
-
硬件中断。
发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;
10. 线程与进程的区别
- 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
- 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
- 线程能减少并发执行的时间和空间开销;
11. 线程的三种实现
-
用户线程(User Thread):在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
-
用户线程的优点
- 每个进程都需要有它私有的线程控制块(TCB)列表,用来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由用户级线程库函数来维护,可用于不支持线程技术的操作系统;
- 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度特别快;
-
用户线程的缺点:
- 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了。
- 当一个线程开始运行后,除非它主动地交出 CPU 的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的。
- 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会比较慢;
-
-
内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程;
-
内核线程的优点:
- 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
- 分配给线程,多线程的进程获得更多的 CPU 运行时间;
-
内核线程的缺点:
- 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息,如 PCB 和 TCB;
- 线程的创建、终止和切换都是通过系统调用的方式来进行,因此对于系统来说,系统开销比较大;
-
-
轻量级进程(LightWeight Process):在内核中来支持用户线程;
-
LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息。
-
1 : 1 模式
一个线程对应到一个 LWP 再对应到一个内核线程,如上图的进程 4,属于此模型。
优点:实现并行,当一个 LWP 阻塞,不会影响其他 LWP;
缺点:每一个用户线程,就产生一个内核线程,创建线程的开销较大。
-
N : 1 模式
多个用户线程对应一个 LWP 再对应一个内核线程,如上图的进程 2,线程管理是在用户空间完成的,此模式中用户的线程对操作系统不可⻅。
优点:用户线程要开几个都没问题,且上下文切换发生用户空间,切换的效率较高;
缺点:一个用户线程如果阻塞了,则整个进程都将会阻塞,另外在多核 CPU 中,是没办法充分利用CPU 的。
-
M : N 模式
根据前面的两个模型混搭一起,就形成 M:N 模型,该模型提供了两级控制,首先多个用户线程对应到多 个 LWP,LWP 再一一对应到内核线程,如上图的进程 3。
优点:综合了前两种优点,大部分的线程上下文发生在用户空间,且多个线程又可以充分利用多核 CPU 的资源。
-
组合模式
如上图的进程 5,此进程结合 1:1 模型和 M:N 模型。开发人员可以针对不同的应用特点调节内核线程的数目来达到物理并行性和逻辑并行性的最佳方案。
-
12. 线程调度原则与调度算法
- CPU 利用率:调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率;
- 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,⻓作业的进程会占用较⻓的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
- 周转时间:周转时间是进程运行和阻塞时间总和,一个进程的周转时间越小越好;
- 等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越⻓,用户越不满意;
- 响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。
- 先来先服务(First Come First Seved, FCFS)
- 最短作业优先(Shortest Job First, SJF)
- 高响应比优先 (Highest Response Ratio Next, HRRN)
- 时间片轮转(Round Robin, RR)
- 最高优先级(Highest Priority First,HPF)
- 多级反馈队列(Multilevel Feedback Queue)
13. 进程间通信的方法、应用场景、优缺点
-
匿名管道
顾名思义,它没有名字标识,匿名管道是特殊文件「只存在于内存」,没有存在于文件系统中,shell 命令中的「 | 」竖线就是匿名管道,通信的「数据是无格式的流并且大小受限」,通信的方式是「单向」的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在「父子关系」的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。
-
命名管道
突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是「缓存在内核中」,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循「先进先出原则」,不支持 lseek 之类的文件定位操作。
-
消息队列
克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是「保存在内核的消息链表」,消息队列的消息体是可以「用户自定义的数据类型」,发送数据时,会被分成一个一个独立的消息体, 当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。 消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷⻉过程。「大小限制」
- 消息队列不适合比较大数据的传输
- 消息队列通信过程中,存在用户态与内核态之间的数据拷⻉开销
-
共享内存
可以解决消息队列通信中用户态与内核态之间数据拷⻉过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,「不需要陷入内核态或者系统调用」,大大提高了通信的速度,享有「最快的进程间通信方式」之名。但是便捷高效的共享内存通信,带来新的问题,「多进程竞争」同个共享资源会造成数据的错乱。
-
信号量
那么,就需要信号量来保护共享资源,以「确保任何时刻只能有一个进程访问共享资源」,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。
-
信号
与信号量名字很相似的叫信号,它俩名字虽然相似,但功能一点儿都不一样。信号是进程间通信机制中唯一的「异步」通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令), 一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP ,这是为了方便我们能在任何时候结束或停止某 个进程。
-
Socket
前面说到的通信机制,都是工作于同一台主机,如果要「与不同主机的进程间通信」,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常⻅的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。
- 实现 TCP 字节流通信:socket 类型是 AF_INET 和 SOCK_STREAM;
- 实现 UDP 数据报通信:socket 类型是 AF_INET 和 SOCK_DGRAM;
- 对于本地字节流 socket,其 socket 类型是 AF_LOCAL 和 SOCK_STREAM。
- 对于本地数据报 socket,其 socket 类型是 AF_LOCAL 和 SOCK_DGRAM。
14. 临界区、互斥、同步
- 临界区(critical section),它是访问共享资源的代码片段,一定不能给多线程同时执行。
- 互斥(mutualexclusion)的,也就说保证一个线程在临界区执行时,其他线程应该被阻止进入临界区
- 所谓同步,就是并发进程**/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/**线程同步。
15. 信号量原理
- P 操作:将 sem 减 1 ,相减后,如果 sem < 0 ,则进程/线程进入阻塞等待,否则继续,表明 P 操作可能会阻塞;
- 保留调用线程CPU现场
- 将该线程的TCB插入到s的等待队列
- 设置该线程为等待状态
- 执行调度程序
- V 操作:将 sem 加 1 ,相加后,如果 sem <= 0 ,唤醒一个等待中的进程/线程,表明 V 操作不会阻塞;
- 移出s等待队列首元素
- 将该线程的TCB插入就绪队列
- 设置该线程为就绪状态
16. 生产者消费者问题、哲学家就餐问题、读者写者问题 220
17. 死锁的条件、排查224、避免
- 死锁只有同时满足以下四个条件才会发生:
- 互斥条件:多个线程不能同时使用同一个资源
- 持有并等待条件:线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源
- 不可剥夺条件:在自己使用完之前不能被其他线程获取
- 环路等待条件:两个线程获取资源的顺序构成了环形链
- 使用资源有序分配法,来破环环路等待条件。
18. 互斥锁、自旋锁、读写锁、悲观锁、乐观锁
- 悲观锁认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
- 乐观锁假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
19. 缺页中断、页面置换算法
- 缺页中断处理过程
- 在 CPU 里访问一条 Load M 指令,然后 CPU 会去找 M 所对应的⻚表项。
- 如果该⻚表项的状态位是「有效的」,那 CPU 就可以直接去访问物理内存了,如果状态位是「无效的」,则 CPU 则会发送缺⻚中断请求。
- 操作系统收到了缺⻚中断,则会执行缺⻚中断处理函数,先会查找该⻚面在磁盘中的⻚面的位置。
- 找到磁盘中对应的⻚面后,需要把该⻚面换入到物理内存中,但是在换入前,需要在物理内存中找空闲⻚,如果找到空闲⻚,就把⻚面换入到物理内存中。
- ⻚面从磁盘换入到物理内存完成后,则把⻚表项中的状态位修改为「有效的」。
- 最后,CPU 重新执行导致缺⻚异常的指令。
- 页表项字段
- 状态位:用于表示该⻚是否有效,也就是说是否在物理内存中,供程序访问时参考。
- 访问字段:用于记录该⻚在一段时间被访问的次数,供⻚面置换算法选择出⻚面时参考。
- 修改位:表示该⻚在调入内存后是否有被修改过,由于内存中的每一⻚都在磁盘上保留一份副本,因此,如果没有修改,在置换该⻚时就不需要将该⻚写回到磁盘上,以减少系统的开销;如果已经被修改,则将该⻚重写到磁盘上,以保证磁盘中所保留的始终是最新的副本。
- 硬盘地址:用于指出该⻚在硬盘上的地址,通常是物理块号,供调入该⻚时使用。
- 置换算法
- 最佳⻚面置换算法(OPT)
- 先进先出置换算法(FIFO)
- 最近最久未使用的置换算法(LRU)
- 时钟⻚面置换算法(Lock)
- 最不常用置换算法(LFU)
20. 磁盘调度算法
-
先来先服务算法
-
最短寻道时间优先算法
-
扫描算法算法
磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上 的最后的磁道,才调换方向,这就是扫描(Scan)算法。
-
循环扫描算法
-
LOOK
磁头在移动到「最远的请求」位置,然后立即反向移动。
-
C-LOOK
21. 文件存储方法、空闲空间管理方法
-
连续空间存放方式
- 读写效率很高
- 文件头里需要指定「起始块的位置」和「⻓度」
- 但是有「磁盘空间碎片」和「文件⻓度不易扩展」的缺陷
-
非连续空间存放方式
-
链表方式
-
离散的,不用连续的,文件的⻓度可以动态扩展
-
隐式链表。
实现的方式是文件头要包含「第一块」和「最后一块」的位置, 并且每个数据块里面留出一个指针空间,用来存放下一个数据块的位置
缺点:
- 无法直接访问数据块,只能通过指针顺序访问文件,以及数据块指针消耗了一定的存储空间。
- 隐式链接分配的稳定性较差,系统在运行过程中由于软件或者硬件错误导致链表中的指针丢失或损坏,会导致文件数据的丢失。
-
显式链接。
它指把用于链接文件各数据块的指针,显式地存放在内存的一张链接表中, 该表在整个磁盘仅设置一张,每个表项中存放链接指针,指向下一个数据块号。文件分配表(File Allocation Table,FAT)。
不仅显著地提高了检索速度,而且大大减少了访问磁盘的次数。
但也正是整个表都存放在内存中的关系,它的主要的缺点是不适用于大磁盘。
-
-
索引方式
- 为每个文件创建一个「索引数据块」,里面存放的是指向文件数据块的指针列表。文件头需要包含指向「索引数据块」的指针
- 索引的方式优点
- 文件的创建、增大、缩小很方便;
- 不会有碎片的问题;
- 支持顺序读写和随机读写;
-
链式索引块
在索引数据块留出一个存放下一个索引数据块的指针
-
多级索引块
实现方式是通过一个索引块来存放多个索引数据块
-
-
空闲空间管理方法
- 空闲表法
- 空闲链表法
- 位图法
22. 软连接与硬连接
- 硬链接是多个目录项中的「索引节点」指向一个文件,也就是指向同一个 inode,但是 inode 是不可能跨越文件系统的,每个文件系统都有各自的 inode 数据结构和列表,所以硬链接是不可用于跨文件系统的。 由于多个目录项都是指向一个 inode,那么只有删除文件的所有硬链接以及源文件时,系统才会彻底删除该文件。
- 软链接相当于重新创建一个文件,这个文件有独立的 inode,但是这个文件的内容是另外一个文件的路径,所以访问软链接的时候,实际上相当于访问到了另外一个文件,所以软链接是可以跨文件系统的,甚 至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。
23. 缓冲与非缓冲I/O、直接与非直接I/O、阻塞与非阻塞I/O、同步与异步I/O
- 文件操作的标准库是可以实现数据的缓存,那么根据「是否利用标准库缓冲」划分:
- 缓冲 I/O,利用的是标准库的缓存实现文件的加速访问,而标准库再通过系统调用访问文件。
- 非缓冲 I/O,直接通过系统调用访问文件,不经过标准库缓存。
- 根据「是否利用操作系统的缓存」划分:
- 直接 I/O,不会发生内核缓存和用户程序之间数据复制,而是直接经过文件系统访问磁盘。
- 非直接 I/O,读操作时,数据从内核缓存中拷⻉给用户程序,写操作时,数据从用户程序拷⻉给内核 缓存,再由内核决定什么时候写入数据到磁盘。
- 阻塞等待的是「内核数据准备好」和「数据从内核态拷⻉到用户态」这两个过程。这里最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷⻉到用户程序的缓存区这个过程。
- 无论是阻塞 I/O、非阻塞 I/O,还是基于非阻塞 I/O 的多路复用都是同步调用。因为它们在 read 调用时,内核将数据从内核空间拷⻉到应用程序空间,过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷⻉效率不高,read 调用就会在这个同步过程中等待比较⻓的时间。
- 阻塞 I/O 会阻塞在「过程 1 」和「过程 2」,
- 非阻塞 I/O 和基于非阻塞 I/O 的多路复用只会阻塞在「过程2」,所以这三个都可以认为是同步 I/O。
- 异步 I/O 则不同,「过程 1 」和「过程 2 」都不会阻塞。
24. DMA工作方式
直接内存访问(Direct Memory Access) 技术:在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。
- 用户进程调用 read 方法,向操作系统发出 I/O 请求,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态;
- 操作系统收到请求后,进一步将 I/O 请求发送 DMA,然后让 CPU 执行其他任务;
- DMA 进一步将 I/O 请求发送给磁盘;
- 磁盘收到 DMA 的 I/O 请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,向 DMA 发起中断信号,告知自己缓冲区已满;
- DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷⻉到内核缓冲区中,此时不占用 CPU,CPU 可以执行其他任务;
- 当 DMA 读取了足够多的数据,就会发送中断信号给 CPU;
- CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷⻉到用户空间,系统调用返回;
25. TCP/IP网络模型
- OSI
- 应用层,负责给应用程序提供统一的接口;
- 表示层,负责把数据转换成兼容另一个系统能识别的格式;
- 会话层,负责建立、管理和终止表示层实体之间的通信会话;
- 传输层,负责端到端的数据传输;
- 网络层,负责数据的路由、转发、分片;
- 数据链路层,负责数据的封帧和差错检测,以及 MAC 寻址;
- 物理层,负责在物理网络中传输数据帧;
- TCP/IP
- 应用层,负责向用户提供一组应用程序,比如 HTTP、DNS、FTP 等;
- 传输层,负责端到端的通信,比如 TCP、UDP 等;
- 网络层,负责网络包的封装、分片、路由、转发,比如 IP、ICMP 等;
- 网络接口层,负责网络包在物理网络中的传输,比如网络包的封帧、 MAC 寻址、差错检测,以及通过网卡传输网络帧等;
26. 零拷贝 333
-
mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空 间就不需要再进行任何的数据拷⻉操作。
-
在 Linux 内核版本 2.1 中,提供了一个专⻔发送文件的系统调用函数 sendfile()
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的⻓度,返回值是实际复制数据的⻓度。
首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。
-
如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access) 技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷⻉到 socket 缓冲区的过程。
-
这就是所谓的零拷⻉(Zero-copy)技术,因为我们没有在内存层面去拷⻉数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
-
零拷⻉技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷⻉次数,只需要 2 次上下文切换和数据拷⻉次数,就可以完成文件的传输,而且 2 次的数据拷⻉过程,都不需要通过 CPU, 2 次都是由 DMA 来搬运。
27. 文件传输方式的选择
location /video/ {
sendfile on;
aio on;
directio 1024m;
}
-
当文件大小大于 directio 值后,使用「异步 I/O + 直接 I/O」,否则使用「零拷⻉技术」。
-
绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O。通常,对于磁盘,异步 I/O 只支持直接 I/O。
-
零拷⻉技术是不允许进程对文件内容作进一步的加工的,比如压缩数据再发送。
-
28. 服务器单机理论最大连接数
- TCP 连接是由四元组唯一确认的,这个四元组就是:本机IP, 本机端口**,** 对端IP, 对端端口。
- 服务器作为服务方,通常会在本地固定监听一个端口,等待客户端的连接。因此服务器的本地 IP 和端口是固定的,于是对于服务端 TCP 连接的四元组只有对端 IP 和端口是会变化的,所以最大 TCP 连接数 = 客户端 IP 数×客户端端口数。
- 对于 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数约为 2 的 48 次方。
- 这个理论值相当“丰满”,但是服务器肯定承载不了那么大的连接数,主要会受两个方面的限制:
- 文件描述符,Socket 实际上是一个文件,也就会对应一个文件描述符。在 Linux 下,单个进程打开的文件描述符数是有限制的,没有经过修改的值一般都是 1024,不过我们可以通过 ulimit 增大文件描述符的数目;
- 系统内存,每个 TCP 连接在内核中都有对应的数据结构,意味着每个连接都是会占用一定内存的;
29. select、poll、epoll
-
select 使用固定⻓度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024 ,只能监听 0~1023 的文件描述符。
-
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
-
但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核 态之间拷⻉文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增⻓。
-
epoll 通过两个方面,很好解决了 select/poll 的问题。
-
epoll 在内核里使用「红黑树」来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里
红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn) ,通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷⻉和内存分配。
-
epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
-
-
边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)
- 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
- 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏 醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
30. reactor、proactor
-
reactor(非阻塞同步网络模式)
-
单 Reactor 单进程 / 线程
-
组成
Reactor 对象的作用是监听和分发事件;
Acceptor 对象的作用是获取连接;
Handler 对象的作用是处理业务;
-
过程
- Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
- 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
-
缺点
因为只有一个进程,无法充分利用多核 CPU 的性能;
Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较⻓,那么就造成响应的延迟;
-
应用场景
- 不适用计算机密集型的场景,只适用于业务处理非常快速的场景。
-
-
单 Reactor 多线程 / 多进程
- 过程
- Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
- 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法获取连接,并创建一个 Handler 对象来处理后续的响应事件;
- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
- Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后, 会将数据发给子线程里的 Processor 对象进行业务处理;
- 子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;
- 缺点
- 单 Reator 多线程的方案优势在于能够充分利用多核 CPU 的能,那既然引入多线程,那么自然就带来了多线程竞争资源的问题。
- 因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
- 过程
-
多 Reactor 多进程 / 线程
- 过程:
- 主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;
- 子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。
- 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
- 过程:
-
-
Proactor(异步网络模式)
- 对比
- Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件,Reactor 模式是基于「待完成」的 I/O 事件
- Proactor 是异步网络模式, 感知的是已完成的读写事件,Proactor 模式则是基于「已完成」的 I/O 事件
- 过程
- Proactor Initiator 负责创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过Asynchronous Operation Processor 注册到内核;
- Asynchronous Operation Processor 负责处理注册请求,并处理 I/O 操作; Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor; Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理;
- Handler 完成业务处理;
- 对比