进程唤醒与睡眠

注:本文主要参考自<<现代操作系统>>2.3.4, 2.3.5节

进程唤醒与睡眠

使用睡眠与唤醒避免忙等待

在前一节如何避免多进程(线程)因竞争条件引发的错误?,我们提出了集中能保证多个进程互斥访问临界区,我们所提出的解决方案均使用忙等待策略,即在进程等待进入临界区时,其持续检查,直到能够进入临界区为止.我们能否在进程未满足下一步工作条件时进入休眠状态,当进程满足下一步工作条件时,由其他进程唤醒该进程呢?如果可以实现这种方法,则我们可以避免进程的忙等待,从而节省CPU资源.

操作系统提供的sleep系统调用可以使当前进程进入休眠状态,此时进程将被阻塞,进入阻塞状态,CPU的占有权被交出.如果当前某一进程达到唤醒条件,则可以执行wakeup(pid)唤醒编号为pid的进程.

使用如下生产者-消费者问题解释进程的睡眠与唤醒.为简化问题,我们假定只有一个生产者,一个消费者.如果当前用于存放待消费物品的空间有限,假定其一次最多存放N个物品.因此,如果当前空间满,则生产者应当停止生产,进入睡眠状态.当空间不满时,再唤醒生产者.如果当前空间为空,则消费者进程应当停止消费,进入休眠状态,当空间不为空时,再唤醒消费者进程.我们可以用如下伪代码实现这一算法:

int N = 100;
int count = 0;

void producer(){
	int item = produce_item();
	if(count == N)
		sleep();
	count++;
	if(count == 1)
		wakeup(consumer);
}

void consumer(){
	if(count == 0)
		sleep();
	int item = remove_item();
	count--;
	if(count == N-1)
		wakeup(producer);
	counsume_item(item);
}

以上代码初看起来没有问题,当注意到生产者进程与消费者进程具有共享内存区,且对共享内存区变量的访问并未进行有效保护.在一般情况下,两个进程同时访问共享内存区似乎不会有太大问题,例如假设此时count == 3, 此时如果消费者进程被执行,则count变为2,假设此时还未进行实际消费,由于时钟中断,进程被切换到生成者进程,此时生产者添加生产item, count变为3,随后进程切换回消费者,完成实际的消费.注意到,虽然count变量的增加与实际物品的生产与消费被划分为两个阶段,但并未造成不良后果.

但如果是更为特殊的情形,则可能就会有麻烦了.假设此时count==1, 消费者进程首先执行,假设消费者进程消费完后,count = 0, 消费者程序再次调用判断count == 0,此时发生时钟中断,由于消费者进程被中断,因此并未执行sleep()这条语句,即消费者进程还未进入休眠状态.此时切换到执行生产者进程.生产者完成生产后,count变量变为1,生产者随后执行wakeup(counsumer)系统调用唤醒消费者进程.然而,由于此时消费者进程并未进入休眠状态,因此当前信号将被忽略.等到再次切换会消费者进程时,消费者进程执行sleep()进入休眠状态,但该进程永远不会被唤醒了.因为count变量此后会一直增加到N, 永远不会再次等于1了.当count等于N时,生产者进程也进入睡眠状态.由此,两个进程均进入睡眠状态,无法再次唤醒.

问题出在哪了?问题在于唤醒消费者进程的信号发送的太早了,此时消费者进程并未进入睡眠状态,因此该信号被忽略了.如果我们在接收到唤醒信号时保存起来,当消费者进程准备进入休眠状态时,我们检查是否存在唤醒信号,如果有,则不进入休眠,并将当前唤醒信号清楚.否则,进入休眠状态.如果我们有多个进程,那我们就需要为每一进程保存可能接受到的唤醒信号,有没有其他更合适的方法呢?

信号量

信号量是E.W.Dijkstra在1965年提出的一种方法,它使用一个整型变量来累计唤醒次数,供以后使用。在他的建议中引入了一个新的变量类型,称作信号量(semaphore)。一个信号量的取值可以为0(表示没有保存下来的唤醒操作)或者为正值(表示有一个或多个唤醒操作)。

