目录
1.什么是进程?
1.进程的概念
进程的结构:控制块(PCB),数据段,程序段
进程的特征
进程与『线程』基本概述
线程与进程的比较
线程的上下文切换
线程的实现
2.进程是怎么运行的?
进程的状态
进程控制
进程的上下文切换
处理机调度
调度原则
处理机调度:调度的层次
常见的调度算法
3.进程之间是怎么协作的?
进程通信
共享内存
信号量
信号
Socket
进程同步
4.如何处理死锁问题?
死锁的概念
死锁的处理策略
预防:
死锁的避免
进程(Process),是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是系统进行『资源分配和调度』的一个独立单位。
进程(Process),是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是系统进行『资源分配和调度』的一个独立单位。
几个要点
- 进程是『程序』的『一次执行』
- an instance of a computer program that is being executed
- 进程是一个程序及其数据在处理机上顺序执行时所发生的『活动』
- 进程是程序在一个『数据集合』上运行的过程
- 进程是系统进行『资源分配和调度』的一个『独立』单位(或者说基本单位)
PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。
PCB 具体包含什么信息呢?
进程描述信息:
- 进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符;
- 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
进程控制和管理信息:
- 进程当前状态,如 new、ready、running、waiting 或 blocked 等;
- 进程优先级:进程抢占 CPU 时的优先级;
资源分配清单:
- 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。
CPU 相关信息:
- CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。
每个 PCB 是如何组织的呢?
通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:
- 将所有处于就绪状态的进程链在一起,称为就绪队列;
- 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列;
- 另外,对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。
那么,就绪队列和阻塞队列链表的组织形式如下图:
除了链接的组织方式,还有索引方式,它的工作原理:将同一状态的进程组织在一个索引表中,索引表项指向相应的 PCB,不同状态对应不同的索引表。
一般会选择链表,因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除。
什么是线程?
Thread,进程的轻型实体,也叫“轻量级进程”,是一系列活动按事先设定好的顺序依次执行的过程,是一系列指令的集合
是一条执行路径,不能单独存在,必须包含在进程中
线程是OS中运算调度的最小单位
- 线程之间可以并发运行;
- 线程之间共享相同的地址空间;
为什么引入线程?
提高OS的并发性
线程的属性
进程与线程比较
重点:线程相对于进程,大大降低了创建、撤销和切换可执行实体的成本和难度。
线程的优缺点?
线程的优点:
- 一个进程中可以同时存在多个线程;
- 各个线程之间可以并发执行;
- 各个线程之间可以共享地址空间和文件等资源;
线程的缺点:
- 当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃(这里是针对 C/C++ 语言,Java语言中的线程奔溃不会造成进程崩溃)。
举个例子,对于游戏的用户设计,则不应该使用多线程的方式,否则一个用户挂了,会影响其他同个进程的线程。
线程与进程的比较如下:
- 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
- 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
- 线程能减少并发执行的时间和空间开销;
对于,线程相比进程能减少开销,体现在:
- 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
- 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
- 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
- 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;
所以,不管是时间效率,还是空间效率线程比进程都要高。
在前面我们知道了,线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位。
所以,所谓操作系统的任务调度,实际上的调度对象是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。
对于线程和进程,我们可以这么理解:
- 当进程只有一个线程时,可以认为进程就等于线程;
- 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的;
另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
线程上下文切换的是什么?
这还得看线程是不是属于同一个进程:
- 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
- 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;
所以,线程的上下文切换相比进程,开销要小很多。
主要有三种线程的实现方式:
- 用户线程(User Thread):在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
- 内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程;
- 轻量级进程(LightWeight Process):在内核中来支持用户线程;
那么,这还需要考虑一个问题,用户线程和内核线程的对应关系。
首先,第一种关系是多对一的关系,也就是多个用户线程对应同一个内核线程:
第二种是一对一的关系,也就是一个用户线程对应一个内核线程:
第三种是多对多的关系,也就是多个用户线程对应到多个内核线程:
用户线程如何理解?存在什么优势和缺陷?
用户线程是基于用户态的线程管理库来实现的,那么线程控制块(Thread Control Block, TCB) 也是在库里面来实现的,对于操作系统而言是看不到这个 TCB 的,它只能看到整个进程的 PCB。
所以,用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。
用户级线程的模型,也就类似前面提到的多对一的关系,即多个用户线程对应同一个内核线程,如下图所示:
用户线程的优点:
用户线程的缺点:
以上,就是用户线程的优缺点了。
那内核线程如何理解?存在什么优势和缺陷?
内核线程是由操作系统管理的,线程对应的 TCB 自然是放在操作系统里的,这样线程的创建、终止和管理都是由操作系统负责。
内核线程的模型,也就类似前面提到的一对一的关系,即一个用户线程对应一个内核线程,如下图所示:
内核线程的优点:
内核线程的缺点:
以上,就是内核线程的优缺点了。
最后的轻量级进程如何理解?
轻量级进程(Light-weight process,LWP)是内核支持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持,而且 LWP 是由内核管理并像普通进程一样被调度。
在大多数系统中,LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息。一般来说,一个进程代表程序的一个实例,而 LWP 代表程序的执行线程,因为一个执行线程不像进程那样需要那么多状态信息,所以 LWP 也不带有这样的信息。
在 LWP 之上也是可以使用用户线程的,那么 LWP 与用户线程的对应关系就有三种:
1 : 1
,即一个 LWP 对应 一个用户线程;N : 1
,即一个 LWP 对应多个用户线程;M : N
,即多个 LWP 对应多个用户线程;接下来针对上面这三种对应关系说明它们优缺点。先看下图的 LWP 模型:
1 : 1 模式
一个线程对应到一个 LWP 再对应到一个内核线程,如上图的进程 4,属于此模型。
N : 1 模式
多个用户线程对应一个 LWP 再对应一个内核线程,如上图的进程 2,线程管理是在用户空间完成的,此模式中用户的线程对操作系统不可见。
M : N 模式
根据前面的两个模型混搭一起,就形成 M:N
模型,该模型提供了两级控制,首先多个用户线程对应到多个 LWP,LWP 再一一对应到内核线程,如上图的进程 3。
组合模式
如上图的进程 5,此进程结合 1:1
模型和 M:N
模型。开发人员可以针对不同的应用特点调节内核线程的数目来达到物理并行性和逻辑并行性的最佳方案。
一个进程的生命周期包括三种基本状态,也被称为"三态模型",包括:
1. 运行态(Running):在任何时刻,只有运行在 CPU 上的进程(或者在多核系统中,每一个 CPU 或核心上运行的进程)才被认为是在运行态。也就是说,它正在执行或者已经准备好执行指令。
2. 就绪态(Ready):就绪态的进程已经拥有了运行所需要的所有资源(除了CPU),只是正在等待 CPU 的调度。一旦获得 CPU 时间,就绪态的进程就会转变为运行态。
3. 阻塞态(Waiting / Blocked):阻塞态的进程是指由于某种原因,如等待用户输入或者等待磁盘读写完成等,不能继续执行下去,也无法被调度到 CPU 上执行。当等待的事件(比如用户输入或磁盘操作)完成后,阻塞态的进程会转变为就绪态,等待获得 CPU 时间,而一旦被阻塞等待,它只能由另一个进程唤醒。
以上就是进程的三种基本状态。进程在运行过程中,会根据所发生的事件,反复在这些状态之间转换。
创建和终止状态
创建(Create)和终止(Terminate)是进程生命周期中的两个特殊状态。
1. 创建状态(Create):当一个新进程被创建时,它将进入创建状态。在这个状态下,操作系统会为新进程分配内存,创建进程控制块(Process Control Block, PCB),为进程加载程序和数据,并初始化与进程相关的系统资源。创建状态完成后,进程通常会进入就绪态(Ready),准备开始执行。
2. 终止状态(Terminate):当一个进程完成其任务或由于某些原因需要被终止时,它将进入终止状态。在这个状态下,操作系统会回收进程占用的资源(如内存、文件描述符等),清理与进程相关的数据结构,并将进程从进程列表中移除。一旦进程进入终止状态,它将不再执行任何指令,它的生命周期结束。
这两个状态分别标志着进程生命周期的开始和结束。在进程的整个生命周期中,它会根据不同的条件或事件,在各种状态(包括三态模型中的运行、就绪和阻塞状态)之间切换。
即OS对进程实现有效的管理,包括创建新进程、撤销已有进程、挂起、阻塞和唤醒、进程切换等多种操作。OS通过原语(Primitive)操作实现进程控制。
原语的概念:由若干条指令组成,完成特定的功能,是一种原子操作(Action Operation)原语的特点:
原子操作,要么全做,要么全不做,执行过程不会被中断。
在管态/系统态/内核态下执行,常驻内存。
是内核三大支撑功能(中断处理/时钟管理/原语操作)之一
创建原语:create 阻塞原语:block 唤醒原语:wakeup 撤销原语:destroy
01 创建进程
操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源。
创建进程的过程如下:
- 申请一个空白的 PCB,并向 PCB 中填写一些控制和管理进程的信息,比如进程的唯一标识等;
- 为该进程分配运行时所必需的资源,比如内存资源;
- 将 PCB 插入到就绪队列,等待被调度运行;
02 终止进程
进程可以有 3 种终止方式:正常结束、异常结束以及外界干预(信号
kill
掉)。当子进程被终止时,其在父进程处继承的资源应当还给父进程。而当父进程被终止时,该父进程的子进程就变为孤儿进程,会被 1 号进程收养,并由 1 号进程对它们完成状态收集工作。
终止进程的过程如下:
- 查找需要终止的进程的 PCB;
- 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程;
- 如果其还有子进程,则应将该进程的子进程交给 1 号进程接管;
- 将该进程所拥有的全部资源都归还给操作系统;
- 将其从 PCB 所在队列中删除;
03 阻塞进程
当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒。
阻塞进程的过程如下:
- 找到将要被阻塞进程标识号对应的 PCB;
- 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;
- 将该 PCB 插入到阻塞队列中去;
04 唤醒进程
进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的。
如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。
唤醒进程的过程如下:
- 在该事件的阻塞队列中找到相应进程的 PCB;
- 将其从阻塞队列中移出,并置其状态为就绪状态;
- 把该 PCB 插入到就绪队列中,等待调度程序调度;
进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。
挂起与激活
为了系统和用户观察和分析进程
挂起原语: suspend
静止就绪:放外存,不调度
静止阻塞:等待事件
激活原语:active
活动就绪:等待调度
活动阻塞:等待唤醒
各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换。
在详细说进程上下文切换前,我们先来看看 CPU 上下文切换。
大多数操作系统都是多任务,通常支持大于 CPU 数量的任务同时运行。实际上,这些任务并不是同时运行的,只是因为系统在很短的时间内,让各个任务分别在 CPU 运行,于是就造成同时运行的错觉。
任务是交给 CPU 运行的,那么在每个任务运行前,CPU 需要知道任务从哪里加载,又从哪里开始运行。
所以,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器。
CPU 寄存器是 CPU 内部一个容量小,但是速度极快的内存(缓存)。我举个例子,寄存器像是你的口袋,内存像你的书包,硬盘则是你家里的柜子,如果你的东西存放到口袋,那肯定是比你从书包或家里柜子取出来要快的多。
再来,程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
所以说,CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文。
既然知道了什么是 CPU 上下文,那理解 CPU 上下文切换就不难了。
CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
系统内核会存储保持下来的上下文信息,当此任务再次被分配给 CPU 运行时,CPU 会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
上面说到所谓的「任务」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换。
进程的上下文切换到底是切换什么呢?
进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:
大家需要注意,进程的上下文开销是很关键的,我们希望它的开销越小越好,这样可以使得进程可以把更多时间花费在执行程序上,而不是耗费在上下文切换。
发生进程上下文切换有哪些场景?
- 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行;
- 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
- 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;
以上,就是发生进程上下文切换的常见场景了。
根据一定的算法和原则将处理机资源进行重新分配的过程。
前提:作业/进程数远远大于处理机数
目的:提高资源利用率,减少处理机空闲时间
调度程序:一方面要满足特定系统用户的需求(快速响应),另一方面要考虑系统整体效率(系统平均周转时间)和调度算法本身的开销
原则一:如果运行的程序,发生了 I/O 事件的请求,那 CPU 使用率必然会很低,因为此时进程在阻塞等待硬盘的数据返回。这样的过程,势必会造成 CPU 突然的空闲。所以,为了提高 CPU 利用率,在这种发送 I/O 事件致使 CPU 空闲的情况下,调度程序需要从就绪队列中选择一个进程来运行。
原则二:有的程序执行某个任务花费的时间会比较长,如果这个程序一直占用着 CPU,会造成系统吞吐量(CPU 在单位时间内完成的进程数量)的降低。所以,要提高系统的吞吐率,调度程序要权衡长任务和短任务进程的运行完成数量。
原则三:从进程开始到结束的过程中,实际上是包含两个时间,分别是进程运行时间和进程等待时间,这两个时间总和就称为周转时间。进程的周转时间越小越好,如果进程的等待时间很长而运行时间很短,那周转时间就很长,这不是我们所期望的,调度程序应该避免这种情况发生。
原则四:处于就绪队列的进程,也不能等太久,当然希望这个等待的时间越短越好,这样可以使得进程更快的在 CPU 中执行。所以,就绪队列中进程的等待时间也是调度程序所需要考虑的原则。
原则五:对于鼠标、键盘这种交互式比较强的应用,我们当然希望它的响应时间越快越好,否则就会影响用户体验了。所以,对于交互式比较强的应用,响应时间也是调度程序需要考虑的原则。
针对上面的五种调度原则,总结成如下:
- CPU 利用率:调度程序应确保 CPU 是始终匆忙的状态,这可提高 CPU 的利用率;
- 系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,长作业的进程会占用较长的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
- 周转时间:周转时间是进程运行+阻塞时间+等待时间的总和,一个进程的周转时间越小越好;
- 等待时间:这个等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意;
- 响应时间:用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。
说白了,这么多调度原则,目的就是要使得进程要「快」。
高级调度/作业调度
- 把后备作业调入内存
- 只调入一次,调出一次
中级调度/内存调度
- 将进程调至外存,条件合适再调入内存
- 在内、外存对换区进行进程对换
低级调度/进程调度
- 从就绪队列选取进程分配给处理机
- 最基本的调度,频率非常高(相当于一个时间片完成)
1. 高级调度:又被称为长程调度或者是作业调度。这种调度的主要任务是控制进程从磁盘切换到主存,这也是创建一个新的进程的过程。它基于系统资源如处理器时间,尽可能多地保持足够数量的进程在存储器中。
2. 中级调度:又被称为中程调度或者是交换调度。这个调度的目标是为了平衡存储器中的进程数量。如果在主存储器中生存的进程数量过多,那么中级调度器就会将一些进程移到磁盘上,这个过程被称为换出(Swapping-out)。若主存储器空间充足,它则会将进程从磁盘移回主存,这个过程被称为换入(Swapping-in)。
3. 低级调度:又被称为短程调度或者是CPU调度。这是在现存进程集合中选择并执行下一个运行在处理器上的进程。这个调度工作要频繁地进行,因此设计低级调度程序时需要尽可能的快速和有效。
在一个典型的系统中,长程调度器更少地运行,而短程调度器更频繁地运行。然而,中程调度器运行的频率则位于这两者之间。
处理机调度:调度方式
剥夺式/抢占式调度
- 立即暂停当前进程
- 分配处理机给另一个进程
- 原则:优先权/短进程优先/时间片原则
非剥夺/非抢占式调度
- 若有进程请求执行
- 等待直到当前进程完成或阻塞
- 缺点:适用于批处理系统,不适用分时/实时系统
处理机调度:调度时机
- 进程运行完毕
- 进程时间片用完
- 进程要求I/O操作
- 执行某种原语操作
- 高优先级进程申请运行(剥夺式调度)
处理机调度:调度过程
- 保存镜像:记录进程现场信息
- 调度算法:确定分配处理机的原则
- 进程切换:分配处理机给其它进程
- 处理机回收:从进程收回处理机
进程调度:调度算法指标
- CPU利用率↑忙碌时间/总时间
- 系统吞吐量↑完成作业数/总时间
- 周转时间↓作业完成时间-提交时间
- 带权周转时间:周转时间/实际运行时间
- 等待时间↓作业等待处理机调度时间
- 关注平均值
- 响应时间↓提交请求到首次响应间隔
:先来先服务(FCFS,First Come First Served)
算法内容:调度作业/就绪队列中最先入队者,等待操作完成或阻塞
算法原则:按作业/进程到达顺序服务(执行)
调度方式:非抢占式调度
适用场景:作业/进程调度
优缺点:
有利于CPU繁忙型作业,充分利用CPU资源
不利于I/O繁忙型作业,操作耗时,其它饥饿
短作业优先(SJF,Shortest Job First )
算法内容:所需服务时间最短的作业/进程优先服务(执行)
算法原则:追求最少的平均(带权)周转时间
调度方式:SJF/SPF非抢占式
适用场景:作业/进程调度
优缺点:
平均等待/周转时间最少
长作业周转时间会增加或饥饿
估计时间不准确,不能保证紧迫任务及时处理
高响应比优先调度(HRRN,Highest Response Ratio Next)
算法内容:结合FCFS和SJF,综合考虑等待时间和服务时间计算响应比,高的优先调度
算法原则:综合考虑作业/进程的等待时间和服务时间
调度方式:非抢占式
适用场景:作业/进程调度
响应比计算:
响应比=(等待时间+服务时间)/服务时间, ≥1
只有当前进程放弃执行权(完成/阻塞)时,重新计算所有进程响应比
长作业等待越久响应比越高,更容易获得处理机
优先级调度(PSA,Priority-Scheduling Algorithm)
算法内容:又叫优先权调度,按作业/进程的优先级(紧迫程度)进行调度
算法原则:优先级最高(最紧迫)的作业/进程先调度
调度方式:抢占/非抢占式(并不能获得及时执行)
适用场景:作业/进程调度
优先级设置原则:
静态/动态优先级
系统>用户;交互型>非交互型;I/O型>计算型
低优先级进程可能会产生“饥饿”
时间片轮转调度(RR,Round-Robin)
算法内容:按进程到达就绪队列的顺序,轮流分配一个时间片去执行,时间用完则剥夺
算法原则:公平、轮流为每个进程服务,进程在一定时间内都能得到响应
调度方式:抢占式,由时钟中断确定时间到
适用场景:进程调度
优缺点:
公平,响应快,适用于分时系统
时间片决定因素:系统响应时间、就绪队列进程数量、系统处理能力
时间片太大,相当于FCFS;太小,处理机切换频繁,开销增大
多级反馈队列调度(MFQ,Multileveled Feedback Queue)
算法内容:
设置多个按优先级排序的就绪队列优先级从高到底,时间片从小到大新进程采用队列降级法
进入第一级队列,按FCFS分时间片没有执行完,移到第二级,第三级。。。前面队列不为空,不执行后续队列进程。
算法原则:集前几种算法优点,相当于PSA+RR
调度方式:抢占式
适用场景:进程调度
优缺点:
对各类型相对公平;快速响应;
终端型作业用户:短作业优先
批处理作业用户:周转时间短
长批处理作业用户:在前几个队列部分执行
进程间的协作:进程通信,进程同步
概念:进程通信即进程间的信息交换
- 进程是资源分配的基本单位,各进程内存空间彼此独立
- 一个进程不能随意访问其它进程的地址空间
特点:
- 共享存储(Shared-Memory)
- 消息传递(Message-Passing)
- 管道通信(Pipe)
共享存储(Shared-Memory)
基于共享数据结构的通信方式
- 多个进程共用某个数据结构(OS提供并控制)
- 由用户(程序员)负责同步处理
- 低级通信:可以传递少量数据,效率低
基于共享存储区的通信方式
- 多个进程共用内存中的一块存储区域
- 由进程控制数据的形式和方式方式
- 高级通信:可以传递大量数据,效率高
消息传递(Message-Passing)
直接通信:点到点发送
- 发送和接收时指明双方进程的ID
- 每个进程维护一个消息缓冲队列
间接通信:广播信箱
- 以信箱为媒介,作为中间实体
- 发进程将消息发送到信箱,收进程从信箱读取
- 可以广播,容易建立双向通信链
管道通信(Pipe)
管道
- 用于连接读/写进程的共享文件,pipe文件
- 本质是内存中固定大小的缓冲区
半双工通信
- 同一时段只能单向通信,双工通信需要两个管道
- 以先进先出(FIFO)方式组织数据传输
- 通过系统调用read()/write()函数进行读写操作
具体的通信方式有:
管道,消息队列,共享内存,信号量,信号,Socket。
消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义
MSGMAX
和MSGMNB
,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。
消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。
现代操作系统,对于内存管理,采用的是虚拟内存技术,也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。
用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
信号量表示资源的数量,控制信号量的方式有两种原子操作:
P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。
接下来,举个例子,如果要使得两个进程互斥访问共享内存,我们可以初始化信号量为 1
。
具体的过程如下:
可以发现,信号初始化为 1
,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存。
另外,在多进程里,每个进程并不一定是顺序执行的,它们基本是以各自独立的、不可预知的速度向前推进,但有时候我们又希望多个进程能密切合作,以实现一个共同的任务。
例如,进程 A 是负责生产数据,而进程 B 是负责读取数据,这两个进程是相互合作、相互依赖的,进程 A 必须先生产了数据,进程 B 才能读取到数据,所以执行是有前后顺序的。
那么这时候,就可以用信号量来实现多进程同步的方式,我们可以初始化信号量为 0
。
具体过程:
可以发现,信号初始化为 0
,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。
上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
信号跟信号量虽然名字相似度 66.66%,但两者用途完全不一样,就好像 Java 和 JavaScript 的区别。
在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l
命令,查看所有的信号:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
运行在 shell 终端的进程,我们可以通过键盘输入某些组合键的时候,给进程发送信号。例如
SIGINT
信号,表示终止该进程;SIGTSTP
信号,表示停止该进程,但还未结束;如果进程在后台运行,可以通过 kill
命令的方式给进程发送信号,但前提需要知道运行中的进程 PID 号,例如:
SIGKILL
信号,用来立即结束该进程;所以,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。
1.执行默认操作。Linux 对每种信号都规定了默认操作,例如,上面列表中的 SIGTERM 信号,就是终止进程的意思。
2.捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
3.忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL
和 SEGSTOP
,它们用于在任何时候中断或结束某一进程。
前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。
实际上,Socket 通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。
我们来看看创建 socket 的系统调用:
int socket(int domain, int type, int protocal)
三个参数分别代表:
根据创建 socket 类型的不同,通信的方式也就不同:
接下来,简单说一下这三种通信的编程模式。
针对 TCP 协议通信的 socket 编程模型
socket
,得到文件描述符;bind
,将绑定在 IP 地址和端口;listen
,进行监听;accept
,等待客户端连接;connect
,向服务器端的地址和端口发起连接请求;accept
返回用于传输的 socket
的文件描述符;write
写入数据;服务端调用 read
读取数据;close
,那么服务端 read
读取数据的时候,就会读取到了 EOF
,待处理完数据后,服务端调用 close
,表示连接关闭。这里需要注意的是,服务端调用 accept
时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。
所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
针对 UDP 协议通信的 socket 编程模型
UDP 是没有连接的,所以不需要三次握手,也就不需要像 TCP 调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因此也需要 bind。
对于 UDP 来说,不需要要维护连接,那么也就没有所谓的发送方和接收方,甚至都不存在客户端和服务端的概念,只要有一个 socket 多台机器就可以任意通信,因此每一个 UDP 的 socket 都需要 bind。
另外,每次通信时,调用 sendto 和 recvfrom,都要传入目标主机的 IP 地址和端口。
针对本地进程间通信的 socket 编程模型
本地 socket 被用于在同一台主机上进程间通信的场景:
对于本地字节流 socket,其 socket 类型是 AF_LOCAL 和 SOCK_STREAM。
对于本地数据报 socket,其 socket 类型是 AF_LOCAL 和 SOCK_DGRAM。
本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别。
协调进程间的相互制约关系,使它们按照预期的方式执行的过程
前提
- 进程是并发执行的,进程间存在着相互制约关系
- 并发的进程对系统共享资源进行竞争
- 进程通信,过程中相互发送的信号称为消息或事件
两种相互制约形式
- 间接相互制约关系(互斥):进程排他性地访问共享资源
- 直接相互制约关系(同步):进程间的合作,比如管道通信
- 同步(Synchronization):
同步是指多个线程在执行过程中,需要按照一定的顺序(通常是先后顺序)来协作完成任务。同步保证了多个线程按照预期执行。例如,在生产者消费者问题中,消费者需要等待生产者将数据放入缓冲区后,才能从缓冲区取出数据进行处理。为了实现同步,通常使用一些同步原语(例如信号量、事件、屏障等)来控制线程的执行顺序。- 互斥(Mutex):
互斥是指在同一时刻,只允许一个线程访问共享资源。互斥的目的是防止多个线程同时对共享资源进行操作,从而避免数据不一致和线程竞争等问题。例如,在生产者消费者问题中,需要保证同时只有一个线程访问缓冲区。为了实现互斥,通常使用互斥锁(Mutex Lock)或其他类似的机制来保护共享资源。当一个线程需要访问共享资源时,它必须首先请求互斥锁,如果获得锁,那么线程可以对共享资源进行操作;如果没有获得锁,则线程必须等待,直到锁被释放。
互斥的访问临界资源
访问过程
- 进入区:尝试进入临界区,成功则加锁(lock)
- 临界区:访问共享资源
- 退出区:解锁(unlock),唤醒其它阻塞进程
- 剩余区:其它代码
访问原则
- 空闲让进:临界区空闲,允许一个进程进入
- 忙则等待:临界区已有进程,其它进程等待(阻塞状态)
- 有限等待:处于等待的进程,等待时间有限
- 让权等待:等待时应让出CPU执行权,防止“忙等待”
软件实现方法
- 单标志法:违背“空闲让进”
p0 while(turn != 0); critical section; turn = 1; remainder section; p1 while(turn != 1); // 进入区 critical section; // 临界区 turn = 0; // 退出区 remainder section;// 剩余区
- 双标志法先检查:违背“忙则等待”
P0 while(flag[0]); // 进入区 flag[1] = true; critical section; // 临界区 flag[1] = false; // 退出区 remainder section;// 剩余区 P1 bool flag[2]; while(flag[1]); flag[0] = true; critical section; flag[0] = false; remainder section;
- 双标志法后检查:违背“空闲让进”、“有限等待”
p0 bool flag[2]; flag[0] = true; while(flag[1]); critical section; flag[0] = false; remainder section; P1 flag[1] = true; while(flag[0]); // 进入区 critical section; // 临界区 flag[1] = false; // 退出区 remainder section;// 剩余区
- 皮特森算法(Peterson's Algorithm):违背“让权等待”,会发生“忙等”
P1 bool flag[2]; int turn = 0; flag[0] = true; turn = 1; while(flag[1] && turn==1); critical section; flag[0] = false; remainder section; P1 flag[1] = true; turn = 0; while(flag[0] && turn==0);// 进入区 critical section; // 临界区 flag[1] = false; // 退出区 remainder section;// 剩余区
硬件实现方法
中断屏蔽方法:关中断/开中断
- 禁止一切中断,CPU执行完临界区之前不会切换
- 关中断可能会被滥用
- 关中断时间长影响效率
- 不适用于多处理机,无法防止其它处理机调度其它进程访问临界区
- 只适用于内核进程(该指令运行在内核态)
Test-And-Set(TS指令/TSL指令)
- 读出标志并设置为true,返回旧值,原子操作
- 也被称作TSL指令( Test-And-Set-Lock )
- 违背“让权等待”,会发生忙等
// lock表示临界区是否被加锁 bool TestAndSet(bool *lock) { bool old; old = *lock; *lock = true; // 加锁 return old; // 返回lock原来的值 } while (TestAndSet(&lock)); 临界区 *lock = false; // 解锁 剩余区
Swap指令( EXCHANGE,XCHG指令)
- 交换两个变量的值,原子操作
- 违背“让权等待”
// lock表示临界区是否被加锁 bool old = true; while (old) Swap(&lock, &old); 临界区 lock = false; // 解锁 剩余区 Swap(bool *a, bool *b) { bool temp; temp = *a; *a = *b; *b = temp; }
信号量(Semaphore)机制
PV操作:
- P操作:wait原语,进程等待
- V操作:signal原语,唤醒等待进程
1.整型信号量:违背“让权等待”,会发生忙等
// 整型信号量,表示可用资源数 int S = 1; // wait原语,相当于进入区 void wait(int S) { // 资源不够,循环等待 while (S <= 0); S = S - 1; } // signal原语,相当于退出区 void signal(int S) { S = S + 1; } // 进程Pn ... wait(S); // 进入区,申请打印机 访问共享资源; // 临界区,访问打印机 signal(S); // 退出区,释放打印机 ...
2.记录型信号量:进程进入阻塞状态,不会忙等
// 记录型信号量定义 typedef struct { int value; // 剩余资源数量 struct process *L; // 进程等待队列 } semaphore; void wait(semaphore S) { // 申请资源 S.value--; if (S.value < 0) { // block原语阻塞进程 block(S.L); } } void signal(semaphore S) { // 释放资源 S.value++; if (S.value <= 0) { // wakeup原语唤醒进程 wakeup(S.L); } } // 进程Pn ... wait(S); // 进入区,申请打印机 访问共享资源; // 临界区,访问打印机 signal(S); // 退出区,释放打印机 ...
管程(Monitor,监视器)
“管理进程”,即用于实现进程同步的工具。是由代表共享资源的数据结构和一组过程(进行PV操作的函数)组成的管理程序(封装)。
管程的组成
- 管程名称
- 局部于管程内部的共享数据结构
- 对该数据结构操作的一组过程(函数)
- 管程内共享数据的初始化语句
管程的基本特性
- 是一个模块化的基本程序单位,可以单独编译
- 是一种抽象数据类型,包含数据和操作
- 信息掩蔽,共享数据只能被管程内的过程访问
条件变量/条件对象
- 进入管程的进程可能由于条件不满足而阻塞
- 此时进程应释放管程以便其它进程调用管程
- 进程被阻塞的条件(原因)有多个,移入不同的条件队列
- 进程被移入条件队列后,应释放管程
死锁定义:
- 多个进程由于竞争资源而造成的阻塞现象,若无外力作用,这些进程将无法继续推进。
相似概念:饥饿
- 等待时间过长以至于给进程推进和响应带来明显影响,“饿而不死”。
死锁产生的原因
- 系统资源的竞争
- 进程推进顺序非法
死锁产生的必要条件
- 互斥条件:共享资源的排他性访问
- 不剥夺条件:访问时该共享资源不会被剥夺
- 请求并保持条件:保持当前资源时请求另一个资源
- 循环等待条件:存在共享资源的循环等待链
破坏以上四个必要条件
破坏互斥条件
- 将只能互斥访问的资源改为同时共享访问
- 将独占锁改为共享锁
- 不是所有资源都能改成可共享的
破坏不剥夺/不可抢占条件
- 请求新资源无法满足时必须释放已有资源
- 由OS协助强制剥夺某进程持有的资源
- 实现复杂,代价高
- 此操作过多导致原进程任务无法推进
破坏请求并保持条件
- 进程开始运行时一次性申请所需资源
- 阶段性请求和释放资源
死锁预防:破坏循环等待条件
- 对所有资源现行排序,按序号请求资源
- 对资源的编号应相对稳定,限制了新设备增加
- 进程使用资源的顺序可能与系统编号顺序不同
- 限制了用户编程
安全性算法:
- 系统安全状态
- 安全状态一定不会出现死锁
- 不安全状态可能出现死锁
银行家算法进行的前置条件:及存在一个安全序列。
银行家算法
- 系统预判进程请求是否导致不安全状态
- 是则拒绝请求,否则答应请求
先用available 满足 need 进程任务完成后得到 Work +Alllocation 再去满足下一个
还需要加一个判断是 request 是否小于need
假设贷款后减少可用资源 P1的allocation就增加了 Need就减少了变成了新的系统的快照,再在这个基础上进行是否安全判断.
死锁的检测与解除
死锁检测
- 需要一种数据结构,保存有关资源的请求和分配信息
- 提供一种算法,利用这些信息检测是否形成了死锁
死锁解除
- 资源剥夺
- 撤销进程
- 进程回退
死锁的检测
1.资源分配图(G=(N, E)):
- 两种资源
- 两种节点
一个顶点的集合V和边的集合E中
V被分为两个部分
- >P=Pi, P2,..., P小,含有系统中全部的进程
- >R ={R, R2,..., R,含有系统中全部的资源
- 请求边:有向边P->R
- 分配边:有向边R->P
2.死锁定理(死锁状态的充分条件):
- 当且仅当此状态下资源分配图是不可完全简化的
- 简化过程类似于“拓扑排序”算法(注意数据结构考察)
死锁的解除
资源剥夺
- 挂起死锁进程
- 剥夺其资源
将资源分配给其它(死锁)进程
撤销进程
进程回退
- 回退到足以避免死锁的地步
- 需要记录进程历史信息,设置还原点