【注】对于单处理机,在多道程序环境下,一段时间内,宏观上有多道程序在同时执行,而在每个时刻,单处理机仅能有一道程序执行。此时操作系统
是通过分时来实现并发性的,没有真正实现并行性。
一个进程实体(进程映像)由 PCB、程序段、数据段组成。进程是动态的,进程实体(进程映像)是静态的。进程是进程实体的运行过程,是系统进行资源分配、拥有资源和调度的一个基本单位。
程序和进程的区别:
项目 | 程序 | 进程 |
---|---|---|
组成 | 代码和数据 | 代码,数据和 PCB |
特点 | 永久的,静态的 | 暂时的,动态的 |
对应关系 | 程序代码经过多次创建可对应不同进程 | 一个进程可由系统调用方法被不同进程多次调用 |
项目 | 内容 |
---|---|
进程描述信息 | 进程标识符 PID,用户标识符 UID |
进程控制和管理信息 | 各硬件使用情况信息,进程当前状态 |
资源分配清单 | 使用的文件、内存区域、I/O设备 |
CPU 相关信息 | PSW、PC,用于实现进程切换 |
进程的状态 | 描述 | CPU 占用 | 拥有资源 |
---|---|---|---|
创建态 | 进程正在被创建时,它的状态是“创建态”,在这个阶段操作系统会为进程分配资源、初始化 PCB | ~ | ~ |
就绪态 | 当进程创建完成后,便进入“就绪态”,处于就绪态的进程已经具备运行条件,但由于没有空闲 CPU,就暂时不能运行 | 否 | 是 |
运行态 | 如果一个进程此时在CPU上运行,那么这个进程处于“运行态” | 是 | 是 |
阻塞态 | 进程请求等待某个事件的发生,在这个事件发生之前,进程无法继续往下执行,此时操作系统会让这个进程下 CPU,并让它进入“阻塞态” | 否 | 否 |
终止态 | 进程请求操作系统终止该进程,此时操作系统会让该进程下 CPU,并回收内存空间等资源,最后还要回收该进程的 PCB | ~ | ~ |
进程的状态转换 | 原语操作 | 何事件会引起状态转换? | 进程主动还是被动发起? |
---|---|---|---|
创建态–>就绪态 | 创建原语 | 用户登录、作业调度、提供服务、应用请求 | 主动/被动 |
就绪态–>运行态 | 切换原语 | 其他进程的时间片用完、更高优先级进程到达 | 被动(操作系统发起) |
运行态–>就绪态 | 切换原语 | 该进程的时间片用完、更高优先级进程到达 | 被动(操作系统发起) |
运行态–>阻塞态 | 切换原语 | 当前进程主动阻塞 | 主动 |
阻塞态–>就绪态 | 唤醒原语 | 等待的事件发生 | 被动(操作系统发起) |
运行态–>终止态 | 撤销原语 | 正常结束、异常结束、用户干预 | 主动/被动 |
阻塞态–>终止态 | 撤销原语 | 正常结束、异常结束、用户干预 | 主动/被动 |
就绪态–>终止态 | 撤销原语 | 正常结束、异常结束、用户干预 | 主动/被动 |
【注 1】假设一个单处理器系统,若同时存在 m 个进程,则 m 个进程不可能同时处于就绪态,但 m 个进程可能同时处于阻塞态(此时就绪队列为空),这时 CPU 将调度闲逛进程(idel)。
【注 2】系统中有 n 个进程,其中至少有一个进程运行,则就绪队列中最多有 n–1 个进程。
原语执行的一般过程:
- 更新 PCB 信息:(a)修改当前进程状态标志;(b)往 PCB 保存当前进程的运行环境;(c)根据 PCB 恢复欲切换进程的运行环境
- 将 PCB 插入对应的队列
- 分配/回收资源
原语操作 | 过程 |
---|---|
创建原语 | (1)申请空白 PCB;(2)为新进程分配资源;(3)初始化 PCB;(4)将 PCB 插入就绪队列 |
切换原语 | (1)保护进程运行现场;(2)PCB 状态信息改为就绪态或阻塞态;(3)将 PCB 插入对应资源的就绪队列或等待队列 |
唤醒原语 | (1)等待队列找到该进程;(2)PCB 状态信息改为就绪态,将 PCB 从等待队列中移出;(3)将 PCB 插入对应资源的等待队列 |
撤销原语 | (1)找到该 PCB;(2)若处于运行态,立即剥夺 CPU;(3)终止其所有子进程,将资源还给父进程或操作系统;(4)删除 PCB |
发送原语 | 略 |
接收原语 | 略 |
【注】切换原语(切换到阻塞态)和唤醒原语需一一对应。
方式 | 描述 |
---|---|
低级方式 | 基于数据结构的共享 |
高级方式 | 基于存储区的共享 |
消息传递方式 | 描述 |
---|---|
直接通信方式 | 发送进程直接把消息发送给接收进程 |
间接通信方式 | 以“信箱”作为中间实体进行消息传递 |
间接通信方式:可以多个进程往同一个信箱 send 消息,也可以多个进程从同一个信箱中 receive 消息。
管道文件:“管道”是一个特殊的共享文件,又名 pipe 文件,其实就是在内存中开辟一个大小固定的内存缓冲区。
项目 | 内容 |
---|---|
通信方式 | 半双工通信 |
进程访问 | 互斥进行 |
管道写满 | 写进程将阻塞,直到读进程将管道中的数据取走,即可唤醒写进程 |
管道读空 | 读进程将阻塞,直到写进程往管道中写入数据,即可唤醒读进程 |
【注】多进程读一个管道的解决方案:一个管道允许多个写进程,一个读进程。
项目 | 引入进程和线程的操作系统 | 仅引入进程的操作系统 |
---|---|---|
状态 | 线程和进程都有创建态、就绪态、运行态、阻塞态、终止态 | 进程有创建态、就绪态、运行态、阻塞态、终止态 |
组成 | 线程 ID、线程控制块(TCB);进程 ID、进程控制块(PCB) | 进程 ID、进程控制块(PCB) |
调度 | 同一进程下的线程切换不引起进程切换,调度开销小;从一个进程中的线程切换到另一个进程中的线程引起进程切换(线程是调度的基本单位) | 一定引起进程切换,调度开销大(进程是调度的基本单位) |
系统资源 | 进程拥有系统资源,线程不拥有系统资源,但可以访问它从属于进程的系统资源(进程是资源分配的基本单位) | 进程拥有系统资源(进程是资源分配的基本单位) |
系统开销 | 创建、撤销、切换线程的开销比进程的小 | 创建、撤销、切换进程的开销大 |
独立性 | 每个进程都有自己独立的地址空间,而同一进程的不同线程共享同一地址空间 | 每个进程都有自己独立的地址空间 |
处理机 | 多个线程尅分配在多个处理机上 | 进程只能运行在一个处理机上 |
项目 | 用户级线程 | 内核级线程 |
---|---|---|
谁来完成管理? | 应用程序 | 操作系统内核 |
在哪完成切换? | 用户态 | 内核态 |
系统开销 | 开销小,效率高 | 开销大,效率慢 |
线程阻塞会发生什么? | 整个进程被阻塞,并发度低 | 同一进程中的其他线程还可以继续执行,并发度高 |
项目 | 一对一模型 | 多对一模型 | 多对多模型 |
---|---|---|---|
描述 | 在同一进程中,每个用户级线程各自映射到不同内核级线程(一对一) | 在同一进程中,多个用户级线程映射到一个内核级线程(多对一) | n 个用户级线程映射到 m 个内核级线程(n >= m)(多对多) |
占用的内核级线程 | 多,每个用户进程有与用户级线程同数量的内核级线程 | 少,一个进程只被分配一个内核级线程 | 一般,每个用户进程对应 m 个内核级线程 |
在哪完成切换? | 内核态 | 用户态 | 内核态 、用户态 |
系统开销 | 开销大,效率慢 | 开销小,效率高 | 一般 |
线程阻塞会发生什么? | 同一进程中的其他线程还可以继续执行,并发度高 | 整个进程被阻塞,并发度低 | ~ |
下面是一道将进程与线程、互斥结合在一起的考题:
【例】进程 P1 和 P2 均包含并发执行的线程,部分伪代码描述如下所示:
// 进程 P1
int x=0;
thread1()
{
int a;
a=1; // 1
x+=1; // 2
}
thread2()
{
int a;
a=2; // 3
x+=2; // 4
}
// 进程 P2
int x=0;
thread3()
{
int a;
a=x; // 5
x+=3; // 6
}
thread4()
{
int b;
b=x; // 7
x+=4; // 8
}
需要互斥执行的操作是( )
A.a=1 与 a=2
B.a=x 与 b=x
C.x+=1 与 x+=2
D.x+=1 与 x+=3
【解】进程有独立的地址空间,因而进程 1 和进程 2 的 x 不是同一个 x;线程共享所属进程的地址空间,因而进程 1 的 x 被线程 1 和线程 2 所共享(x 是进程 1 的全局变量),进程 2 的 x 被线程 3 和线程 4 所共享(x 是进程 2 的全局变量)。在线程内定义的 a、b 变量均为线程自己所有,不共享到其他线程(局部变量)。
显然,语句 2、4 和语句 6、8 没有关系,不会发生互斥。语句 1 和 2 不会互斥,语句 5 和 6 也不会互斥,因为 a 和 b 是线程各自的局部变量。但是进程 1 内部的语句 2 和 4 会发生互斥,考虑到机器内部的执行过程,两个线程可能同时访问 x 所在的存储器,而两个线程对存储器同时进行写操作或读操作是不允许的。同理,进程 2 内部的语句 6 和 8 会发生互斥。只有 C 项符合题意。
项目 | 要做什么? | 调度发生在? | 发生频率 | 对进程状态的影响 |
---|---|---|---|---|
作业调度(高级调度) | 从后备队列中选择合适的作业将其调入内存,并为其创建进程 | 外存–>内存(面向作业) | 低 | 无–>创建态–>就绪态 |
内存调度(中级调度) | 从挂起队列中选择合适的进程将其数据调回内存 | 外存–>内存(面向进程) | 中 | 挂起态–>就绪态(阻塞挂起–>阻塞态) |
进程调度(低级调度) | 从就绪队列中选择一个进程为其分配处理机 | 内存–>CPU | 低 | 就绪态–>运行态 |
【注】挂起和阻塞的区别:两种状态都是暂时不能获得CPU的服务,但挂起态是将进程映像调到外存去了,而阻塞态下进程映像还在内存中。
不能进行进程调度与切换的情况:
【注】进程在普通临界区中是可以进行调度、切换的。
(届时将专门开一篇文章来整理)
(届时将专门开一篇文章来整理)
同步指的是进程之间的直接制约关系。
同步互斥机制应遵守的四大准则:
【注 1】关于“让权等待”的问题,除了信号量方法能解决外,其他方案无法解决,因为这些方案都包含了 while 语句,进程会一直卡在 while 语句死等。
【注 2】注意区别“忙则等待”、“有限等待”和“让权等待”:违反“有限等待”是有人想进临界区,但一直进不了,一直死等;违反“让权等待”是有人进了临界区,其他人在干等这个人退出临界区;违反“忙则等待”是有人进了临界区,其他人也闯进来了。
互斥指的是进程之间的间接制约关系。
方法 | 描述 | 缺点 |
---|---|---|
单标志检查法 | 在进入区只“检查”,不“上锁” | 违反“空闲让进” |
双标志先检查法 | 在进入区先“检查”后“上锁”,退出区“解锁” | 违反“忙则等待” |
双标志后检查法 | 在进入区先“上锁”后“检查”,退出区“解锁” | 违反“空闲让进”“有限等待” |
Peterson 算法 | 在进入区“主动争取-主动谦让-检查对方是否想进、己方是否谦让” | 违反“让权等待” |
如何分析出这些算法的缺点?注意到这些算法通常由“检查”和“上锁”两个操作组成,而这两个操作必须一气呵成地完成才能达到目的。因此,我们只需让这些操作不再一气呵成地完成,缺点也就自然分析出来了。
如何分析出这些算法是否可能引起饥饿?将算法的缺点分析出来,自然就知道会不会产生饥饿现象了。只有违反“有限等待”的算法可能导致饥饿现象,而违反“忙则等待”则是“喂得过饱”了。
来看下面实例:
(1)单标志检查法
// 全局变量
turn = 1;
// P1 进程
while (trun != 1); // 1(检查)
critical section;
turn = 2;
remainder section;
// P2 进程
while (turn != 2); // 2(检查)
critical section;
turn = 1;
remainder section;
(2)双标志先检查法
// 全局变量
flag[0] = false;
flag[1] = false;
// P1 进程
while (flag[2]); // 1(检查)
flag[1] = TRUE; // 2(加锁)
critical section;
flag[1] = FALSE;
remainder section;
// P2 进程
while (flag[1]); // 3(检查)
flag[2] = TRUE; // 4(加锁)
critical section;
flag[2] = FALSE;
remainder section;
(3)双标志后检查法
// 全局变量
flag[0] = false;
flag[1] = false;
// P1 进程
flag[1] = TRUE; // 1(加锁)
while (flag[2]); // 2(检查)
critical section;
flag[1] = FALSE;
remainder section;
// P2 进程
flag[2] = TRUE; // 3(加锁)
while (flag[1]); // 4(检查)
critical section;
flag[2] = FALSE;
remainder section;
(4)Peterson 算法
// 注意 turn 为全局变量
// P1 进程
flag[1] = TRUE; // 1
turn = 2; // 2
while (flag[2] && turn == 2) // 3
critical section,
flag[1] = false;
remainder section;
// P2 进程
flag[2] = TRUE; // 4
turn = 1; // 5
while (flag[1] && turn == 1); // 6
critical section;
flag[2] = false;
remainder section;
方法 | 描述 | 优点 | 缺点 |
---|---|---|---|
中断屏蔽方法 | 利用“开/关中断指令”实现 | 简单高效 | 只适用于单处理机,只适用于操作系统内核进程 |
TestAndSet 指令(TSL 指令) | 使用变量 old 记录原来 lock 的值,再将 lock 设为 true,最后不断检查临界区是否已被其他进程上锁 | 实现简单,适用于多处理机 | 不满足让权等待 |
Swap 指令 | 逻辑上同 TSL 指令 | 实现简单,适用于多处理机 | 不满足让权等待 |
【注】后两者是用硬件实现的,它把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作,但仍未解决让权等待的问题。
P 操作负责分配资源,没有资源的时候就等着(进入阻塞队列)。V 操作负责释放资源,在阻塞队列不为空的时候唤醒某个进程进入临界区。
一般信号量:初值一般为可用物理资源的总数,用于进程同步问题。若期望的消息尚未产生,则初值应设为 0;若期望的消息已存在,则初值应设为一个非零正整数。
- 信号量 > 0:表示某类可用资源的数量。
- 信号量 = 0:表示某类资源已经没有。
- 信号量 < 0:表示某类资源已经没有,还有因请求该资源而被阻塞的进程。
- 信号量 ≤ 0 的绝对值:表示等待该资源的进程数。
实现功能:执行完 A 后才能执行 B。因此,执行 A 后执行 V 操作,再执行 P 操作,然后执行 B。
信号量 S=0;
P1(){
A;
V(S);
}
P2(){
P(S);
B;
}
二元信号量:取值仅为“0”或“1”,用作实现互斥。初值一定为“1”,此时 P、V 操作需夹紧临界区。
实现功能:两个进程对临界资源的互斥访问。
信号量 S=1;
P1(){
P(S);
P1的临界区;
V(S);
}
P2(){
P(S);
P2的临界区;
V(S);
}
例如:
分析:
代码:
信号量 a, b, c = 0;
P1(){
...;
V(a); V(b);
}
P2(){
P(a);
...;
V(c);
}
P3(){
P(b); P(c);
...;
}
管程的组成(类似于面向对象的类):
管程的基本特征:
信号量与条件变量:
阻塞原因定义为条件变量,每个条件变量保存一个等待队列。它们的区别:
项目 | 信号量 | 条件变量 |
---|---|---|
操作 | P、V 操作 | signal、wait 操作 |
功能 | 实现进程的阻塞、唤醒 | 实现进程的阻塞、唤醒 |
是否有值? | 有值,反映剩余资源数 | 没有值,实现排队等待的功能 |
【注】死锁和饥饿的区别:
项目 | 死锁 | 饥饿 |
---|---|---|
产生原因 | 循环等待对方手里的资源 | 长期得不到需要的 I/O 设备,或长期得不到处理机 |
进程状态 | 阻塞态 | 阻塞态或就绪态 |
进程数量 | 两个或以上 | 一个 |
【注】发生死锁时一定有循环等待,但是发生循环等待时未必死锁。
由以上四个必要条件,可得以下常用结论:
【推导思路】系统有 x 个进程,每个进程已拥有 y-1 个资源,此时如果资源总数刚好为 x*(y-1) 个,意味着资源已用完,则一定发生死锁,此时再多加一个资源,令其中一个进程获得所需的最大资源,则死锁解除。即使每个进程所需资源数各不相同,一样可以按此思路分析。
项目 | 方法 | 缺点 |
---|---|---|
破坏互斥条件 | 把互斥资源改造为共享资源(如 SPOOLing 技术) | 可行性不高 |
破坏不剥夺条件 | (1)申请的资源得不到满足时,立即释放拥有的所有资源;(2)申请的资源被其他进程占用时,由操作系统协助剥夺 | 可能导致进程部分工作失效,系统开销大,可能引起饥饿 |
破坏请求并保持条件 | 运行前分配好所有需要的资源,之后一直保持 | 资源利用率高,可能引起饥饿 |
破坏循环等待条件 | 给资源编号,必须按编号递增的顺序请求资源 | 用户编程麻烦,不方便新增新设备,资源浪费 |
安全性算法和银行家算法:
【例】已知资源可用数为 S(x,y,z),某时刻状态如下:
进程 | 最大需求 | 已拥有 | 最多还要 |
---|---|---|---|
A | (0,0,4) | (0,0,3) | (0,0,1) |
B | (1,7,5) | (1,0,0) | (0,7,5) |
C | (2,3,5) | (1,3,5) | (1,0,0) |
D | (0,6,4) | (0,0,2) | (0,6,2) |
E | (0,6,5) | (0,0,1) | (0,6,4) |
则 x、y、z 取以下值时系统处于安全状态?
(1)1,4,0;(2)0,6,2;(3)1,1,1;(4)0,4,7。
(1)【解】S(1,4,0) > A(0,0,1),S(1,4,0) > C(1,0,0),说明可以分配给 A 和 C。回收 A、C 资源后 S(1+0+1, 4+0+3, 0+3+5) = S(2,7,8)。
考虑 B、D、E 进程,S(2,7,8) > B(0,7,5),S(2,7,8) > D(0,6,2),S(2,7,8) > E(0,6,4),说明可以分配给 B、D 和 E。回收 B、D、E 资源后 S(2+1+0+0, 7+0+0, 8+0+2+1) = S(3,7,11)。
安全序列可以为 A、C、B、D、E,也可以为 C、A、D、B、E 等,因此处于安全状态。
(2)【解】S(0,6,2) > A(0,0,1),S(0,6,2) = D(0,6,2),说明可以分配给 A 和 D。回收 A、D 资源后 S(0+0+0, 6+0+0, 2+3+2) = S(0,6,7)。
考虑 B、D、E 进程,S(0,6,7) > D(0,6,2),说明可以分配给 D。回收 D 资源后 S(0+0, 6+0, 7+2) = S(0,6,9)。
考虑 B、E 进程,S(0,6,9) < B(0,7,5),S(0,6,9) < E(0,6,4),说明不可以分配给 B、E,因此系统处于不安全状态。
(3)【解】S(1,1,1) > A(0,0,1),S(1,1,1) > C(1,0,0),说明可以分配给 A 和 C。回收 A、C 资源后 S(1+0+1, 1+0+3, 1+3+5) = S(2,4,9)。
考虑 B、D、E 进程,S(2,4,9) < B(0,7,5),S(2,4,9) < D(0,6,2),S(2,4,9) < E(0,6,4),说明不可以分配给 B、D 和 E。因此处于不安全状态。
(4)【解】S(0,4,7) > A(0,0,1),说明可以分配给 A。回收 A 资源后 S(0+0, 4+0, 7+3) = S(0,4,10)。
考虑 B、C、D、E 进程,S(0,4,10) < B(0,7,5),S(0,4,10) < C(1,0,0),S(0,4,10) < D(0,6,2),S(0,4,10) < E(0,6,4),说明不可以分配给 B、C、D、E,因此处于不安全状态。
【注】如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)。
1. 死锁检测
(1)资源分配图的组成:
项目 | 解释 |
---|---|
进程结点 | 对应一个进程 |
资源结点 | 对应一类资源,一类资源可能有多个资源 |
进程结点到资源结点的边 | 进程申请一个资源(每条边代表一个) |
资源结点到进程结点的边 | 已经为进程分配了一个资源(每条边代表一个) |
(2)使用死锁检测算法简化资源分配图:
(3)死锁定理:如果某时刻系统的资源分配图是不可完全简化的,那么此时系统死锁。注意,并不是系统中所有的进程都是死锁状态,用死锁检测算法化简资源分配图后,还连着边的那些进程就是死锁进程。
2. 死锁解除