# 操作系统

# 进程和线程的区别

1、调度:进程是资源分配的基本单位,是独立运行的基本单位。线程是程序执行的基本单位,是CPU调度的基本单位,用于保证程序的实时性,实现进程内部的并发,每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。

2、切换:线程上下文切换比进程上下文切换要快得多。

3、拥有资源: 进程是拥有资源的一个独立单位,包括用于存放程序正文、数据的磁盘和内存地址空间,以及在运行时所需要的I/O设备,已打开的文件,信号量等,线程不拥有系统资源,但是可以访问隶属于进程的资源。

4、系统开销: 创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O设备等,OS所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。

# 进程控制块

进程控制块信息可以分为三类:进程标识信息、处理器状态信息、进程控制信息。

  • 进程标识信息:进程标识符主要包括:当前进程的标识符(Process ID)、创建这个进程的父进程的标识符、用户标识符(User ID)。
  • 处理器状态信息:包括处理器寄存器的内容。
  • 进程控制信息:进程状态、优先级、调度相关信息、数据结构、进程间通信、进程特权、存储管理、资源所有权和使用信息。

# 为什么需要线程

**线程产生的原因:**进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:

1、 进程在同一时刻只能做一个任务,很多时候不能充分利用CPU资源。

2、进程在执行的过程中如果发生阻塞,整个进程就会挂起,即使进程中其它任务不依赖于等待的资源,进程仍会被阻塞。

引入线程就是为了解决以上进程的不足,线程具有以下的优点:

1、从资源上来讲,开辟一个线程所需要的资源要远小于一个进程。

2、从切换效率上来讲,运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间(这种时间的差异主要由于缓存的大量未命中导致)。

3、从通信机制上来讲,线程间方便的通信机制。对不同进程来说,它们具有独立的地址空间,要进行数据的传递只能通过进程间通信的方式进行。线程则不然,属于同一个进程的不同线程之间共享同一地址空间,所以一个线程的数据可以被其它线程感知,线程间可以直接读写进程数据段(如全局变量)来进行通信(需要一些同步措施)。

# 协程和线程的区别

1、线程和进程都是同步机制,而协程是异步机制。

2、线程是抢占式,而协程是非抢占式的。需要用户释放使用权切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。

3、一个线程可以有多个协程,一个进程也可以有多个协程。

4、协程不被操作系统内核管理,而完全是由程序控制。线程是被分割的CPU资源,协程是组织好的代码流程,线程是协程的资源。但协程不会直接使用线程,协程直接利用的是执行器关联任意线程或线程池。

5、协程能保留上一次调用时的状态。

# 并发和并行

1、并发就是在一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务在执行。

单核处理器可以做到并发。比如有两个进程A和B,A运行一个时间片之后,切换到B,B运行一个时间片之后又切换到A。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。

2、并行就是在同一时刻,有多个任务在执行。这个需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。

进程与线程的切换流程

进程切换分两步:

1、切换页表以使用新的地址空间,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。

2、切换内核栈和硬件上下文。

对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2步是进程和线程切换都要做的。

因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

# 虚拟地址空间切换耗时原因

进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个Cache就是TLB(translation Lookaside Buffer,TLB本质上就是一个Cache,是用来加速页表查找的)。

由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,Cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。

# 进程间通信方式

1、管道:管道这种通讯方式有两种限制,一是半双工的通信,数据只能单向流动,二是只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

管道可以分为两类:匿名管道和命名管道。匿名管道是单向的,只能在有亲缘关系的进程间通信;命名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。

优缺点:速度慢,容量有限

2、信号:信号是一种比较复杂的通信方式,信号可以在任何时候发给某一进程,而无需知道该进程的状态。

3、消息队列:消息队列是消息的链接表,包括Posix消息队列和System V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

优缺点:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

4、信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

优缺点:不能传递复杂消息,只能用来同步

5、共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

优缺点:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。

6、Socket:与其他通信机制不同的是,它可用于不同机器间的进程通信。

优缺点:任何进程间都能通讯,但速度慢;

# 进程的状态

进程在运行时有三种基本状态:就绪态、运行态和阻塞态。

1、运行(running)态:进程占有处理器正在运行的状态。进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态;在多处理机系统中,则有多个进程处于执行状态。

2、**就绪(ready)态:**进程具备运行条件,等待系统分配处理器以便运行的状态。 当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。

3、阻塞(wait)态:又称等待态或睡眠态,指进程不具备运行条件,正在等待某个时间完成的状态。

