进程
- 进程地址空间里,对于不同平台的栈和堆,其生长方向不固定。

进程控制块PCB
存在于内核的一种结构,由于在内核中,所以可以被OS观察到,因此能利用OS的功能,实现进程调度和切换等。
- 组织:
- 链表:各种状态形成不同的链表,多个状态对应多个不同的链表。(就绪、阻塞链表)
-
索引表:同一个状态的进程归入一个索引表(由索引指向PCB)。(就绪、阻塞索引表)
-
进程控制块包括:
- 进程标识信息
- 处理器现场保存
- 用户可见reg:通用寄存器GPR;
- 控制与状态reg:PC寄存器、PSW寄存器;
- 栈ptr:SP寄存器
- 进程控制信息
- 指向下一个PCB,组织PCB需要。
- 略
五状态进程模型

- 时钟中断:当时间片完时,由外部时钟(不是OS)发送一个时钟中断,用于切换进程。
- 多就绪模型:对于多级队列(见后文MQ/MFQ)来说,需要设置多个低就绪节点和高就绪节点。
- 多等待模型:在多就绪模型的基础上,不同的阻塞事件,可能唤醒后会到不同的就绪队列中,因此不同阻塞事件可能涉及多个阻塞节点。
- 等待事件例子:IO调用、调页中断。
- 区分抢占和唤醒:
- 如果进程退出临界区等情况导致临界区被释放,则会唤醒其他进程;
- 如果进程运行到时间片完,则会被其他进程抢占。
挂起进程模型
-
在上述的三状态模型的基础上,我们引入外存,增加一系列挂起状态,包括就绪挂起和等待挂起,从而进一步缓解内存的空间压力。
-
挂起就是将进程信息==对换==到外存中。(见虚拟内存一节)
-
将优先级较低的的进程挂起,是通用的策略。
-
常见挂起:
-
等待到等待挂起:没有进程处于就绪态,或就绪过程需要挤占更多内存
- 就绪到就绪挂起:高优先级等待进程遇到低优先级就绪进程,认为前者将更快就绪,所以后者就绪挂起(就绪时被插队机制)
- 运行到就绪挂起:高优先级等待挂起因事件出现而进入就绪挂起(适用于抢占式系统,运行时被插队机制)
-
等待挂起到就绪挂起:当有等待挂起进程因相关事件出现而发生转换
-
常见解挂/激活:
-
就绪挂起到就绪:没有就绪进程或挂起就绪进程的优先级高于就绪进程
- 等待挂起到等待:当一个进程释放足够内存,并有高优先级等待挂起进程

- 状态迁移实质上就是PCB在状态队列中切换。
线程
如果要实现==可同步进行且相互通信==的三个流程,用进程就不行了。
虽然进程可以实现并行化,但是:
- 进程间相对隔离的性质和相互通信的目标相悖
- 使用进程的开销过大,需要各自开辟一块相类似的PCB
线程控制块TCB
- 各个线程间的stack和reg不同。但是进程内的其他资源都是共享的。

- 缺点:由于各个线程之间去耦合程度较低,因而如果一个线程出现问题,其他线程可能随之崩溃。

