信号量机制
上面说的4种软件互斥方法和3种硬件互斥方法的共同缺点是不满足 让权等待 原则。而信号量机制可以解决这个问题。
信号量S是一个用来表示系统中某种资源的可用数量的变量。
信号量机制是操作系统提供的一对用来操作信号量的 wait(S) 和 signal(S) 原语,通过 wait/signal 原语可以很方便的实现进程互斥和同步。
对信号量的操作只有3种:初始化,wait操作 和 signal操作。
从前面的知识我们知道,原语是不可中断的,原语是通过“关中断/开中断”指令实现的,关中断/开中断之间的指令就是原语指令集。因此 wait 和 signal 内的代码也是被关中断/开中断包裹着,是不可中断的,执行时不会发生进程切换的。
wait 和 signal 原语简称为P、V操作。
信号量可以分为 整型信号量(信号量是一个整型) 和 记录型信号量(信号量是一个对象,一个结构体)。因此,信号量机制也分为两种。
整型信号量
用一个整数型的变量作为信号量,用来表示系统中某种资源的可用数量。
下面是整型信号量机制的逻辑实现:
从逻辑上看,整型信号量机制类似于 双标志先检查 的互斥机制,即先检查后上锁(S--可以理解为上锁这一过程),只不过在这里,检查和上锁是原子操作,不可分割和中断。
该机制的问题依旧是不满足 “让权等待”,进程等待资源释放过程中会占用CPU。
稍微提一个问题:我们知道wait是一个原子操作,wait内的代码都不会接收中断,不会发生进程切换。如果wait陷入while循环,即使该进程时间片用完了也不会切换进程,这个CPU会一直卡在这个进程上,无法执行其他任务。除非其他进程在其他CPU执行了signal释放临界区,原来那个CPU执行的wait才能结束。但这样一直占用CPU其实不合理。这个问题留给读者,本文不过多解读,我们认为进程陷入wait的while循环后,系统依然能够在该进程时间片结束时让该进程被迫让出CPU即可。
记录型信号量
记录型信号量除了记录资源的可用数量,还包含了一个等待队列(阻塞队列)。
wait 操作申请资源时,如果可用资源数量不够,会使用 block原语 使进程进入阻塞态,主动让出CPU,并将该进程PCB挂到信号量S的等待队列中。
signal 操作给资源释放锁时,若还有别的进程 在等待这种资源,则使用 wakeup 原语 唤醒等待队列中的 一个进程,该进程从阻塞态变 为就绪态,让该进程进入临界区。(如果调用signal时没有其他进程阻塞,则不做唤醒操作)。
下面是整型信号量机制的逻辑实现,类似于:
使用记录型信号量的信号量机制实现互斥,满足了 ”让权等待“ 的要求,真正意义上完美实现了互斥。
信号量小结:无论是哪种类型的信号量,对信号量的两种操作分别表示:
wait:申请一单位的资源并对这一单位资源上锁;
signal:释放一单位的资源;
问题:能否按如下所示代码实现wait 和 signal?
答:
两种方案的区别在于wait是先上锁后检查还是先检查后上锁(是在进程表达使用意愿时上锁,还是真正可以使用资源时上锁)。
后者方案没有前者方案的wait和signal的实现完美,因为后者方案释放资源时,S.value <= 1 说明本进程是最后一个进入临界区的进程,此时可能有其他进程被阻塞,也可能没有,如果是后者,那么调用wakeup()就会做无用功。而前者方案中signal则可以明确知道 本进程在归还资源后仍然有 S.value <= 0,说明有其他进程在等待资源。因此进程只要表达自己想要使用资源的意愿即可对 S.value-- ,而非真正使用时才对 S.value--。
用信号量实现进程互斥和同步问题
信号量实现进程互斥的基本逻辑如下:
注意:对不同的临界资源需要设置 不同的互斥信号量。 P、V操作必须成对出现。缺少 P(mutex) 就不能保证临界资源的互 斥访问。缺少 V(mutex) 会导致资源 永不被释放,等待进程永不被唤醒。
信号量实现进程同步的基本逻辑如下:
1. 分析什么地方需要实现“同步关系”,即必须保证不同进程的“一前一后”执行的两个操作
2. 设置同步信号量 S, 初始为 0
3. 在进程1的“前操作”之后执行 V(S),在进程2的“后操作”之前执行 P(S),这样就可以保证如果CPU先执行后操作时进程2会被阻塞
我们可以将进程同步中的信号量S看做“某种虚拟的资 源”,刚开始是没有这种资 源的。进程2的后操作需要使用这种资源, 而又只能由进程1的前操作产生这种资源。
假如存在两个进程P1和P2,P1要执行代码1~3,P2要执行代码4~6。要求执行顺序为 1~2->4~6->3。
若先执行到 V(S) 操作,则 S++ 后 S=1。之后当执行到 P(S) 操作 时,由于 S=1,表示有可用资源,会执行 S--,S 的值变回 0, P2 进程不会执行 block 原语,而是继续往下执行代码4。
若先执行到 P(S) 操作,由于 S=0,S-- 后 S=-1,表示此时没有 可用资源,因此P操作中会执行 block 原语,主动请求阻塞。 之后当执行完代码2,继而执行 V(S) 操作, S++,使 S 变回 0, 由于此时有进程在该信号量对应的阻塞队列中,因此会在 V 操作中执行 wakeup 原语,唤醒 P2 进程。这样 P2 就可以继续 执行 代码4 了。
再看一个简单的信号量实现同步的问题。
进程 P1 中有句代码 S1,P2 中有句代码 S2 ,P3中有句代码S3 …… P6 中有句代码 S6。这些代码要求 按如下前驱图所示的顺序来执行:
1. 要为每一对前驱关系各设置一个同步信号量
2. 在“前操作”之后对相应的同步信号量执行 V 操作
3. 在“后操作”之前对相应的同步信号量执行 P 操作
实现逻辑如下:
小结:无论是用信号量实现互斥还是同步,关键都在于“阻塞”。
互斥是P和V操作在同一进程中;
同步则是P和V操作分别发生在两个不同进程中。