进程同步:异步环境下的一组并发进程因直接制约而互相发送消息、互相合作、互相等待,使得各进程按照一定的速度执行的过程。
进程同步的主要任务是使并发执行的诸进程之间能有效地共享资源和相互合作,使执行的结果具有可再现性。
进程之间的两种制约关系
间接相互制约关系:系统资源共享(互斥地访问、系统统一分配)
直接相互制约关系:进程间合作
同步强调的是进程之间的先后次序的约束,互斥强调的是对共享资源的互斥访问
进程的异步性会使进程对共享变量和数据结构等资源产生不正确的访问次序
临界资源(critical resource):一段时间仅允许一个进程访问的资源。
临界资源可能是硬件,也可能是软件:变量,数据,表格,队列等。
并发进程对临界资源的访问必须做某种限制,否则就可能出与时间有关的错误,如:联网售票。
计算机系统中的每个进程都可以使用或释放某类资源。这些资源可以是硬件资源,也可以是软件资源。当某一进程使用某一资源时,可以看作是消费,称该进程为消费者。而当某一进程释放某一资源时,它就相当于生产者。
生产者-消费者之间设置一个具有n个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区;消费者进程可从一个缓冲区中取走产品去消费。不允许消费者进程到一个空缓冲区去取产品;不允许生产者进程向一个已装满产品且尚未取走的缓冲区投放产品。
问题分析:
利用一个数组表示具有n个缓冲区的循环缓冲池
用输入指针in指示下一个可投放产品的缓冲区,每当生产者进程生产并投放一个产品,输入指针加1 (in:=(in+1) % n);
用指针out指示下一个可从中获取产品的缓冲区,每当消费者进程取出一个产品,输出指针1 (out:=(out+1) % n) ;
(in+1)%n=out 缓冲池满;
(out+1)%n=in 缓冲池空;
counter表示缓冲池内产品的数量。
在生产者进程中使用一局部变量nextp,用于暂时存放
每次刚生产出来的产品;
使用一个局部变量nextc用于存放每次要消费的产品。
必须对临界资源counter的访问进行控制!
临界区(critical section):临界段,在每个程序中,访问临界资源的那段程序。
注意:临界区是对某一临界资源而言的,对于不同临界资源的临界区,它们之间不存在互斥。
如程序段A、B有关于变量X的临界区,而C、D有关于变量Y的临界区,那么,A、B之间需要互斥执行,C、D之间也要互斥执行,而A与C、B与D之间不用互斥执行 。
同步机制应遵循的规则:
空闲让进:多中选一
忙则等待:互斥访问
有限等待:避免死等
让权等待:避免忙等
关中断是实现互斥最简单的方法。
进入锁测试之前关闭中断,直到完成锁测试并且关上锁以后再打开中断。
进程进入临界区执行期间,计算机系统不会响应中断,也不会引发调度,就不会引起线程或者进程的切换。
缺点:影响系统效率、不适合多CPU、会导致严重后果
boolean TS(boolean *lock)
{
boolean old;
old=*lock;
*lock=TRUE;
return old;
}
do
{
…
while TS(&lock);
critical section;
lock:=false;
remainder seciton;
}while(TRUE)
进程循环测试,处于忙等
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);
进程循环测试,处于忙等
1965年,由荷兰学者Dijkstra提出的一种卓有成效的进程同步机制。
经历整型信号量、记录型信号量,AND型信号量、发展为“信号量集”机制。
信号量本身是一个数据结构,用来表示资源数目(整型),但是对这个特殊的信号量只能通过两个原语来进行操作和访问。(P/V)
把整型信号量定义为一个整型量(表示资源数目),并且,由两个标准原子操作wait(S)(P操作)和signal(S)(V操作)来访问。
P/wait(S): {while (S≤0);/*do no-op*/S:=S-1;}
V/signal(S): { S:=S+1;}
记录型信号量机制采取“让权等待”策略,避免了整型信号量出现的“忙等”现象。
实现时需要一个用于代表资源数目的整型变量value(资源信号量):用一个少一个
一个用于链接所有阻塞进程的进程链表list :让权等待
信号量是一个数据结构,定义如下:
tupedef struct
{
int value;
struct Process_Control_Block *list;
}semaphore
semaphore s;
P操作
P(semaphore *s)/wait(semaphore *s)
{
s.value = s.value -1;
if (s.value < 0)
{
//该进程状态置为阻塞状态
//将该进程的PCB插入相应的阻塞队列末尾s.queue;
}
}
V操作
V(semaphore *s)/signal(semaphore *s)
{
s.value = s.value +1;
if (s.value < = 0)
{
//唤醒相应阻塞队列s.queue中阻塞的一个进程
//改变其状态为就绪状态并将其插入就绪队列
}
}
例如:两个进程A、B要求访问共享数据D、E。
为D、E分别设置用于互斥的信号量Dmutex、Emutex,初值为1
process A:
wait(Dmutex);
wait(Emutex);
process B:
wait(Emutex);
wait(Dmutex);
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; //释放占用的资源;
//将与Si相关的所有阻塞进程移出到就绪队列
}
}
一次需要N个某类临界资源时,要进行N次P操作?——低效又可能死锁。
信号量集是指同时需要多种资源、每种占用的数目不同、且可分配的资源还存在一个临界值时的信号量处理。一般信号量集的基本思路就是在AND型信号量的基础上进行扩充,在一次原语操作中完成所有的资源申请。
进程对信号量Si的测试值为t_i(表示信号量的判断条件,要求S_i >= t_i;即当资源数量低于t_i时,便不予分配)
占用值为d_i(表示资源的申请量,即S_i=S_i-d_i)
对应的P、V原语格式为:Swait(S_1, t_1, d_1; ...; S_n, t_n, d_n);Ssignal(S_1, d_1; ...; S_n, d_n);
Swait(S1,t1,d1,…, Sn,tn,dn)
if(S1≥t1 and … and Sn≥tn)then
for i:=1 to n do
Si:=Si-di;
end for
else
//当发现有Si
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)可作为一个可控开关(当S≥1时,允许多个进程进入特定区域;当S=0时,禁止任何进程进入该特定区域)
一般“信号量集”未必成对使用。如:一起申请,但不一起释放!
1.利用信号量实现进程互斥
要使多个进程能互斥地访问某临界资源:
① 为临界资源设置一互斥信号量mutex,设其初始值为1;
② 将各进程访问该临界资源的临界区CS置于wait(mutex)和signal(mutex)操作之间即可;
③ 每一个要访问该临界资源的进程在进入临界区之前,都必须对互斥信号量mutex执行wait操作;当进程退出临界区以后,需要对互斥信号量mutex执行signal操作。
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
end
当仅有两个并发进程共享临界资源时,互斥信号量mutex仅能取:-1 , 0 ,1三个值
其中:
mutex =1,表示无进程进入临界区
mutex =0,表示已有一个进程进入临界区
mutex =-1,表示已有一个进程正在等待进入临界区
Wait和signal原语的物理意义:
每执行一次wait操作,意味着请求分配一个单位的资源,描述为mutex=mutex-1; 当mutex<0表示已无资源,请求该资源的进程将被阻塞。|mutex|表示等待该信号量的等待进程数。
每执行一次signal操作,意味着释放一个单位的资源,描述为mutex=mutex+1;若mutex<0表示仍有被阻塞进程。将被阻进程队列中的第一个进程唤醒插入就绪队列中。
mutex>0时,mutex表示可使用资源数,
mutex=0时,表示已无资源可用,或表示不允许进程再进。
mutex<0时,|mutex|表示等待使用资源的进程个数。
2.利用信号量实现前趋关系
若干个进程为了完成一个共同任务而并发执行,在这些进程中,有些进程之间有次序的要求,有些进程之间没有次序的要求,为了描述方便,可以用一个前驱图来表示进程集合的执行次序。
在需要顺序执行的进程(P1→P2)间设置一个公用信号量S(标识后面的进程是否可以开始运行),供两个进程共享,并赋初值为0。
若进程P1要求在P2之前执行,为P1、P2之间的前趋关系设置一个信号量S,初始化为0,S为0表示任何一个进程也没有执行(跟S有关的进程都应该被阻塞)。
P1执行完后,置S为1,P2若要执行,必须是S为1才能执行。
在进程P1中,运行…,signal(S);在进程P2中,wait(S),运行…
设有3个信号量S1、S5、S6,初值均为0;根据下表的PV操作画出其前趋图。
设P1、P2有如下语句和执行的前趋关系:
引入的原因:大量的同步操作分散在各个进程中,给系统的管理带来麻烦,还会因为同步操作使用不当而导致死锁。
解决的办法——引入管程:为每个可共享资源设立一个专门的管程,来统一管理各进程对该资源的访问。
管程(Monitor)的定义:一个管程包含一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据(构成一个操作系统的资源管理模块)。
管程的四个部分:
管程名称
局部于管程的共享变量说明
对该数据结构进行操作的一组过程
对局部于管程的数据设置初始值的语句
管程的语法:
Type monitor-name=monitor
Variable declarations
procedure entry P1()
begin…end;
…
procedure entry Pn()
begin…end;
begin
initialization code;
end
管程的特点:
模块化(面向对象思想)、抽象数据类型、信息隐蔽(隐藏实现细节)
局部数据变量只能被管程的过程访问,任何外部过程都不能访问。封装于管程内部的过程也只能访问管程内部的数据结构。
所有进程要访问临界资源时,都只能通过管程间接访问,在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被挂起,以等待管程变成可用的。一个进程通过调用管程的一个过程进入管程。
管程和进程的区别:
进程定义的是私有的PCB;管程定义的数据结构是公共数据结构。
进程中涉及到的操作由程序的顺序执行有关系;管程中的操作是初始化以及帮助进程同步。
进程实现多道程序并发执行;管程实现共享资源互斥使用。
管程是被动执行,进程是主动工作。
进程可以并发执行;管程不能与其他调用者并发。
进程具有动态性,由创建而产生,由撤销而消亡;管程是操作系统中的一个资源模块,供进程调用。
用管程实现进程同步:
为了保证共享变量的数据一致性,管程应互斥使用。
管程通常是用于管理资源的,因此管程中有进程等待队列和相应的等待和唤醒操作。
在管程入口有一个等待队列,称为入口等待队列。仅当一个已进入管程的进程释放管程的互斥使用权时;管程才会唤醒另一个等待进程。两者必须有一个退出或停止使用管程。
在管程内部,由于执行唤醒操作,可能存在多个等待进程(等待使用管程)。
管程内部有自己的等待机制。
管程可以说明一种特殊的条件型变量:condition;实际上是一个指针,指向一个等待该条件的PCB队列。
条件变量实际上是区分阻塞的原因。
对条件变量的访问只能在管程中进行,管程中对每个条件变量都必须予以说明:condition x,y;
对条件变量的操作仅能采用PV操作,因此条件变量也是一种抽象的数据类型,每个条件变量保存了一个链表,用于记录因为该条件变量而阻塞的所有进程。
条件变量
管程利用wait原语让进程等待时,等待的原因可用条件变量condition区别
应置于wait和signal之前
每个条件变量还关联一个链表
例如:X:condition,
X.Wait 表示正在调用管程的进程原因因为X条件需要被阻塞或者挂起。调用X.Wait 将自己插在X条件的等待队列上,并释放管程直到X条件发生变化。X.Signal 表示正在调用管程的进程发现X条件发生了变化,则调用 X.signal重新启动一个因为X条件而被阻塞的进程,如果有多个进程选择一个;如果没有被阻塞的进程,不产生任何后果.
生产者消费者问题是一种同步问题的抽象描述。计算机系统中的每个进程都可以使用或释放某类资源。这些资源可以是硬件资源,也可以是软件资源。
当某一进程使用某一资源时,可以看作是消费,称该进程为消费者。而当某一进程释放某一资源时,它就相当于生产者。
通过一个有界缓冲区可以把一群生产者p1,p2…,pm,和一群消费者Q1,Q2, …,Qn联系起来。
只要缓冲区未满,生产者就可以把产品送入缓冲区;
只要缓冲区未空,消费者就可以从缓冲区中取走产品。
由于在此问题中有M个生产者和N个消费者,它们在执行生产活动和消费活动中要对有界缓冲区进行操作。
由于有界缓冲区是一个临界资源,必须互斥使用,所以还需要设置一个互斥信号量mutex,其初值为1。
为解决生产者消费者问题,可以设两个同步(资源)信号量:
一个代表空缓冲区的数目,用empty表示,初值为有界缓冲区的大小n;
一个代表已用缓冲区的数目,用full表示,初值为0。
mutex的PV操作在一个进程中必须成对出现,且分别作为进入区和退出区。
empty的P操作和full的V操作必须在同一进程成对出现。
Empty、full各自的PV操作也必须都存在,但出现在不同的进程中。
资源信号量的P、V操作应放在互斥信号量的PV操作的外侧。
采用AND信号量解决生产者-消费者问题:
利用管程解决生产者-消费者问题:
建立管程PC(包括以下两个过程)
put(X)过程 将生产的产品投放到缓冲池中,并用整形变量count来表示缓冲池中已有的产品数目,当count≥n时,表示缓冲池已满,生产者须等待。
get(X)过程 从缓冲池中取走一个产品。当count≤0时,表示缓冲池空,消费者须等待。
五个哲学家围坐在一圆桌旁,桌中央有一盘通心粉,每人面前有一只空盘子,每两人之间放一只筷子;每个哲学家的行为是思考,感到饥饿,然后吃面;为了吃面,每个哲学家必须拿到两只筷子,并且每个人只能直接从自己的左边或右边去取筷子。
分析:
筷子为临界资源;
为每一只筷子设置一个信号量,
用数组chopstick[i]表示,初始值为1。
记录型信号量解法会出现死锁。为防止死锁发生还可采取的措施:
最多允许4个哲学家同时去拿左边的叉子;
仅当一个哲学家左右两边的叉子都可用时,才允许他拿叉子;(√)
给所有哲学家编号,奇数号的哲学家必须首先拿左边的叉子,偶数号的哲学家则反之。
AND信号量解决哲学家就餐问题:
Philosopher i:
while (1)
{
//思考;
Swait(chopstick[i], chopstick[(i+1) % 5] );
//进食;
Ssignal(chopstick[i], chopstick[(i+1) % 5] );
}
有两组并发进程: :读者和写者,共享一组数据区。读者/写者问题是指保证一个写者进程必须与其他进程互斥访问共享对象的同步问题。
要求:
允许多个读者同时执行读操作
不允许读者、写者同时操作
不允许多个写者同时操作
分析:
如果读者来:
无读者、写者,新读者可以读
有读者在读,则新读者也可以读,无论有否写者等
有写者写,新读者等
如果写者来:
无读者、写者,新写者可以写
有读者,新写者等待
有其它写者,新写者等待
记录型信号量解法:
wmutex用于读者和写者、写者和写者之间的互斥;
readcount表示正在读的读者数目;
rmutex用于对readcount 这个临界资源的互斥访问;
所以,本问题的记录型信号量解决方案设一个全局变量readcount =0,设有两个信号量wmutex =1,rmutex=1。
仅当readcount=0,读者进程才需要执行wait(wmutex),同时readcount+1;
如果有写者写,就不允许读者读;
只要有一个读者在读,便不允许写者去写。
仅当readcount-1后其值为0时,读者进程才需要执行signal(wmutex),以便让写者写。
信号量集解法:
增加一个限制条件:同时读的“读者”最多RN个
mx 表示“无进程写”,初值是1
L 表示“允许进入的读者数目”,初值为RN