-
进程与线程区别:
-
进程是资源分配单位,线程是CPU调度单位。
- 资源分配不同。
- 状态转换相同。
-
线程可减少并发的时空开销。
-
内核线程、用户线程与轻量级进程
-
内核线程:依赖OS的功能,线程TCB存在于内核空间。
- OS能够观察线程的状态,线程的切换等功能需要依赖OS的系统调用/内核函数调用。
-
用户线程:不依赖OS的功能,而是由线程库在逻辑上实现线程、维护TCB结构,线程TCB存在于用户空间。
- 因此实际上OS无法观察线程的状态,只会以为它们是进程代码段的一部分。
- 线程阻塞后,OS会以为整个进程阻塞了,导致OS停止进程的运行。
- 当进程中的线程A正在运行时,除非线程A主动交出CPU控制权,否则==进程内的其他进程无法运行==
- 协程是其中一类用户线程
-
轻量级进程:用一个内核线程来支持一个进程的活动
- 体量类似于线程,同时OS也能够观察到,功能类似于进程
进程控制
1. 进程创建
- fork():创建一个子进程,它复制该进程的地址空间,因此子进程和父进程几乎完全一致。
- fork得到的子进程和父进程只有fork()的返回值不同。
2. 进程加载
- exec():允许程序加载一个完全不同的程序,并从main开始执行(和操作系统启动时的思路类似)
- exec调用成功时,还是相同的进程,但是运行了不同的程序。其代码段、堆栈和堆都会被完全重写。
- 由于大多数情况下,调用fork()之后,都会使用系统调用exec(),加载新程序取代当前运行进程,所以:
- 在fork操作中内存复制是没有作用的
- 子进程将可能关闭打开的文件和链接
- 因而在创建进程时,可以不立即重复创建一个同样的内存映像,将fork和exec结合起来,成为轻量级fork,接口为vfork()。
- vfork创建子进程,但子进程与父进程共享地址空间,直到子进程执行exec()或退出。这种方式更快,但更危险。
- 现在vfork使用CopyOnWrite技术实现(类似于Lazy的写时复制方法)
- 只读时:大家共享一大块数据(地址空间),不复制;
- 第一次写入时:才为写入者单独复制一份数据(只复制写的那部分即可),以保证不影响其他共享者。
3. 进程切换(上下文切换)
- 切换方式

