这里写目录标题
进程同步的基本概念
两种形式的制约关系
1.间接相互制约。资源共享——(CPU,I/O设备,打印机)
2.直接相互制约。进程合作——输入(A)与计算(B)进程(A->单缓冲->B)
临界资源
定义:进程在使用它们时,进程间采用互斥方式共享的资源。例如:打印机、磁带机。
临界资源即一次只允许一个进程访问的资源。
生产者-消费者问题
描述:有一组生产者进程在生产产品,并将这些产品提供给一组消费者进程去消费,这是一个著名的进程同步问题的抽象。
为了使生产者进程与消费者进程并发执行,在两者间设置一个具有n个缓冲区的缓冲池(item buffer[n]),生产者将生产的产品放入一个缓冲区中,消费者进程可从一个缓冲区中取走产品并进行消费。
不应允许消费者进程到空缓冲区中取产品,也不应允许生产者进程向满缓冲区投放产品
代码实现:
变量初始化:
int in=0,out=0;//in,out为数组单元指针,(in+1)%n=out表示缓冲池已满
int counter=0;//记录产品数量
item buffer[n];//模拟缓冲池
生产者程序:
void producer(){
while(1){
producer an item in nextp;//局部变量nextp
//nextp用于暂存每次刚生产出来的产品
...
while(counter==n);//缓冲池已满则阻塞
buffer[in]=nextp;
in=(in+1)%n;
counter++;
}
}
消费者程序:
void consumer(){
while(1){
while(counter==0);//缓冲池为空则忙等
nextc=buffer[out];
out=(out+1)%n;
counter--;
consumer the item in nextc;//局部变量nextc
//nextc用于暂存每次要消费的产品
...
}
}
上述生产者、消费者程序在顺序执行时的结果是正确的,但若并发执行,就会出现错误,原因在于两进程共享变量counter,生产者对它做counter++
操作,消费者对它做counter--
操作。两个操作用机器语言实现描述如下:
counter=5;
//register1
register1=counter;
register1=register1+1;
counter=register1;
//register2
register2=counter;
register2=register2+1;
counter=register2;
若先由生产者执行register1的三条语句,再由消费者执行register2的三条语句;或先由消费者执行register1的三条语句,再由生产者执行register2的三条语句,counter的值为5,显然正确。
但如果按照如下顺序执行:
counter=5;
register1=counter;
register1=register1+1;
register2=counter;
register2=register2-1;
counter=register1;
counter=register2;
显然,最后counter=4,得到了错误的答案,这仅仅是发生错误的一种情况。为了预防这种错误,我们应该把变量counter作为临界资源进行处理,换句话说,应该让生产者和消费者互斥地访问变量counter.
临界区
定义:每个进程中访问临界资源的代码成为临界区。
每个进程进入临界区前,应该先对欲访问的临界资源进行检查,看它是否正在被访问。如果未被访问,则进入临界区对其进行访问并将访问标志设为“正被访问”;若已有进程访问该临界资源,则本进程不能进入临界区进行访问。
代码表述如下:
while(true)
{
进入区;//用于访问前的检查
临界区;
退出区;//将临界区正在访问的标志恢复为未被访问的标志
剩余区;
}
为了使进程互斥地进入自己的临界区,我们应设置同步机构协调各进程间的运行,同步机制应遵循下述4条准则:
1.空闲让进:当无进程处于临界区时,表明临界资源处于空闲状态,应允许一个请求进入临界区的进程进入利用资源。
2.忙则等待:当已有进程进入临界区时,表明临界资源正在被访问,则其他请求进入的进程必须等待,以保证进程间互斥访问。
3.有限等待:对于请求访问临界资源的进程,应保证其在有限时间内进入自己的临界区,以免陷入“死等”状态。
4.让权等待:当进程不能进入自己的临界区,应立即释放处理机,以免陷入“忙等”状态。(非必须)
进程同步机制
4种进程同步机制:
1.软件同步机制:使用编程方法解决临界区问题,有难度、具有局限性,现在很少采用。
2.硬件同步机制:使用特殊的硬件指令,可有效实现进程互斥。
3.信号量机制:一种有效的进程同步机制,已被广泛应用。
4.管程机制:新的进程同步机制。
我们主要介绍信号量机制和管程机制。
信号量机制
信号量概念
信号量机制(semaphores):一种进程同步工具,受启发于交通控制中的信号灯。
信号量的定义:表示一类资源的物理实体,是一个与进程等待队列有关的整型变量。eg . S
信号量的值:表示资源数量,只能由wait(s)和signal(s)原子操作改变。(曾长期被称作P,V操作).
信号量的类型:整型,记录型,And型,信号量集
我们根据信号量的初始值把信号量分为:
1.互斥信号量(初值为1,1次只允许1个进程访问)
2.资源信号量(初值为大于1的正整数,代表可用资源个数)。
信号量机制介绍
1.整形信号量
定义为一个用于表示资源数目的整形量S,与一般的整形量不同,除初始化外,仅能通过P(S)、V(S)进行操作。
代码描述:
wait(S){
while(S<=0); /*do no-operation*/ //忙等
S--;//分配一个资源
}
signal(S){
S++;//释放一个资源
}
wait操作流程如下:
应用:S信号量初始值为1(S为互斥信号量,初始只有1各单位资源,一次只能由一个进程访问)。
如图,实现了进程p1和p2互斥地访问。
优点:允许多个并发进程互斥地使用同一临界资源。
缺点:忙等(如p2),同步机构还不完善。
2.记录型信号量
在整型信号量机制中的wait操作,只要是信号量S≤0,就会不断地测试,该机制并未遵循“让权等待”原则。为此,在信号量机制中,除了需要一个用于代表资源数目的整型变量value外,还应增加一个进程链表指针list,用于链接所有等待进程。
每个信号量S除一个整数值S.value外,还有一个进程等待队列S.list,存放阻塞在该信号量的各个进程PCB
代码描述:
typedef struct{
int value;
struct process_control_block *list;
}semaphore;
wait(S)和signal(S)操作可描述为:
wait(semaphore *S){
S->value--;
if(S->value<0) block(S->list);//进程自我阻塞
}
signal(semaphore *S){
S->value++;
if(S->value<=0) wakeup(S->list);//唤醒等待队列中的一个进程
}
如图,执行wait(S)后,可用资源量数目-1,这时判断如果资源量<0,说明此资源已经分配完毕,该进程调用block原语进行自我阻塞,放弃处理机,插入信号量链表中。
执行signal(S)后,执行进程释放一个资源,这时判断如果当前资源量<=0,那么应该唤醒等待队列中的一个等待进程来占用该资源。
综上分析可知,当S->value>0时,表示可用资源数目;
当S->value=0时,表示当前无可用资源同时无等待进程。
当S->value<0时,则S->value的绝对值即为该信号量链表中已阻塞进程的个数;
信号量S的取值范围
1.当仅有两个并发进程共享临界资源时,互斥信号量S.value的取值范围为[-1,1].
2.当用n个进程互斥访问临界资源时,互斥信号量S.value的取值范围为[-(n-1),1].(极端情况为:1个进程运行,n-1个进程阻塞)
忙等与阻塞的辨析
通过学习整形信号量和记录型信号量的联系与区别,我们辨析一组概念:忙等与阻塞。
忙等占用处理机,阻塞不占用处理机,阻塞的进程等待某个从临界区退出的进程执行signal(S)唤醒它。(signal(S)原语中的wakeup(S->list))
这就好比我们去吃海底捞,里面位置已经坐满了,如果我们在门口干等有顾客吃完了我们进去这叫做忙等;
而现在海底捞采取排号模式,我们只需要领取一个号在指定的休息区排队(相当于我们进入了等待链表中),有顾客吃完后排到我们了我们就可以进去就餐,这叫做阻塞。
我们只需要在指定等待区等待,而不是在门口干等影响大门正常流通,这里门口就好比处理机。
3.AND型信号量
前面的进程互斥问题,针对的是多个并发进程共享一个临界资源的情况,在有些时候,一个进程往往需要获得两个或更多共享资源。
假定有进程A,B,他们都要求访问共享数据D,E,当然D,E都应作为临界资源处理,为这两个数据分别设置互斥信号量Dmutex和Emutex,两个进程部分操作代码描述如下:
processA:
wait(Dmutex);
wait(Emutex);
processB:
wait(Dmutex);
wait(Emutex);
我们试想按照这样的操作顺序执行操作:
processA:wait(Dmutex);//Dmutex--; Dmutex=0;
processB: wait(Emutex);//Emutex--; Emutex=0;
processA: wait(Emutex);//Emutex=0;A阻塞
processB: wait(Dmutex);//Dmutex=0;B阻塞
显然,A与B进入了死锁状态,同时共享的资源越多,进程死锁的可能性就越大。
为了解决这一问题,我们引入AND型信号量机制。
AND型信号量同步的基本思想:将进程在整个运行过程中需要的所有资源,一次性全部分配给进程,待进程使用完后再一起释放。
在wait(S)操作中增加了一个“AND”条件,故称之为AND同步,或同时wait(S)操作,即Swait(Simultaneous wait),定义如下:
Swait(S1,S2,...,Sn){
while(true){
if(S1>=1&&...&&Sn>=1){
for(int i=1;i<=n;i++) Si--;
break;
}
else{
将进程阻塞于第一个<1的Si之资源的等待队列中,将程序计数器置于该原语之首;
}
}
}
Ssignal(S1,S2,...,Sn){
while(true){
for(int i=0;i<=n;i++){
Si++;
唤醒Si阻塞队列中所有进程;
}
}
}
AND型信号量采取原子操作方式:要么把进程所请求的资源全部分配给它,要么一个也不分配。
4.信号量集
信号量集是对AND信号量机制的扩充,针对进程所申请的所有资源以及该资源的不同需求量,在一次wait(S)或signal(S)中完成他们的申请或释放。
分配方式可以笼统的概括为:一次多个。代码描述如下:
Swait(S1,t1,d1,...,Sn,tn,dn){
if(S1>=t1&&...&&Sn>=tn){
Si=Si-di;
}
else{
将进程阻塞于第一个<di的Si之资源的等待队列中,将程序计数器置于该原语之首;
}
}
Ssignal(S1,d1,...,Sn,dn){
for(int i=1;i<=n;i++){
Si=Si+di;
唤醒Si阻塞队列中所有进程;
}
}
ti为资源Si对进程分配的下限值,只有Si>=ti,才允许进行分配。一旦允许分配,基于进程对该资源的需求值di(资源占用量)进行Si=Si-di;
操作。
信号量集的三种特殊情况
1.Swait(S,d,d):此时只有一个信号量S在信号量集中,但允许进程每次申请d个资源,当现有资源量小于d时不予分配。
2.Swait(S,1,1):此时信号量集已经变成一般的记录型信号量(S>1)或互斥型信号量(S=1)。
3.Swait(S,1,0):当S>=1时,允许多个进程进入临界区访问资源;当S=0时,将阻止任何进程进入临界区。它相当于一个可控开关。
管程机制
管程机制的引入
在引入信号量机制,使用同步操作wait(S)和signal(S)(P、V操作)时,也会造成错误,例如:
1.颠倒
signal(mutex);
critical section
wait(mutex);
可能会引起多个进程同时进入临界区。
2.误写
wait(mutex);
critical section
wait(mutex);
该进程执行了两次P操作,这样以来,任何进程都不能再进入临界区了。
3.遗漏
critical section
signal(mutex);
这样同样会使多个进程同时进入临界区。
为了解决这些问题,我们引入了新的进程同步工具——管程。
定义:一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。
特点:封装。语法描述如下:
monitor monitor_name{//管城名
share variable declarations;//共享变量声明
cond declarations;//条件变量声明
public://能被进程调用的过程
void P1(...){...}//对数据结构操作的过程
void P2(...){...}
...
{//管城主体
initialization code;//初始化代码
...
}
}
管城包含了面向对象的思想,将内容集中并封装在一个对象内部,隐藏实现细节。当有进程访问临界资源时,都只能通过管程间接访问,管程每次只允许一个进程进入管程执行过程,从而实现进程互斥。
管程的功能特点如下:
互斥:
- 管程中的变量只能被管程中的操作访问。
- 任何时候只有一个进程在管程中操作。
- 管程类似于临界区。
- 管程类似于临界区。
同步:
- 设置条件变量。
- 有唤醒和阻塞操作。
条件变量
条件变量:condition x,y;
操作:阻塞操作:wait、唤醒操作:signal
条件变量x,y均保存了一个链表在其内部,用于记录因该条件变量而阻塞的所有进程,同时提供两个操作:
x.wait:如果正在调用管城的进程因x条件需要而被阻塞或挂起,则调用x.wait将自己插入到x条件的等待队列上并释放管程,直到x条件变化。在进程调用x.wait后,其他进程可以使用该管程。
x.signal:如果正在调用管程的进程发现x条件发生变化,则调用x.signal启动一个因x.wait阻塞或挂起的进程,如果存在多个则选择一个重启;如果x条件没有变化,则继续执行原进程,不产生任何结果。这与信号量机制的signal(S)不同,后者总会执行s=s+1从而改变信号量的值。
通俗的讲,引起进程阻塞的条件常有多个,设置条件变量的目的是为了将条件区分开,提高管程的利用率。
小结
本篇介绍了进程同步的基本概念、临界区问题、常用的进程同步机制。在进程同步机制中重点介绍了信号量机制和管程机制。
另有几种经典进程同步问题介绍因篇幅有限,将内容放在另一篇博客中。