# 状态转换

各状态之间的转换:

1、就绪→执行,处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态。

2、执行→就绪,处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态。

3、执行→阻塞,正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。

4、阻塞→就绪,处于阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状态。

用户态内核态

本质是对CPU提供的功能的一层封装抽象,给不同的操作给与不同的“权限”

内核态其从本质上说就是指的内核,是一种特殊的软件程序,即控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行

用户态就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O。内核必须提供一组通用的访问接口,这些接口就叫系统调用。

系统调用时操作系统的最小功能单位。系统调用组成了用户态跟内核态交互的基本接口。

库函数就是在原先系统调用的基础上,屏蔽复杂的底层实现细节,对系统调用进行封装,提供简单的基本接口给用户,给予用户直接使用系统调用访问资源,增强了程序的灵活性,减轻程序员的负担,从而更加关注上层的逻辑实现。

# 线程同步的方法

**1、临界区:**当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止,以此达到用原子方式操作共享资源的目的。

2、事件:事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。

**3、互斥量:**互斥对象和临界区对象非常相似,只是其允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。

**4、信号量:**当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。

区别:

  • 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说互斥量可以跨越进程使用,但创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量 。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
  • 互斥量,信号量,事件都可以被跨越进程使用来进行同步数据操作。

# 调度原则

  • CPU利用率:调度程序应确保CPU是始终匆忙的状态,这可提高CPU的利用率
  • 系统吞吐量:吞吐量表示的是单位时间内CPU完成进程的数量,长作业的进程会占用较长的CPU资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量,权衡长短任务运行完成数量;
  • 周转时间:周转时间是进程运行和阻塞时间总和,一个进程的周转时间越小越好;
  • 等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意;
  • 响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。

# 进程调度策略

1、**先来先服务:**非抢占式的调度算法,按照请求的顺序进行调度。有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。另外,对I/O密集型进程也不利,因为这种进程每次进行I/O操作之后又得重新排队。

2、**最短作业优先:**非抢占式的调度算法,按估计运行时间最短的顺序进行调度。长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。

3、**高响应比优先:**主要权衡了短作业和长作业,每次进行进程调度时,先计算响应比优先级,然后把响应比优先级最高的进程投入运行。如果两个进程的等待时间同时,要求的服务时间越短,响应比就越高,这样短作业的进程容易被选中运行;如果两个进程要求的服务时间相同时,等待时间越长,响应比就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会。

4、**时间片轮转:**将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。

时间片轮转算法的效率和时间片的大小有很大关系:因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 而如果时间片过长,那么实时性就不能得到保证。

5、最高优先级:非抢占式和抢占式,为每个进程分配一个优先级,按优先级进行调度。进程的优先级可以分为,静态优先级或动态优先级,其中为了防止低优先级的进程永远等不到调度,动态优先级可以随着时间的推移增加等待进程的优先级。但是依然有缺点,可能会导致低优先级的进程永远不会运行。

6、多级反馈队列:多级反馈队列(Multilevel Feedback Queue) 调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。 顾名思义:

  • 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
  • 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列;

步骤:

  • 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短;
  • 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
  • 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;

可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。

# 虚拟内存

为了在多进程环境下,使得进程之间的内存地址不受影响,相互隔离,于是操作系统就为每个进程独立分配一套虚拟地址空间,每个程序只关心自己的虚拟地址就可以,实际上大家的虚拟地址都是一样的,但分布到物理地址内存是不一样的。作为程序,也不用关心物理地址的事情。操作系统引入了虚拟内存,进程持有的虚拟地址会通过CPU芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存。

1、内存分段:程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。分段机制下的虚拟地址由两部分组成,**段选择子(段号、特权等标志位)和段内偏移量。虚拟地址是通过段表(段基地址、段界限、特权级别)**与物理地址进行映射的.

内存转换步骤:分段机制会把程序的虚拟地址分成4个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址。

分段的办法很好,解决了程序本身不需要关心具体的物理内存地址的问题,能够产生连续的内存空间,但它也有一些不足之处:

  • 第一个就是内存碎片的问题。这里的内存碎片的问题共有两处地方︰

    • 外部内存碎片,也就是产生了多个不连续的小物理内存,导致新的程序无法被装载;
    • 内部内存碎片,程序所有的内存都被装载到了物理内存,但这个程序有部分内存可能并不是很常使用,这也会导致内存的浪费
  • 第二个就是内存交换的效率低的问题。

