操作系统中的信号量解决方案
信号量可以描述为由计数器、进程等待列表、信号和等待函数组成的对象。信号量最基本的用途就是将其初始化为1。当一个线程要进入临界区时,它会向下调用并进入该区。当另一个线程尝试做同样的事情时,操作系统会将其置于睡眠状态,因为信号量的值已经为零,因为之前调用了 down。当第一个线程完成临界区时,它会调用,这会唤醒等待进入的另一个线程。
从逻辑上讲,信号量 S 是一个整数变量,除了初始化之外,它只能通过两个原子操作访问:
- Wait(S) 或 P :如果信号量值大于 0,则递减该值。否则,等到值大于 0 后再递减。
- Signal(S) or V :增加信号量的值
忙等待信号量解决方案:
如果一个进程处于临界区,则试图进入其临界区的另一个进程必须在入口代码中不断循环。等待和信号的经典定义是——
wait (S) {
while (S<=0);
S--;
}
signal (S) {
S++;
}
实现信号量: n个进程问题的关键部分
Shared Data : semaphore mutex ; // initially mutex=1
Process P :
do {
wait (mutex) ;
signal (mutex) ;
} while (1)
带阻塞和唤醒的信号量解决方案:
在忙等待问题中,进程在等待进入其临界区时会浪费 CPU 周期。将等待操作修改为块操作。该进程可以自己阻塞而不是等待总线。将进程放入与临界区关联的等待队列中。将信号操作修改为唤醒操作。将进程的状态从等待更改为就绪。
当进程执行等待操作,发现信号量值不是正数时,进程可以阻塞自己。块操作将进程置于与信号量相关的等待队列中。当另一个进程执行信号操作时,应该重新启动一个被阻塞等待信号量的进程。被阻塞的进程应该通过唤醒操作重新启动,该操作将该进程置于就绪队列中。
为了实现信号量,我们将信号量定义为记录:
typedef struct {
int value ;
struct process *L ;
} semaphore ;
假设两个操作:
- block :暂停调用它的进程。
- wakeup (P) :恢复被阻塞进程的执行 (P)
信号量操作定义为:
wait (S) {
S.value -- ;
if ( S.value < 0 ) {
add this process to S.L ;
block ;
} }
signal (S) {
S.value ++ ;
if ( S.value <= 0 ) {
removes a process P from S.L ;
wakeup (P) ;
} }
好处 :
- 信号量易于实现且独立于机器
- 更正很容易确定
- 信号量同时获取许多资源
- 可以有许多具有不同信号量的不同临界区
- 不因忙等待而浪费资源
缺点 :
- 可以从程序中的任何地方访问信号量
- 信号量与信号量控制访问的数据之间没有语言联系
- 信号量使用不当会导致死锁或饥饿
- 无法控制或保证正确使用