- 内核为每个进程维护了对应的进程控制块,内核将相同状态的进程的PCB放置在同一队列。
- 就绪队列、等待队列、僵尸队列。
4. 进程等待
- wait():用于父进程等待子进程的结束
- 子进程结束时,通过exit()向父进程返回一个值
- 父进程通过wait()接受并处理返回值
- wait()的功能:
- 有子进程存活时,父进程进入等待状态,等待子进程的返回结果(exit值作为wait的返回值)
- 有僵尸子进程时,wait立即返回其中一个值
- 无子进程时,wait立刻返回
5. 进程退出
-
exit():用于进程结束时,完成进程资源的回收
-
将调用参数作为进程的“结果”
- 关闭所有打开的文件等占用资源、释放内存、释放大部分进程相关的内核数据结构
-
检查是否父进程存活
- 如果存活,保留结果的值,直到父进程需要它,进入僵尸(zombie)状态
- 如果没有,释放所有的数据结构,返回进程结果
-
进程中止是最终的垃圾收集(资源回收)
进程/作业调度算法
- 进程状态:创建、执行(执行挂起)、就绪(就绪挂起)、阻塞(阻塞挂起)、终止
- 作业状态:进入、后备、运行、完成
1. 评价指标
从运行效率来说:
- CPU使用率:CPU处于忙状态的时间百分比
- 吞吐量:单位时间内完成的进程数量
从用户使用来说:
-
周转时间:进程从初始化到完成(包括等待)的时间
-
周转时间:$$t_i = $$ 完成时间$t_{si}$ - 进入系统时间$t_{ci}$
-
平均周转时间:$$\bar t = \frac{周转时间之和}{n}$$
- 带权周转时间:$$w_i= \frac{周转时间t_{i}}{实际执行时间t_{ri}}$$
-
平均带权周转时间:$$\bar w = \frac{带权周转时间之和}{n}$$
-
等待时间:进程在就绪队列中等待的时间
-
响应时间:从提交请求到产生响应所花费的总时间
2. 计算优先级为依据
-
先来先服务FCFS(作业/进程)
-
FIFO策略。完全不考虑资源配置,使得CPU/IO利用率较低。
-
平均等待时间长,平均周转时间长。
-
短任务优先SJF(作业/进程)
-
任务:指代一个进程在执行中的时间段,即执行时间。
-
贪心策略。考虑最小化周转时间,即每次都选择预测执行时间最短的。
- 预测方式和之前页面置换算法中的思路类似,利用过去的情况来预测某个进程的执行时间。
- 预测时间$$\tau_{n+1} = \alpha t_n + (1-\alpha)\alpha t_{n-1} + (1-\alpha)^2\alpha t_{n-2} + ...$$
- a是学习率,在0到1之间。
- 由于是预测的执行时间,因此用户也可以给出自己估计的执行时间。比如乐观的时间会导致任务优先执行。
- 平均周转时间短。
-
根据抢占方式,SJF有两种分类:
-
可抢占的:SRT(最短剩余时间)
- 不可抢占的:SPN(短进程优先)
-
最高响应比优先HRRN(作业)
-
考虑最小化等待时间,每次选择响应比最高(执行期间最不容易等待)的作业,同时可以防止无限期等待。
- 响应比$$R=\frac{w+s}{s}=1+\frac{w}{s}$$,w为等待时间,s为预测执行时间。
- R与s成反比:预测执行时间越短,越不容易等待,优先级越高
- R与w成正比:等待时间越长,为了防止无限期等待,优先级越高
- 不可抢占。
- 平均等待时间短。
3. 时间资源分配为依据
- 时间片轮转Round-Robin(进程)
- FIFO策略,基于FCFS给出的进程队列,操作系统给队头交替分配相应的处理器资源。如果执行完退出,没有执行完回到队尾。
- 大体上讲,不同时间片的长度对于总周转时间的影响并不是非常明显。平均周转时间介于FCFS和SJF之间
4. 综合算法
- 反馈队列MQ(可以融合上述所有算法的技术)(进程)
- 就绪队列被划分为多个独立的子队列,每个队列内都可以采用不同调度策略,队列间可以固定优先级也可以轮转。
- 多级反馈队列MLFQ(作业/进程)
- MQ的升级,在MQ中加入一定的反馈更新机制,就成为多级反馈队列算法MFQ。
- 如果一个时间片内没有执行完,就向下调整优先级。
- 优先级越高,时间片越短
- 因此==CPU密集型的优先级较低,IO密集型的优先级较高。==
- CPU密集型需要长时间片以进行CPU计算;同时需要长的等待时间才不会因为CPU切换而影响吞吐量。
- IO密集型因为计算量少,需要短时间片;同时需要短的等待时间,才能使得IO请求能被立即处理,而不影响吞吐量。
- 公平共享调度算法FSS
- 一些用户组比其他用户组更重要,保证不重要的组无法垄断资源。
6. 总结
- FCFS:不公平,周转时间较长
- SJF(SPN/SRT):周转时间短,需要精确预测时间,可能导致饥饿
- HRRN:基于SJF,不可抢占
- RR:公平,但周转时间较长
- MLFQ:多种算法的集成,设计较为复杂
- FSS:公平是第一要素
特殊的作业调度算法
1. 设备特性为依据
- 磁盘的结构
- 磁道:略
- 柱面:所有盘面上的同半径的磁道(磁道组)
- 盘面:略(有时也叫磁头号,因为一个盘面对应一个磁头)
- 扇区:略
- 查找磁盘数据的流程:
- 定位柱面->定位盘面->定位扇区->找到数据
-
所以磁盘的地址addr=(柱面号,盘面号,扇区号)
-
盘特性:磁头一开始从外侧进入,所以柱面号小者在外侧,大者在内侧。
-
两种考虑方向
- 移臂调度:考虑前进距离最短,选取与当前移动臂前进方向最近的请求
- 旋转调度:考虑旋转角度最小,选取与当前读写头旋转方向最近的请求
- 常用的移臂调度算法
- 最短寻道时间优先SSTF:选择寻找时间最短的请求,缩短平均请求时间,但会使得磁头经常往返移动。
- 扫描算法SCAN(电梯算法):选择磁头前进方向上寻找时间最短的请求,到最外侧/最内侧磁道时才能变向,可以减少磁头的往返移动。
- LOOK:选择磁头前进方向上寻找时间最短的请求,前进方向上没有请求时立即反向移动。SCAN的升级版
- 循环扫描算法C-SCAN:循环移动磁头,到最外侧/最内侧磁道时才能变向,且磁头先移到最里侧,然后快速跳至最外侧继续同方向移动。
- C-LOOK:选择磁头前进方向上寻找时间最短的请求,前进方向上没有请求时立即反向移动,且磁头快速跳至外侧请求处继续同方向移动。CSCAN的升级版
特殊的进程调度算法
1. 实时调度(进程)
- 实时任务:对时限有要求的任务
- 周期实时任务:一系列相似的规律重复的实时任务。由周期、最大执行时间、使用率等决定。波动越小,使用率就可以越高。
-
硬实时和软实时:硬实时必须要满足时限,可以保证系统的确定性,否则会导致严重后果;而软实时只需尽量满足,无法达成时可以降低要求。
-
两个理论可调度的算法:
- 速率单调调度算法RM:通过周期安排优先级,周期越短越早安排。
- 最早截止时间优先算法EDF:截止时间越早优先级越高。
2. 多处理器调度
- 多个处理器组成一个多处理器系统,处理器之间共享负载。
- 对称处理器调度SMP:
- 各处理器都有自己的调度机制
-
访问共享资源时再进行同步
-
静态进程分配:对一个处理器,从一开始就确定其就绪队列,但可能出现忙闲不均
- 动态进程分配:所有处理器共享一个公共就绪队列,各个处理器负载均衡,但调度开销大
优先级倒置
-
优先级倒置是我们不希望发生的一种调度状态。在该种状态下,一个高优先级任务间接被一个低优先级任务所抢先。
-
原理:由于共享资源的互斥,导致了一种违反高优先级优先的阻塞

