进程同步工具之 信号量机制
进程同步、进程互斥的两种机制,这里简单总结是其中的信号量机制(Semaphores)。
建议: 不少概念涉及到进程同步的内容,所以查看这个内容时可以结合或提前参考进程同步的内容,
信号量机制是 荷兰学者 Dijkstra 提出的,这是一种卓有成效的进程同步工具。发展:整型信号量->记录型信号量->AND 型信号量->信号量集,依次讲解下。
四种信号量
整型信号量
定义:把整型信号量定义为一个用于表示资源数目的整型量 S,它与一般整型 量不同,除初始化外,仅能通过两个标准的原子操作(Atomic Operation) wait(S)和 signal(S)来访问。
PV操作由P操作原语和V操作原语组成(原语是执行时不可中断,PV操作即定义中的原子操作wait(S)和signal(S))。因此这个操作通常被叫做PV操作。
注:在荷兰文中,通过叫passeren,释放叫vrijgeven,PV操作因此得名。wait(S)即申请资源,signal(S)释放资源。
描述:
wait(S) { while(S <= 0); S--; } signal(S) { S++; }
不足:当S<=0时,就要不断检测。陷入“忙等”状态,不符合让权等待。(在进程同步总结中有详细说明)
记录型信号量
在整型信号量基础上,增加了一个进程链表指针L,链接所有等待的进程。
数据结构定义描述如下:一个整型变量value表示资源数目,进程链表指针L。
typedef struct{ int value; struct process *L; } semaphore;
相应的,wait(S)和signal(S)操作过程就是:
当wait(S)申请资源时,S.value减1(为负数时的绝对值即等待进程的数目)。若S.value<0时表示资源耗尽了,进程使用block原句 自我阻塞,放弃处理机,插入到等待进程链表中S.L。这就符合了让权等待,避免了“忙等”。
当signal(S)释放资源,S.value加1。若加 1 后仍是 S.value≤0,则表示在该链表L中,仍有等待该资源的进程被阻塞,故还应 调用 wakeup 原语,将 S.L 链表中的第一个等待进程唤醒。
如果 S.value 的初值为 1,表示只允许一个进程访问临界资源,此时的信号量转化为互斥信号量,用于进程互斥。
void wait(semaphore S) { S.value--; if(S.value<0) { block(S.L); } } void signal(semaphore S) { S.value++; if(S.value<=0){ wakeup(S.L); } }
不足:同整型信号量一样,都是正对的是共享一种临界资源。 若一个进程需要两个或更多资源后才可执行,就会出现死锁的可能,共享资源越多,进程死锁可能性越大。
比如:有两个进程A、B。两个临界资源D、E,互斥信号量(初值为1)分别是Dmutex、Emutex。
按下面执行次序,A获得了D等待E,B获得了E等待D,就处于了僵持状态,无外界干预,A、B就陷入了死锁状态。共享资源越多,进程死锁可能越大。
process A: wait(Dmutex); 于是 Dmutex=0
process B: wait(Emutex); 于是 Emutex=0
process A: wait(Emutex); 于是 Emutex=-1 A 阻塞
process B: wait(Dmutex); 于是 Dmutex=-1 B 阻塞
AND 型信号量
如上所述,整型信号量和记录型信号量当共享多种临界资源时都容易引起死锁问题。AND型信号量能够避免上述死锁情况的出现。
AND同步机制:要么把进程在整个运行过程中所请求的资源全部分配到进程(进程使用完成后一起释放),要么一个也不分配。
描述:
wait():申请资源S1-Sn,当碰到Si不满足后,则执行else里,将进程加入到Si关联的等待队列中 而进程指针移向wait()开始。wait()是原语,不可中断。
wait(S1,S2,...,Sn) { if(S1>=1 and … and Sn>=1) { for(i:=1 to n) Si--; } else { place the process in the waiting queue associated with the first Si found with Si<1,and set the program count of this process to the beginning of Swait operation } }
signal():释放资源S1-Sn,将S1-Sn关联的等待队列清空 并调入就绪队列。
signal(S1,S2,...,Sn) { for(i:=1 to n) { Si++; Remove all the process waiting in the queue associated with Si into the ready queue. } }
不足:整型信号量、记录型信号量局限在只共享一种临界资源。AND型信号量局限在单种(一种)临界资源为1,若单种资源为N(N>=1)时,AND型信号量的wait()和signal()就需操作N次,这是很低效的。
但很多情况是一个进程可能申请多种临界资源且某种临界资源数目大于1的。
信号量集
如上所述。当需要单种资源为N(N>=1),AND型操作 wait()和signal()需要操作N次,低效。
除低效问题,还考虑情景:当某种资源数低于某一下限时变不在分配,因此每次都需检测该资源数是否大于下限值。
从而衍生出了信号量集:
操作可描述如下,其中 S 为信号量,d 为需求值,而 t 为下限值。
wait():申请S1-Sn, Si需要di个,而资源Si下限为ti。当S1-Sn中,Si不满足下限值ti后,就不能分配了。进入else,进程加入到等待队列,指针移向wait()开始位置。
wait(S1,t1,d1,…,Sn,tn,dn) { if (S1>=t1 and … and Sn>=tn){ for (i:=1 to n) Si:=Si-di; } else{ Place the executing process in the waiting queue of the first Si with Si<ti and set its program counter to the beginning of the wait Operation. } }
signal():释放资源S1-Sn,Si释放di个,S1-Sn关联的等待队列清空 并调入就绪队列。
signal(S1,d1,…,Sn,dn) { for i:=1 to n do { Si:=Si+di; Remove all the process waiting in the queue associated with Si into the ready queue } }
“信号量集”的几种特殊情况:
(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 后,将阻止任何进程进入特定区。换言之,它相当于一个可
控开关。
信号量应用
实现进程互斥
为使多个进程能互斥地访问某临界资源,只须为该资源设置一互斥信号量 mutex,并设其初始值为 1,然后将各进程访问该资源的临界区 置于 wait(mutex)和 signal(mutex)操作之间即可。
Var mutex: semaphore:=1;
process 1: begin repeat wait(mutex); critical section signal(mutex); remainder seetion until false; end process 2: begin repeat wait(mutex); critical section signal(mutex); remainder section until false; end
在利用信号量机制实现进程互斥时应注意,wait(mutex)和 signal(mutex)必须成对地出现。 缺少 wait(mutex)将会导致系统混乱,不能保证对临界资源的互斥访问;而缺少 signal(mutex)将会使临界资源永远不被释放,从而使因等待该资源而阻塞的进程不能被唤醒。
利用信号量实现前趋关系
设有两个并发执行的进程 P1 和 P2。P1 中有语句 S1;P2 中有语句 S2。
我们希望在 S1 执行后再执行 S2。为实现这种前趋关系: S1->S2。
我们只须使进程 P1 和 P2 共享一个公用信号量 S,并赋予其初值为 0,将 signal(S)操作放在语句 S1 后面;而在 S2 语句前面插入 wait(S)操作,即
在进程 P1 中
S1;
signal(S);
在进程 P2 中
wait(S);
S2;
由于 S 被初始化为 0,这样,若 P2 先执行必定阻塞,只有在进程 P1 执行完 S1;signal(S);操作后使 S 增为 1 时,P2 进程方能执行语句 S2 成功。
这是最基础的实现,在此基础上可以实现更多进程间更复杂的前后驱关系,需要多个公共信号量协同操作。