2、内存分页:分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页 (Page)。在Linux下,每一页的大小为4KB。页表是存储在内存里的,内存管理单元(MMU)就做将虚拟内存地址转换成物理地址的工作。而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

优势:

  • 采用了分页,那么释放的内存都是以页为单位释放的,也就不会产生无法给进程使用的小内存
  • 如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。一旦需要的时候,再加以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。
  • 更进一步地,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去,即按需加载内存资源

在分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址。

总结一下,对于一个内存地址转换,其实就是这样三个步骤:

  • 把虚拟内存地址,切分成页号和偏移量;
  • 根据页号,从页表里面,查询对应的物理页号;
  • 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。

不足之处:

  • 空间缺陷:因为操作系统是可以同时运行非常多的进程的,那这不就意味着页表会非常的庞大
    • 解决方法:多级页表,遵循局部性原理(按需加载使用其二级页表甚至三级页表......)
    • 目的:页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有100多万个页表项来映射,而二级分页则只需要1024个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。
    • 缺陷:多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。
    • 程序是有局部性的,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。利用这一特性,把最常访问的几个页表项存储到访问速度更快的硬件,就在CPU芯片中,加入了一个专门存放程序最常访问的页表项的Cache,这个Cache 就是TLB(Translation Lookaside Buffer),通常称为页表缓存、转址旁路缓存、快表等。

3、段页式:先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页。地址结构就由段号、段内页号和页内位移三部分组成。用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号。

步骤:

  • 第一次访问段表,得到页表起始地址;
  • 第二次访问页表,得到物理页号;
  • 第三次将物理页号与页内位移组合,得到物理地址。

Linux系统中的每个段都是从0地址开始的整个4GB虚拟空间(32位环境下),也就是所有的段的起始地址都是一样的。这意味着,Linux系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。另外,Linxu系统中虚拟空间分布可分为用户态和内核态两部分,其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。

# 页面置换算法

当CPU访问的页面不在物理内存时,便会产生一个缺页中断,请求操作系统将所缺页调入到物理内存。那它与一般中断的主要区别在于:

  • 缺页中断在指令执行「期间」产生和处理中断信号,而一般中断在一条指令执行「完成」后检查和处理中断信号。
  • 缺页中断返回到该指令的开始重新执行「该指令」,而一般中断返回回到该指令的「下一个指令」执行。

步骤:

  • 在CPU里访问一条Load M指令,然后CPU会去找M所对应的页表项。
  • 如果该页表项的状态位是「有效的」,那CPU就可以直接去访问物理内存了,如果状态位是「无效的」,则CPU则会发送缺页中断请求。
  • 操作系统收到了缺页中断,则会执行缺页中断处理函数,先会查找该页面在磁盘中的页面的位置。
  • 找到磁盘中对应的页面后,需要把该页面换入到物理内存中,但是在换入前,需要在物理内存中找空闲页,如果找到空闲页,就把页面换入到物理内存中。
  • 页面从磁盘换入到物理内存完成后,则把页表项中的状态位修改为「有效的」。
  • 最后,CPU重新执行导致缺页异常的指令。

找不到空闲页的话,就说明此时内存已满了,这时候,就需要「页面置换算法」选择一个物理页,如果该物理页有被修改过〈脏页),则把它换出到磁盘,然后把该被置换出去的页表项的状态改成「无效的」,最后把正在访问的页面装入到这个物理页中。

页表项通常有状态位(表示该页是否有效,即是否在物理内存),访问字段(记录该页在一段时间内被访问的次数)、修改位(表示该页在调入内存后是否被修改过)、硬盘地址(指出该页在硬盘上的地址)。

1、 最佳页面置换:置换在「未来」最长时间不访问的页面。这很理想,但是实际系统中无法实现,因为程序访问页面时是动态的,我们是无法预知每个页面在「下一次」访问前的等待时间。所以,最佳页面置换算法作用是为了衡量你的算法的效率,你的算法效率越接近该算法的效率,那么说明你的算法是高效的。

2、先进先出置换:既然我们无法预知页面在下一次访问前所需的等待时间,那我们可以选择在内存驻留时间很长的页面进行中置换,这个就是「先进先出置换」算法的思想。

3、最近最久未使用:最近最久未使用(LRU)的置换算法的基本思路是,发生缺页时,选择最长时间没有被访问的页面进行置换,也就是说,该算法假设已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。开销比较大。

4、时钟页面置换:该算法的思路是,把所有的页面都保存在一个类似钟面的「环形链表」中,一个表针指向最老的页面。