- 优先级50访问共享资源A,但资源A正在被优先级40访问,优先级40准备释放资源A。
- 但此时优先级46需要执行其他任务,其高于优先级40,因此中断了由优先级40的释放过程。
-
此时:中级正在运行,低级被中级阻塞(无法释放锁),高级正在等待低级释放锁。
-
两种解决方法:
-
优先级继承协议:占用资源的低优先级进程,可以继承已经申请资源的高优先级进程的优先级。
- 也就是:对于互斥体外的进程(中级)来说,将处在互斥体内的进程看成一个整体考虑,所以此时整个互斥体中,所有进程的优先级,都为较高进程的优先级(低级=高级)。
-
优先级天花板协议:占用资源进程的优先级,和所有可以申请该资源的进程的最高优先级相同。
-
也就是将可能处在互斥体的进程看成一个整体考虑。
-
因此不管是否发生等待,都会提升占用资源进程的优先级。
-
-
原理:它们均将互斥体考虑为整体,只有其他进程优先级大于整体的最高优先级时,才允许进入临界区,影响“释放”过程。此时其他进程一定不是中级进程,自然也就没有倒置发生。
互斥与同步
互斥只是同步的特例:
- 我们首先分别用3种方法实现锁以实现互斥。
- 通过其中不断的抽象,实现了信号量与管程,进而实现互斥和同步。