Dijkstra建议设立两种操作:down和up(分别为一般化后的sleep和wakeup)。对一信号量执行down操作,则是检查其值是否大于0。若该值大于0,则将其值减1(即用掉一个保存的唤醒信号)并继续;若该值为0,则进程将睡眠,而且此时down操作并未结束。检查数值、修改变量值以及可能发生的睡眠操作均作为一个单一的、不可分割的原子操作完成。保证一旦一个信号量操作开始,则在该操作完成或阻塞之前,其他进程均不允许访问该信号量。这种原子性对于解决同步问题和避免竞争条件是绝对必要的。所谓原子操作,是指一组相关联的操作要么都不间断地执行,要么都不执行。原子操作在计算机科学的其他领域也是非常重要的。

确保信号量能正确工作,最重要的是要采用一种不可分割的方式来实现它。通常是将up和down作为系统调用实现,而且操作系统只需在执行以下操作时暂时屏蔽全部中断:测试信号量、更新信号量以及在需要时使某个进程睡眠。由于这些动作只需要几条指令,所以屏蔽中断不会带来什么副作用。如果使用多个CPU,则每个信号量应由一个锁变量进行保护。通过TSL或XCHG指令来确保同一时刻只有一个CPU在对信号量进行操作。

up操作对信号量的值增1。如果一个或多个进程在该信号量上睡眠(在信号量为0时调用了down操作),无法完成一个先前的down操作,则由系统选择其中的一个(如随机挑选)并允许该进程完成它的down操作。于是,对一个有进程在其上睡眠的信号量执行一次up操作之后,该信号量的值仍旧是0,但在其上睡眠的进程却少了一个。信号量的值增1和唤醒一个进程同样也是不可分割的。不会有某个进程因执行up而阻塞,正如在前面的模型中不会有进程因执行wakeup而阻塞一样。

注意到,多个进程可以共用一个信号量.例如,在生产者-消费者问题中,假设存在多个生产者与多个消费者.则我们可以仅使用一个信号量来纪录对消费者进程可能的睡眠与唤醒信号.假设当前信号量为0,此时如果3个消费者进程先后尝试执行,则其无法成功执行down操作,当前信号量上有三个进程进入睡眠状态.此后,如果生产者进程被调用,则其在信号量上执行up操作,由于3个消费者进程在该信号量上被阻塞,因此随机选择其中一个进程唤醒.此时该信号量上的睡眠进程数变为2.为了表示生产者进程的睡眠与唤醒信号,我们需要使用另一个信号量.每次调用消费者进程时,在该信号量上执行up操作,每次调用生产者进程时,在该信号量上执行down操作.

基于信号量的生产者-消费者多进程解决方案

该解决方案使用了三个信号量:一个称为full,用来记录充满的缓冲槽数目;一个称为empty,记录空的缓冲槽总数;一个称为mutex,用来确保生产者和消费者不会同时访问缓冲区。full的初值为0,empty的初值为缓冲区中槽的数目,mutex初值为1。供两个或多个进程使用的信号量,其初值为1,保证同时只有一个进程可以进入临界区,称作二元信号量(binary semaphore)。如果每个进程在进入临界区前都执行一个down操作,并在刚刚退出时执行一个up操作,就能够实现互斥。

#define N 100
typedef int semaphore

semaphore mutex 1;
semaphore full 0;
semaphore empty N;

void producer(){
	int item;
	item = produce_item();
	down(&empty);
	down(&mutex);
	insert_item(item);
	up(&full);
	up(&mutex);
}

void consume(){
	int item = remove_item();
	down(&full);
	down(&mutex);
	consume_item(item);
	up(&empty);
	up(&mutex);
}

信号量的另一种用途是用于实现同步(synchronization)。信号量full和empty用来保证某种事件的顺序发生或不发生。在本例中,它们保证当缓冲区满的时候生产者停止运行,以及当缓冲区空的时候消费者停止运行。这种用法与互斥是不同的。而信号量mutex在本例中的作用则是保证互斥.

注意观察上面使用信号量的程序,如果我们在生产者进程中,将down(&mutex)放在down(&empty)前面执行可能会造成什么问题?

假设当前缓冲区已满,此时如果生产者进程执行down(&mutex)成功,则在执行down(&empty)时,会被阻塞.因此,其无法执行up(&mutex).这导致消费者进程无法执行down(&mutex).因此消费者进程无法消耗缓冲区中的内容.此时两个进程均进入阻塞状态,即死锁.

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值