当发生缺页中断时,算法首先检查表针指向的页面︰

  • 如果它的访问位位是0就淘汰该页面,并把新的页面插入这个位置,然后把表针前移一个位置;
  • 如果访问位是1就清除访问位,并把表针前移一个位置,重复这个过程直到找到了一个访问位为0的页面为止。

5、最不常用算法:最不常用(LFU)算法,这名字听起来很调皮,但是它的意思不是指这个算法不常用,而是当发生缺页中断时,选择「访问次数」最少的那个页面,并将其淘汰。

它的实现方式是,对每个页面设置一个「访问计数器」,每当一个页面被访问时,该页面的访问计数器就累加1。在发生缺页中断时,淘汰计数器值最小的那个页面。

但还有个问题,LFU算法只考虑了频率问题,没考虑时间的问题,比如有些页面在过去时间里访问的频率很高,但是现在已经没有访问了,而当前频繁访问的页面由于没有这些页面访问的次数高,在发生缺页中断时,就会可能会误伤当前刚开始频繁访问,但访问次数还不高的页面。

那这个问题的解决的办法还是有的,可以**定期减少访问的次数,**比如当发生时间中断时,把过去时间访问的页面的访问次数除以2,也就说,随着时间的流失,以前的高访问次数的页面会慢慢减少,相当于加大了被置换的概率

# 死锁

死锁的概念:在两个或多个并发进程中,如果每个进程持有某种资源而又都等待别的进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗地讲,就是两个或多个进程被无限期地阻塞、相互等待的一种状态。

死锁产生的原因主要是: 系统资源不足; 进程推进顺序非法。

产生死锁的必要条件:

(1)互斥(mutual exclusion),一个资源每次只能被一个进程使用;

(2)不可抢占(no preemption),进程已获得的资源,在未使用完之前,不能强行剥夺;

(3)**占有并等待(**hold and wait),一个进程因请求资源而阻塞时,对已获得的资源保持不放;

(4)环形等待(circular wait),若干进程之间形成一种首尾相接的循环等待资源关系。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

死锁的解除与预防:理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

死锁的处理策略:鸵鸟策略、预防策略、避免策略、检测与恢复策略

# 孤儿进程、僵尸进程、守护进程

1、孤儿进程:如果父进程先退出,子进程还没退出,那么子进程将被托孤给init进程,这是子进程的父进程就是init进程(1号进程),其实还是很好理解的。

2、僵尸进程:一个进程已经终止了,但是其父进程还没有获取其状态,那么这个进程就称之为僵尸进程。僵尸进程还会消耗一定的系统资源,并且还保留一些概要信息供父进程查询子进程的状态可以提供父进程想要的信息。一旦父进程得到想要的信息,僵尸进程就会结束。

3、守护进程:守护进程就是在后台运行,不与任何终端关联的进程

# Linux IO 模型

无论什么 IO 模型,其读取过程总会经历下面两个阶段:

  • 等待数据到达内核缓冲区
  • 从内核缓冲区拷贝数据到程序缓冲区

Linux 根据这两个阶段的是否阻塞,分成5 个经典的 IO 的模型:

  • 阻塞 IO 模型:硬件到系统内核,阻塞。系统内核到程序空间,阻塞。
  • 非阻塞 IO 模型:硬件到系统内核,轮询阻塞。系统内核到程序空间,阻塞。
  • 复用 IO 模型:硬件到系统内核,多流轮询阻塞。系统内核到程序空间,阻塞。
    • 在获取事件时,先将所有连接(文件描述符)传给内核,再由内核返回产生事件的连接,然后在用户态中处理对应请求
    • select:遍历方式 最大容量1024
    • poll:动态数组、链表、遍历、开销大
    • epoll:红黑树跟踪、事件驱动,维护链表记录就绪事件
      • 边缘触发:只苏醒一次,搭配非阻塞IO,效率更高
      • 水平触发:不断苏醒,直到读完
  • 信号驱动 IO 模型:硬件到系统内核,信号回调不阻塞。系统内核到程序空间,阻塞。
  • 异步 IO 模型:硬件到系统内核,信号回调不阻塞。系统内核到程序空间,信号回调不阻塞。

从上面的 5 种 IO 模型,我们可以看出,真正实现异步非阻塞的只有异步 IO 这种模型,而其他四种都是同步性 IO。因为在第二阶段:从内核缓冲区复制到进程缓冲区的时候,不可能干其他事情。