1. 临界区
- 三种进入进程间交互关系
- 互不影响、间接影响(需获取共享资源)、直接影响(进程直接通信)
- 同步问题属于间接影响,可能导致以下状态
- 互斥:正常状态
- 死锁:多个进程各占用部分资源,形成循环等待
- 饥饿:其他进程轮流占用资源,一个进程一直得不到资源
- 临界区的访问规则如下:
- 空闲则入:没有进程在临界区时,任何进程可以进入
- 忙则等待:有进程在临界区时,其他进程均不能进入临界区
- 有限等待:等待进入临界区的进程不能无限等待
- 让权等待(可选):不需要访问临界区的进程,应当释放CPU
2. 锁
- 禁用中断的硬件实现
- 中断处理延迟到中断使能之后,只要不发生进程切换,就不会发生死锁。
- 禁用中断之后,进程无法停止,可能导致饥饿。
- 临界区可能很长,无法确定中断响应时间。
-
仅支持单处理器
-
Peterson算法(可记为“谦让”算法)的软件实现
-
利用硬件自带的load/store原子指令
-
仅针对两个线程,防止线程A因为速度太快,在线程B提出访问请求前,直接提出请求并访问了。
-
想要访问共享资源的进程A需要:
- 举旗:表明线程A需要访问
- 谦让:如果进程B举旗,则先让它们使用
- 访问:当线程A举旗,且线程B谦让给自己(或只有自己需要访问),则可以访问资源。
-
复杂,需要两个进程间的共享数据项;还需要忙等待,浪费CPU时间。
-
类似算法:
-
还有类似的Dekkers算法,区别是Dekkers算法更容易被拓展为多线程。
-
N线程的Eisenberg和McGuire算法
-
-
软硬件共同实现
- 软件上使用一个第三方的共享变量,表示哪个进程可以访问资源(获得钥匙)
- 同时硬件需要将几个指令合为一条原子指令,使得防止锁操作被打断
- 锁操作:
- 获得钥匙Acquire:使用tas(test-and-set)或者xchg原子指令
- 释放钥匙Release:使用xchg原子指令
- 两类锁:
- spin-lock:如果无法获得钥匙,则==一直循环查看锁状态==,直到获得钥匙。
- mutex-lock:如果无法获得钥匙,则==发送中断,让OS将其休眠==。等到释放钥匙时,OS利用进程调度(遵守优先级)唤醒进程去获得钥匙。
- 单处理器和多处理器均可,还可以支持多临界区,但还是会忙等待,甚至可能造成死锁和饥饿。
- 由于需要进程调度,可能因为优先级倒置导致死锁
3. 信号量(计数器型条件同步)
- 可以实现互斥,还可以实现==条件同步==
- 信号量初值就是同步的条件。
-
信号量可分为二进制信号量(资源数目为0或1)和资源信号量(资源数目为任何非负值),基于其一可以实现另一个。
-
PV操作:
- P操作(Wait):信号量-1,操作后若信号量<0,则该信号量阻塞该进程
-
V操作(Signal):信号量+1,操作后若信号量>=0,则该信号量解除阻塞该进程
-
信号量的三种实现方法:
-
禁用中断的实现、原子指令tas的实现、类似锁的实现
-
经典例子:Buffer大小固定的生产者消费者问题
- 任何一个时间只能有一个进程操作Buffer(互斥操作)
- 使用一个二值信号量实现互斥
- Buffer空时,消费者必须等待生产者(同步约束)
- 使用一个资源信号量fullBuffers来控制消费者是否等待
- Buffer满时,生产者必须等待消费者(同步约束)
- 使用一个资源信号量emptyBuffers来控制生产者是否等待
-
注:如果只有一个buffer且buffer大小为1,则两个同步约束就能间接实现互斥,无需增加二值信号
-
P操作之间或者V操作之间是否能调换顺序?
- V操作不影响,因为它只起到唤醒进程的作用,不阻塞进程
- P操作影响,因为它会产生阻塞,而调换阻塞的顺序,可能会导致死锁
4. 条件变量与管程(进程型条件同步)
- 管程:临界区升级版,实际上它是一个第三方进程,用于保证互斥与同步。
- 包含==一个锁==来保证被管理的临界区是进程互斥的
- 包含==0或多个条件变量==来等待/通知信号量,用于管理并发访问共享数据
-
需要保证每次只能有一个线程进入管程(互斥进入),受它管理
-
管程是一种更为现代的实现互斥访问的方法。与临界区相比,管程有更好的封装,如果设计得当,则处理各种问题时,使用将极为便捷。

- 条件变量:

- 一个整型变量numWaiting表示等待进程的数量,还有一个等待队列
- Wait操作:增加变量值,移入等待队列,先释放锁的钥匙(让当前使用管程的线程释放),再试图获得锁的钥匙(让其他线程进入管程)。
- Signal操作:移出等待队列,唤醒线程,减少变量值。
- 与信号量的一个最为重要的不同:
- 使用条件变量时,如果线程进入等待状态,还能暂时放弃管程的互斥访问等待事件出现时恢复,从而并行进行其他操作,而不是简单地像对临界区那样“固守”。
-
依据不同释放处理方式,分为==两种条件变量==:
- Hoare:内部的线程优先执行,wait放在if里
- Hansen:正占用管程处于执行状态的线程优先执行,wait放在while里
-
经典例子:Buffer大小固定的生产者消费者问题

