进程通信和死锁
并发进程的特点
-
对资源共享引起的互斥关系
-
间接制约关系
-
进程-资源-进程
-
eg.两个进程均要单独占用打印机
-
-
协作完成同一任务引起的同步关系
-
直接制约关系
-
进程-进程
-
eg.生产者-消费者问题
-
进程间通信
进程之间既相互依赖又相互制约(辩证法吗),既相互合作又相互竞争
进程通信模式
- 共享内存
- 建立起一块供协作进程共享的内存区域,进程通过此区域读或写数据交换信息
- 信息传递
- 通过在协作进程间交换信息实现通信

临界资源与临界区
临界资源:一次仅允许一个进程使用的系统中共享资源
临界区:并发进程访问临界资源的那段必须互斥执行的程序
并发进程进入临界区需要遵循的四个准则:
- 互斥使用:不能同时有两个进程在临界区执行
- 让权等待:等待进入临界区的进程,应释放处理机后阻塞等待
- 有空让进:临界资源空闲时,应运行一个请求临界资源的进程进入临界区
- 有限等待:不应使要进入临界区的进程无限期等待在临界区外
进程A和进程B互斥使用临界区:

Peterson算法
Peterson算法(Peterson's Algorithm)是一个经典的进程互斥(mutual exclusion)算法 ,用于在两个进程之间实现同步访问临界区(critical section),确保不会出现同时进入临界区的情况。
适用于两个进程交替执行临界区与剩余区
假设系统中只有两个进程 P0 和 P1 共享两个数据项:。
Peterson算法通过两个共享变量实现同步:
java
int turn; // 表示哪个进程可以进入临界区
boolean flag[2] // 表示哪个进程准备进入临界区
flag[i] = true表示进程 Pi 想进入临界区;turn = j表示"让 Pj 先进入临界区"。
以进程 Pi(i = 0 或 1)为例:
c
do {
flag[i] = true; // 表示我想进入临界区
turn = j; // 让对方先执行(表示礼让)
while (flag[j] && turn == j)
; // 忙等待(直到对方退出或轮到我)
// ---- 临界区 ----
...
// ----------------
flag[i] = false; // 离开临界区
// ---- 剩余区 ----
...
} while (true);
执行逻辑解释
- 意图声明 :
每个进程先设置自己的flag[i] = true,表示"我想进入临界区"。 - 礼让机制 :
把turn设置为对方的编号,让对方有优先权。 - 等待条件 :
只有在"对方也想进入(flag[j] == true)"且"现在轮到对方(turn == j)"时,才等待。 - 互斥保证 :
两个进程不可能同时满足进入条件,因此能保证同一时间最多只有一个进程在临界区。 - 退出临界区 :
将自己的flag[i] = false,表示我已经退出临界区。
eg.
cboolean flag = false; int x = 0; Thread1: while(!flag); print x; Thread2: x = 100; flag = true;线程1输出的x会是100吗?
解决进程间互斥的硬件实现方法
关中断
因为CPU只有在发生中断时才会进行进程切换
在进程刚进入临界区时立即禁止所有中断,离开前再打开中断
实现方式:
关中断
critical section
开中断
优点
- 简单有效;
- 完全保证互斥。
缺点
- 仅适用于单CPU系统
- 用户态进程不能随意关中断(危险,可能导致系统无法响应);
- 不适合多核并行系统,多核系统中,禁止中断仅对执行本指令的CPU有效,其他CPU仍将继续运行,并可访问共享资源。
测试和设置硬件指令
为每个临界区设置锁位变量w,为0表示资源空闲可用,为1表示资源已被占用
测试和设置是一条机器指令,不会被中断,可以定义如下

在进程中循环调用该指令可以检测临界资源是否为空,但这浪费了CPU的时间,所以被称为"忙等待"
信号量
信号量是表示资源的实体, 是一个整型计数器
信号量是一种用于进程合作完成一项任务的同步机制
信号量Semaphore的类型描述

操作:
- 初始化:将value初始化为该类资源的可用数量
两种原子操作
P()(也叫 wait()、down()) : 申请资源
V()(也叫 signal()、up()) : 释放资源
P(s): 如果s>0则s := s-1并继续执行;若s==0则执行者阻塞,直到其他线程做V(s)使s>0。V(s): 将s := s+1;若有线程因P(s)阻塞,则唤醒(通常唤醒一个)。
关键点 :P 与 V 必须是原子的的(不可中断的),以避免竞态条件。
二值信号量(binary semaphore) :取值只有
0或1。常用作互斥锁(mutex)的替代。初值通常为1(可用)或0(不可用)。计数信号量(counting semaphore):整数范围不限,用于表示资源数量或事件计数。初值可以为资源数(如 N 把共享资源)或 0(用于事件/消息的同步)。
信号量value为负数时,其绝对值表示在该信号量上等待的进程数目
wait(S);down(S)
signal(S);up(S)
信号量S值直观表示空闲资源数,P操作表示请求资源,该值减1,V操作表示释放资源,该值加1
- S>0,空闲资源有S个
- S=0,无空闲资源也没有进程在等待资源
- S<0,有-S个进程在等待资源
互斥
利用信号量实现n个进程间的互斥, 保证在某段临界区内同时只有一个线程访问共享资源。
二值公用信号量
-
互斥信号量mutex,初值为1
cP(mutex); // 临界区 V(mutex);
同步
利用信号量实现进程间的同步
信号量也常用于线程间的顺序/事件同步,而不是互斥。关键技巧是把信号量初值设为 0 ,由事件发生方做 V,等待方做 P。
示例:利用信号量实现计算进程与打印进程之间的同步过程。假定计算进程和打印进程共用一个单缓冲。(生产者-消费者 问题)
计算进程:信号量empty,表示缓冲区是否空,初值为1
打印进程:信号量full,表示缓冲区中是否有可供打印的计算结果,初始值为0
mutex = 1(互斥保护 buffer)
c// 生产者 P(empty); P(mutex); buffer = item; V(mutex); V(full); // 消费者 P(full); P(mutex); item = buffer; V(mutex); V(empty);

信号量分类:
- 公用信号量
- 互斥信号量,用于解决进程之间互斥进入临界区
- 私用信号量
- 同步信号量,用于解决异步环境下进程之间同步
经典IPC问题
生产者消费者问题
生产者-消费者是相互合作进程关系的一种抽象
生产者:当进程释放一个资源时,可把它看成是该资源的生产者
消费者:当进程申请使用一个资源时,可把它看成是该资源的消费者
设置三个信号量:
- empty:表示空缓冲区的个数,初值为k
- full:有数据的缓冲区个数,初值为0
- mutex:互斥访问临界区的信号量,初值为1
伪码参考如下:
c
int mutex=1, empty=k, full=0, i=0, j=0;
DataType array[k];
Producer:
...
produce a product x;
P(empty); //申请一个空缓冲
P(mutex); //申请进入缓冲区
array[i] = x; //放入产品
i = (i+1)mod k;
V(full); // 有数据的缓冲区个数加1
V(mutex); //退出缓冲区
Consumer:
...
P(full); //申请一个产品
P(mutex); //申请进入缓冲区
y = array[j]; //取产品
j = (j+1)mod k;
V(empty); //释放1个空缓冲
V(mutex); //退出缓冲区
注意P操作必须将互斥访问信号量放在之后,否则会发生死锁
读者和写者问题
要求:
- 多个读进程可以同时读这个数据区
- 一次只有一个写进程可以往数据区中写
- 若一个写进程在写,禁止任何进程读
理发师问题
设置两个信号量:
- 用s1制约理发师,初值为0,表示有0个顾客
- 用s2制约顾客,表示可用椅子数,初值为n

哲学家进餐问题
为保证不发生死锁,除了五个代表叉子的信号量外,还需要一个互斥信号量mutex
伪码参考如下
c
int fork[0]=fork[1]=...=fork[4]=1;
第i个哲学家所执行的程序:
do{
P(mutex);
P(fork[i]);
P(fork[(i+1)mod5]);
V(mutex);
吃饭
V(fork[i]);
V(fork[(i+1)mod5]);
} while(1);
进程高级通信的实现机制
高级通信是指进程采用操作系统提供的多种通信方式实现通信,如消息缓冲、信箱、管道、共享主存区等
发送进程和接收进程采用消息通信方式时可能的组合
- 非阻塞发送,阻塞接收。发送进程发送完成后继续前进,接收进程未收到消息时阻塞等待。单向通信。
- 非阻塞发送,非阻塞接收。单向通信
- 阻塞发送,阻塞接收,发送者在发送完消息后阻塞等待接收者进程发送回答消息,接收者进程在接收消息前阻塞等待,收到消息后发送回答消息。双向通信
消息缓冲通信
属于直接通信方式,实现方法:
- 系统设置消息缓冲池,包含多个缓冲区,每个缓冲区可以存放一个消息
- 当进程欲发送消息时,向系统申请一个缓冲区,将消息存入缓冲区,然后将该缓冲区链接到接收进程的消息队列上,消息队列通常位于接收进程的PCB上
发送原语:send (接收者,被发送消息始址)
接收原语:receive (发送者,接收区始址)
发送者先在自己的地址空间形成一个消息发送区,将消息写入其中。然后调用发送原语,从系统缓冲区申请一个消息缓冲区,将消息从发送区送入其中,然后挂到接收进程的消息队列
接收者调用接收原语,将消息队列内的消息接受到自己的接收区

信箱通信
间接通信方式,发送进程将消息发送到中间媒介------信箱,接收进程从中取得消息
发送原语:send(A, Msg),将一个消息Msg发送到信箱A
接收原语:receive(A,Msg),从信箱A中接收一个消息Msg
管道通信
是指用于连接一个读进程和一个写进程的共享文件,通过操作系统管理的环形缓冲区,先进先出
共享存储区
进程申请一块共享存储器,将共享存储区映射到各自地址空间,通过读或写共享存储区的数据通信
死锁产生的必要条件与解决方法
死锁是指一组中的每个进程都在等待该组中其他进程所占用资源的现象
死锁产生的必要条件
- 互斥条件,每个资源不可共享使用
- 保持和等待条件,进程因请求资源而阻塞时,对已经获得的资源保持不放
- 不剥夺条件,已分配给进程的资源不可剥夺
- 循环等待条件,存在进程循环链,链中每个进程都在等待链中的下一个进程所占用的资源。
根本原因:
- 对独占资源的共享
- 并发执行进程同步关系不当
解决死锁的方法
允许死锁发生:
- (消极)鸵鸟算法,忽略死锁,不予理睬,假定系统永不死锁
- (积极)死锁的检测和恢复:动态分配资源资源后检测是否发生死锁,是则恢复系统
不允许死锁发生:
-
(严格)死锁的预防,静态分配资源
-
(较为严格)死锁的避免,动态分配资源前判断系统安全性

死锁的预防
- 破坏互斥条件。不可破坏,可以采用spooling技术打造虚拟设备,但适用范围有限
- 破坏保持和请求条件:在进程开始前就获得全部所需系统资源,资源利用率低,且无法精确提出所需资源
- 破坏非剥夺条件:当一个进程进入阻塞申请资源而得不到满足时,强行释放其占有的资源,之后再申请,这样恢复现场要付出很高的代价
- 破坏循环等待条件:将系统全部资源按类进行全局编号排序,进程对资源的请求必须按照资源的序号递增顺序进行,但找到能满足所有进程要求的编号是不可能的
目前没什么预防的好方法
死锁的避免
基本思想:允许进程动态申请资源,系统在进行资源分配之前,先计算资源分配的安全性,若此次分配会导致系统进入不安全状态,则进程等待,否则分配资源
-
进程--资源轨迹图:示例如下
有两个进程A、B和两个资源(打印机和绘图仪)

水平坐标表示进程A执行的指令序列;垂直坐标表示进程B执行的指令序列。两个进程不可同时进入阴影区域,为防止死锁,应避免进入不安全区域
安全性算法:
-
银行家算法,之后详细说明
死锁的检测与恢复
定期启动一个软件检测系统状态,若发现死锁存在,则采取措施恢复
用进程资源图检测死锁:检查由进程和资源构成的有向图是否包含一个或多个环路,若是,则存在死锁,否则不存在。例如下

死锁的恢复:
- 故障终止一些进程
- 故障终止所有死锁进程
- 故障中止一个死锁进程
- 资源剥夺
- 夺走一个进程的资源
- 将一死锁进程回滚至获得资源之前的执行点
银行家算法
银行家算法(Banker's Algorithm)是操作系统中用于死锁避免的经典算法。它由Dijkstra提出,用来在多进程、多个可重入资源的系统中判断在给定资源分配情况下是否安全(即存在一种序列使所有进程都能按最大需求得到资源并完成),并在进程发出资源请求时决定是否授予请求以保证系统始终保持在安全状态。
系统有:
- m 类资源(每类有若干个单元)
- n 个进程
我们维护四个主要矩阵/向量(均为非负整数):
Available[m]:各类资源当前可用的单元数(总量减去已分配)。Max[n][m]:进程 i 对各类资源的最大需求(启动时或进程声明的上限)。Allocation[n][m]:当前已分配给进程 i 的各类资源数。Need[n][m]:尚需资源数,即Need[i][j] = Max[i][j] - Allocation[i][j]。
安全性定义:如果存在一个进程序列 P[i1], P[i2], ..., P[in],使得对该序列中的每个进程 P[ik],其 Need[ik] 在当时都 ≤ 当前可用资源(考虑之前序列完成后释放的资源),则称系统处于安全状态。安全状态保证不会发生死锁(因为有一条完成顺序)。
安全状态:系统能按照某种顺序,来为每个进程分配其所需资源,直至最大需求,使每个进程都可顺利完成
检查状态的方法:
- 检查剩余请求矩阵R是否有一行,其剩余请求向量 小于等于系统剩余资源向量,若不存在,则系统会死锁
- 若找到这样一行,则可以假设它获得所需资源并运行结束,将资源还给系统
- 重复两个步骤直到所有进程都标记为终止