3 进程同步与通信
3.1 进程同步
3.1.1 进程同步的基本概念
进程之间的两种制约关系
- 间接相互制约关系(系统资源共享)
- 直接相互制约关系(进程间合作)
进程同步的主要任务是使并发执行的诸进程之间能有效地共享资源和相互合作,使执行的结果具有可再现性。
临界资源(critical resource):一段时间仅允许一个进程访问的资源。
临界资源可能是硬件,也可能是软件:变量,数据,表格,队列等。
并发进程对临界资源的访问必须做某种限制,否则就可能出与时间有关的错误,如:联网售票。
实例:生产者-消费者问题:
一种同步问题的抽象描述
- 计算机系统中的每个进程都可以使用或释放某类资源。这些资源可以是硬件资源,也可以是软件资源。
- 当某一进程使用某一资源时,可以看作是消费,称该进程为消费者。而当某一进程释放某一资源时,它就相当于生产者。
- 生产者-消费者之间设置一个具有n个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区;消费者进程可从一个缓冲区中取走产品去消费。
- 不允许消费者进程到一个空缓冲区去取产品;
- 不允许生产者进程向一个已装满产品且尚未取走的缓冲区投放产品。
问题分析:
- 用输入指针in指示下一个可投放产品的缓冲区,每当生产者进程生产并投放一个产品,输入指针加1
(in:=(in+1) mod n); - 用指针out指示下一个可从中获取产品的缓冲区,每当消费者进程取出一个产品,输出指针加1
(out:=(out+1) mod n) ; - (in+1) mod n = out 缓冲池满;
- (out+1) mod n=in 缓冲池空;
- counter表示缓冲池内产品的数量,counter=((n+in-out) mod n)。
- 在生产者进程中使用一局部变量nextp,用于暂时存放每次刚生产出来的产品;
- 使用一个局部变量nextc用于存放每次要消费的产品。
后面会有消费者问题的详细解决方案
临界区(critical section):临界段,在每个程序中,访问临界资源的那段程序。
注意:临界区是对某一临界资源而言的,对于不同临界资源的临界区,它们之间不存在互斥。
如程序段A、B有关于变量X的临界区,而C、D有关于变量Y的临界区,那么,A、B之间需要互斥执行,C、D之间也要互斥执行,而A与C、B与D之间不用互斥执行 。
同步机制应遵循的规则:
- 空闲让进:资源空闲允许进程进入。
- 忙则等待:若资源忙,则排队等待。
- 有限等待:占用资源的进程有时间限制,不会一直占用。
- 让权等待:暂用cpu的进程将处于等待状态时,让出cpu。
3.1.2 硬件同步机制
①利用Test-and-Set指令实现互斥
boolean TS(boolean *lock){
boolean old;
old=*lock;
*lock=TRUE;
return old;
}//lock置true,并获得之前的状态
do{
…
while TS(&lock);//实现空闲让进,忙则等待
critical section;
lock:=false;//实现有限等待,但未实现让权等待
remainder seciton;
}//若资源空闲返回false,将lock置true,并执行对应操作,最后再置false,不是false就循环检查。
②利用swap指令实现互斥
void swap(boolean *a, boolean *b){
boolean temp;
temp=*a;
*a=*b;
*b=temp;
}
do{
key=TRUE;
do{
swap(&lock, &key);
} while(key!=FALSE);
critical section;
lock:=false;
…
} while(TRUE);//原理类似上一个方法
两种方法实现空闲让进,忙则等待,有限等待,但均未实现让权等待
3.1.3 信号量机制
1、整型信号量
- 把整型信号量定义为一个整型量
- 由两个标准原子操作wait(S)(P操作)和signal(S)(V操作)来访问。
wait(S): while(S≤0);
option;
S--;
signal(S): S++;
仍未实现让权等待
2、记录型信号量
- 记录型信号量机制采取“让权等待”策略,避免了整型信号量出现的“忙等”现象。
- 实现时需要一个用于代表资源数目的整型变量value,一个用于链接所有阻塞进程的进程链表queue。
信号量是一个数据结构,定义如下:
struct semaphore
{
int value;
pointer_PCB queue;
}
P操作:
P(s)
{
s.value = s.value -1;
if (s.value < 0)
{
/*该进程状态置为阻塞状态
将该进程的PCB插入相应的阻塞队列末尾s.queue;*/
}
}
V操作:
V(s)
{
s.value = s.value +1;
if (s.value < = 0)
{
/*唤醒相应阻塞队列s.queue中阻塞的一个进程
改变其状态为就绪状态
并将其插入就绪队列*/
}
}
3、AND型信号量
例如:
两个进程A、B要求访问共享数据D、E。
为D、E分别设置用于互斥的信号量Dmutex、Emutex,初值为1。
process A: process B:
wait(Dmutex); wait(Emutex);
wait(Emutex); wait(Dmutex);
出现了AB进程相互锁死了对面,永久阻塞现象,这就是死锁现象,于是AND型信号量就产生了
- AND同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未分配给进程,其它所有可能为之分配的资源,也不分配给它。
- 实现时在wait操作中,增加一个“AND”条件,故称AND同步,或同时wait操作(Swait)。
Swait(S1, S2, …, Sn) //P原语;
{
if(S1 >=1 && S2 >= 1 && … && Sn >= 1)
{ //满足资源要求时的处理;
for (i = 1; i <= n; ++i) --Si;
}
else
{ /*某些资源不够时的处理;
调用进程将自己转为阻塞状态,
将其插入第一个小于1信号量的阻塞队列Si.queue;*/
}
}
Ssignal(S1, S2, …, Sn)
{
for (i = 1; i <= n; ++i)
{
++Si; //释放占用的资源;
for (在Si.queue中阻塞的每一个进程P)
{
从等待队列Si.queue中取出进程P;
if(判断进程P是否通过Swait中的测试) //重新判断
{
进程P进入就绪队列;
break;
}
else 进程P进入某阻塞队列;
}
}}
4、信号量集
一次需要N个某类临界资源时,就要进行N次P操作——低效又可能死锁。
- 信号量集是指同时需要多种资源、每种占用的数目不同、且可分配的资源还存在一个临界值时的信号量处理。
- 一般信号量集的基本思路就是在AND型信号量的基础上进行扩充,在一次原语操作中完成所有的资源申请。
- 进程对信号量Si的测试值为ti(表示信号量的判断条件,要求Si >= ti;即当资源数量低于ti时,便不予分配)
- 占用值为di(表示资源的申请量,即Si=Si-di)
- 对应的P、V原语格式为:
Swait(S1, t1, d1; …; Sn, tn, dn);
Ssignal(S1, d1; …; Sn, dn);
Swait(S1,t1,d1,…, Sn,tn,dn)
if S1≥t1 and … and Sn≥tn then
for i:=1 to n do
Si:=Si-di;
endfor
else
当发现有Si<ti时,该进程状态置为阻塞状态
endif
Ssignal(S1, d1,…, Sn, dn)
for i:=1 to n do
Si:=Si+di;
将与Si相关的所有阻塞进程移出 到就绪队列
endfor
一般“信号量集”可以用于各种情况的资源分配和释放
但还存在几种特殊情况:
- Swait(S, d, d)表示每次申请d个资源,当少于d个时,便不分配
- Swait(S, 1, 1)表示记录型信号量或互斥信号量
- Swait(S, 1, 0)可作为一个可控开关(当S1时,允许多个进程进入特定 - 区域;当S=0时,禁止任何进程进入该特定区域)
- 一般“信号量集”未必成对使用。如:一起申请,但不一起释放!
3.1.4 信号量的应用
1、利用信号量实现进程互斥
- 只须为临界资源设置一互斥信号量mutex,设其初始值为1;
- 将各进程访问该临界资源的临界区CS置于wait(mutex)和signal(mutex)操作之间即可。
Var mutex:semaphore :=1
begin
parbegin
process 1:begin
repeat
wait(mutex);
critical section
signal(mutex);
remainder section
until false;
end
Process 2: begin
repeat
wait(mutex);
critical section
signal(mutex);
remainder section
until false;
end
parend
2、利用信号量实现前趋关系
若干个进程为了完成一个共同任务而并发执行,在这些进程中,有些进程之间有次序的要求,有些进程之间没有次序的要求,为了描述方便,可以用一个图来表示进程集合的执行次序。
解决方法:
方法1:
- 在需要顺序执行的进程(P1→P2)间设置一个公用信号量S(标识后面的进程是否可以开始运行),供两个进程共享,并赋初值为0。
- 在进程P1中,运行…. ,signal(S)
在进程P2中,wait(S),运行…
方法2:
- 在需要顺序执行的进程(P1→P2)间设置一个同步信号量f,用来标识前面进程是否执行完成,并赋初值为0。
- 在进程P1中,运行…. ,signal(f)
在进程P2中,wait(f),运行….
方法不同之处——两种方法使用的信号量代表的含义不同,导致解决具体问题时,不同方法使用的信号量个数可能有所不同。
练习题1:
解1:
练习题2:
解析2:
3.2 经典的进程同步问题
3.2.1 生产者/消费者问题
- 生产者消费者问题是一种同步问题的抽象描述。计算机系统中的每个进程都可以使用或释放某类资源。这些资源可以是硬件资源,也可以是软件资源。
- 当某一进程使用某一资源时,可以看作是消费,称该进程为消费者。而当某一进程释放某一资源时,它就相当于生产者。
问题分析:
- 由于在此问题中有M个生产者和N个消费者,它们在执行生产活动和消费活动中要对有界缓冲区进行操作。
- 由于有界缓冲区是一个临界资源,必须互斥使用,所以还需要设置一个互斥信号量mutex,其初值为1。
- 为解决生产者消费者问题,可以设两个同步(资源)信号量:
一个代表空缓冲区的数目,用empty表示,初值为有界缓冲区的大小n;
一个代表已用缓冲区的数目,用full表示,初值为0。
代码:
P:i = 0;while (1)
{ 生产产品;
P(empty);
P(mutex);
往Buffer [i]放产品;
i = (i+1) % n;
V(mutex);
V(full);
};
Q:
j = 0;
while (1)
{
P(full);
P(mutex);
从Buffer[j]取产品;
j = (j+1) % n;
V(mutex);
V(empty);
消费产品;
};
//mutex的PV操作在一个进程中必须成对出现,且分别作为进入区和退出区。
//Empty、full各自的PV操作也必须都存在,但出现在不同的进程中
思考:资源信号量和互斥信号量的P操作是否可以交换顺序?
- 资源信号量的P、V操作应放在互斥信号量的P、V操作的外侧!
- 放在内侧可能出现死锁现象
为避免死锁:采用AND信号量解决生产者-消费者问题
P:i = 0;while (1)
{ 生产产品;
Swait(empty, mutex);
往Buffer [i]放产品;
i = (i+1) % n;
Ssignal(mutex,full);
};
Q:
j = 0;
while (1)
{
Swait(full, mutex);
从Buffer[j]取产品;
j = (j+1) % n;
Ssignal(mutex,empty);
消费产品;
};
练习题:
有一个仓库,可以存放A和B两种产品,但要求:
(1) 每次只能存入一种产品(A或B)
(2) -N<A产品数量-B产品数量<M。
其中,N和M是正整数。试用P、V操作描述产品A与B的入库过程。
解:
设互斥信号量mutex,初值为1。
设两个信号量Sa、Sb,
Sa表示还允许A产品比B产品多入库的数量
Sb表示还允许B产品比A产品多入库的数量
初值分别为M-1,N-1。
A产品入库进程:
while (1)
{
P(Sa);
P(mutex);
A产品入库
V(mutex);
V(Sb);
};
B产品入库进程:
while (1)
{
P(Sb);
P(mutex);
B产品入库
V(mutex);
V(Sa);
};
3.2.2 读者/写者问题
问题描述:有两组并发进程,读者和写者,共享一组数据区。
要求:
- 允许多个读者同时执行读操作
- 不允许读者、写者同时操作
- 不允许多个写者同时操作
读者/写者问题是指保证一个写者进程必须与其他进程互斥访问共享对象的同步问题。
问题分析:
如果读者来:
1)无读者、写者,新读者可以读
2)有读者在读,则新读者也可以读,无论有否写者等
3)有写者写,新读者等如果写者来:
1)无读者、写者,新写者可以写
2)有读者,新写者等待
3)有其它写者,新写者等待
信号量与变量的设置:
- w用于读者和写者、写者和写者之间的互斥;
- readcount表示正在读的读者数目;
- mutex用于对readcount 这个临界资源的互斥访问;
- 设一个全局变量readcount =0,设有两个信号量w=1,mutex=1 。
- 仅当readcount=0,读者进程才需要执行wait(w),同时readcount+1;
如果有写者写,就不允许读者读;
只要有一个读者在读,便不允许写者去写。
仅当readcount-1后其值为0时,读者进程才需要执行signal(w),以便让写者写。
读者:
while (1)
{
P(mutex);
if (readcount==0) P(w);
readcount ++;
V(mutex);
读
P(mutex);
readcount --;
if (readcount==0) V(w);
V(mutex);
};
写者:
while (1)
{
P(w);
写
V(w);
};
变题:
增加一个限制条件:同时读的“读者”最多R个
- wmutex 表示“无进程写”,初值是1
- rcount 表示“允许进入的读者数目”,初值为R
读者:
while(1)
{
Swait(rcount,1,1,
wmutex,1,0);
读;
Ssignal(rcount,1);
}
写者:
while(1)
{
Swait(wmutex,1,1,
rcount,R,0);
写;
Ssignal(wmutex,1);
}
3.2.3 哲学家进餐问题
- 每个哲学家的行为是思考,感到饥饿,然后吃通心粉;
- 为了吃通心粉,每个哲学家必须拿到两把叉子,并且每个人只能直接从自己的左边或右边去取叉子。
分析:
- 叉子为临界资源;
- 为每一把叉子设置一个信号量,用数组fork[i]表示,初始值为1;
Philosopher i:
while (1)
{
思考;
P(fork[i]);
P(fork[(i+1) % 5]);
进食;
V(fork[i]);
V(fork[(i+1) % 5]);
}
//此方法可能导致死锁
为防止死锁发生可采取的措施:
- 最多允许4个哲学家同时去拿左边的叉子;
- 仅当一个哲学家左右两边的叉子都可用时,才允许他拿叉子;
- 给所有哲学家编号,奇数号的哲学家必须首先拿左边的叉子,偶数号的哲学家则反之
实现代码:
解1:采用AND信号量解决哲学家就餐问题:
Philosopher i:
while (1)
{
思考;
Swait( fork[i], fork[(i+1) % 5] );
进食;
Ssignal( fork[i], fork[(i+1) % 5] );
}
解2:
- 设fork[5]为5 个信号量,初值均为1
- 设有全局变量Count 初值为0
- 设信号量Mutex、W ,初值为1
- W用于封锁第5个哲学家
- Mutex用于对临界资源Count 的访问
Philosopher i:
while (1)
{ 思考;
P(Mutex);
Count++;
if(Count>=4) P(W);
V(Mutex);
P(fork[i]);
P(fork[(i+1) % 5]);
进食;
V(fork[i]);
V(fork[(i+1) % 5]);
P(Mutex);
Count--;
if(Count==3) V(W);
V(Mutex);
}
解3:
- 设fork[5]为5 个信号量,初值为均1
- 设信号量S ,用于封锁第5个哲学家,初值为4
Philosopheri:
while (1)
{ 思考;
P(S);
P(fork[i]);
P(fork[(i+1) % 5]);
进食;
V(fork[i]);
V(fork[(i+1) % 5]);
V(S);
}
3.3 管程机制
3.3.1 管程的基本概念
- 引入的原因
可以减少大量的同步操作分散在各个进程中。 - 解决的办法——引入管程
为每个可共享资源设立一个专门的管程,来统一管理各进程对该资源的访问。 - 管程(Monitor)的定义
一个管程包含一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。 - 管程的三个部分
1.局部于管程的共享变量说明
2.对该数据结构进行操作的一组过程
3.对局部于管程的数据设置初始值的语句
管程的特点
- 局部数据变量只能被管程的过程访问,任何外部过程都不能访问。
- 一个进程通过调用管程的一个过程进入管程。
- 在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被挂起,以等待管程变成可用的
条件变量
- 管程内部的同步机制
- 每个条件变量表示一种等待原因,对应一个等待队列
- 可执行wait和signal两种操作,且应置于wait和signal之前,如x.wait,x.signal
3.3.2 利用管程解决生产者-消费者问题
- 建立管程PC
- put(item)过程 将生产的产品投放到缓冲池中,并用整形变量count来表示缓冲池中已有的产品数目,当count≥n时,表示缓冲池已满,生产者须等待。
- get(item)过程 从缓冲池中取走一个产品。当count≤0时,表示缓冲池空,消费者须等待。
Type PC=monitor
var in, out, count :integer;
buffer :array[0,…n-1] of item;
notfull, notempty :condition;
procedure entry put(item)
begin
if count≥n then notfull.wait;
buffer(in):=nextp;
in:=(in+1) mod n;
count:=count+1;
if notempty.queue then notempty.signal
end
Procedure entry get(item)
begin
if count≤0 then notempty.wait
nextc:=buffer(out);
out:=(out+1)mod n;
count:=count-1;
if notfull.queue then notfull.signal;
end
生产者:
begin
repeat
将生产的产品赋给nextp
PC. put(item);
until false;
end
消费者:
begin
repeat
PC.get(item);
将取得产品赋给nextc
until false;
end
3.4 进程通信
3.4.1 共享存储器系统(Shared-Memory System)
基于共享数据结构的通信方式
诸进程公用某些数据结构,进程通过它们交换信息。
如生产者-消费者问题中的有界缓冲区。
基于共享存储区的通信方式
- 高级通信,在存储器中划出一块共享存储区;
- 进程在通信前,向系统申请共享存储区中的一个分区,并指定该分区的关键字,若系统已经给其它进程分配了这个分区,则将该分区的描述符返回给申请者。
- 申请者把获得的共享存储分区连接到本进程上,此后可读写该公用的分区。
以上两种方式的同步互斥都要由进程自己负责。
特点:
- 最快的方法
- 一个进程写另外一个进程立即可见
- 没有系统调用干预
- 没有数据复制
- 不提供同步
3.4.2 消息传递系统(Message Passing System)
- 进程间的数据交换以消息为单位,程序员利用系统的通信原语实现通信。
- 操作系统隐藏了通信的实现细节,简化了通信程序编制的复杂性,因而得到广泛应用。
消息传递系统可分为:
1.直接通信:发送进程直接把消息发送给接收者,并将它挂在接收进程的消息缓冲队列上。接收进程从消息缓冲队列中取得消息。
(也称为消息缓冲通信。)
2.间接通信:发送进程将消息发送到某种中间实体中(信箱),接收进程从中取得消息。
(也称信箱通信。在网络中称为电子邮件系统。)
3.4.3 管道(Pipe)通信 (共享文件方式)
- 管道是最初的UNIX IPC形式,它能传送大量数据,被广泛采用。
- 所谓管道,是指用于连接一个读进程和一个写进程的文件,称pipe文件。
- 向管道提供输入的进程(称写进程),以字符流的形式将大量数据送入管道,而接受管道输出的进程(读进程)可从管道中接收数据。
- 管道可以是单工的,也可以是双工的。
- 管道分无名管道和有名管道。
管道与消息队列的区别?
- 管道是外存的,消息队列是内存的
- 管道中的消息是无界的