- lock实现线程互斥进入管程、count表示等待进程数、两个条件变量实现条件互斥
经典同步问题
1. 哲学家就餐
-
五人和五个筷子交替围圈,每个人有思考吃饭两个状态,吃饭需要左右两只筷子。
-
需要防止死锁,又要有尽可能多的人吃到。
-
解决思路:避免所有人都用同样的手(防止大家都摸不到另一只筷子而导致死锁)
-
法一:至多只能让4个人都用相同手,此时最后一个人必须拿左右2个筷子保证就餐。
- 5个二值信号量chopsticks[0...4],表示每根筷子都是互斥访问的
- 1个资源信号量表示最大可同手人数,初值为4,表示最多4个人可同步使用相同手
-
法二:仅当哲学家的左右筷子都空闲时才允许进餐。
- 法三:规定奇数号哲学家先拿左筷子再拿右筷子,而偶数号哲学家相反。
2. 读者写者问题
- 读读不互斥,读写和写写都互斥
-
解决思路:读者优先
-
设置一个共享变量ReadCount,用来记录读者数量
- 二值信号量CountMutex实现==ReadCount的访问互斥==
- tips:如果设置了一个变量是共享的(会被很多进程同时读/写到),一定要加锁或信号量等机制来保证互斥访问。
- 二值信号量WriteMutex实现写写互斥、有读者时的读写互斥
3.Buffer大小固定的生产者消费者问题
- 略
死锁问题
- 死锁模型(资源分配有向图)
- 进程和资源(资源内存放实例)为顶点
- 资源请求边:进程指向资源
- 资源分配边:资源的实例指向进程
-
==循环等待==的充要条件是图中存在环
-
出现死锁的==必要条件==(死锁=>四个条件,死锁</=四个条件)
- 互斥
- 非抢占
- 持有并等待
- 循环等待
- 四种处理方法:预防、避免、检测、恢复
银行家算法
- 系统资源安全状态:原理上不会死锁的状态。
-
实现思路:进程排成一个序列,前一个进程依次释放的资源 + 已有可用资源 >= 后一个的需求。
-
银行家算法就是一种基于资源安全状态判断的算法,借鉴银行贷款的策略实现。银行家算法的核心就是进行安全状态判断
-
为了利益的最大化,要考虑两个方面的因素:
- 不能超出贷款限额,防止资金链断裂
- 要使得资金流通起来,增大资金利用率
-
这两点对应就是尽可能利用资源且不超出资源总量。
-
几类资源:
- 空间资源:空间总资源(All),初始空闲资源(Available),剩余空闲资源(Available-Need)
-
进程资源:进程最大资源(Max),进程总资源(Allocation),申请资源(Need),已分配资源(Allocation+Need)
-
实现步骤:
- 当一个进程申请使用资源的时候,银行家算法通过先试探分配给该进程资源
- 然后通过安全性算法判断分配后的系统是否处于==安全状态(以下条件均需要满足)==
- 空闲资源足够分配,即:空闲资源(Available)>=进程申请资源Need(=进程最大需求MAX-已分配Allocation)
- 分配后的资源可以支撑至少一个进程结束,即:剩余资源(Available-Need)>=至少一个进程的Need
- 若不安全则试探分配作废,让该进程继续等待
- 反之则分配,当资源分配给某个进程后,进程结束运行,需要回收已分配资源(Allocation+Need)
- 等待下一个
进程通信IPC
- 直接通信和间接通信

-
阻塞通信与非阻塞通信
-
阻塞发送:发送后等待,直到对方接收
- 阻塞接收:在接收请求之后等待,直到接收
- 非阻塞发送:消息发送之后,立即进行其他操作
-
非阻塞接收:没有消息发送时,即便有接收请求也不管
-
通信链路Buffer分为三种:
-
0容量:发送方必须等待接收方
- 有限容量:通信链路缓冲队列满时,发送方必须等待
- 无限容量:发送方不需要考虑等待接收方的问题
1. 信号
信号是进程间软件中断通知和处理机制。如SIGKILL,SIGSTOP,SIGCONT等。

2. 管道
进程间==基于内存==文件的通信机制。
- 进程并不关心另一端的性质
- 子进程从父进程继承文件描述符
- 相关系统调用:
- read、write、pipe
3. 消息队列
消息队列是由操作系统维护的以字节序列为基本单位的间接通信机制。
- 每个消息是一个字节序列
- 相同标识的消息组成按先进先出顺序组成一个消息队列

- 相关系统调用:
- msgget、msgsnd、msgrcv、msgctl
4. 共享内存
在段页式内存中提到,因为有共享段的存在,两个进程可以使用同一个段。
换句话说,共享内存通信是把同一个物理内存区域同时映射到多个进程的内存地址空间的通信机制。
- 特点:快速方便地共享数据、还需要同步机制才能实现通信。