课程引入:进程同步与信号量
接下来这节课开始,我们再开始讲多进程图像。讲多进程图像的下一个点,前面我们讲清楚了多进程图像要想实现切换,调度是如何做的。同时,多个进程放在内存中,就会存在多进程合作的情况,而这种合作应该是合理有序的,这部分内容就是进程同步——让多进程之间的合作变得合理有序。那么怎么来实现这种合理有序呢?就要靠信号量。这堂课我们要讲清楚为什么会有信号量,以及如何依靠信号量来实现多个进程推进的合理有序,即同步。
现实案例:进程合作与同步的重要性
首先从一个例子看起,在现实社会中,司机和售货员就如同两个进程,司机的动作是启动车、运行、到站停车,售货员的动作是关门、售票、开门 ,他们各自有一套执行流程。这两个进程在同一台车上,为完成车辆的合理有序行驶,必须进行合作。
- 不合作的后果:如果二者执行顺序没有约束,比如售票员开门售票时,司机启动车辆,就会造成严重后果。所以,司机启动车辆不能随意进行,需要等待一个信号,比如售票员卖完票告知可以走了;同样,售票员开门也不是随便进行的,要等车辆到站停车的信号。
- 合作与同步的体现:司机等待售票员关门的信号再启动车辆,售票员在车辆到站后得到停车信号才开门,这就体现了进程间的合作。一个进程等待信号,另一个进程在合适的时候发送信号,从而使多个进程按照一定顺序向前推进,这就是同步。每个进程有自己的执行程序,但不是每条程序都能随便执行,有时需要停下来等待信号,当收到信号后再继续执行,这就是多进程合理有序的合作与同步。
技术案例:生产者 - 消费者模型中的同步问题
接下来以生产者 - 消费者模型为例进一步说明。有一个共享缓冲区,生产者不断向里放内容,每放完一个 counter
加一;消费者不断从里取内容,每取出一个 counter
减一 ,这是典型的多进程合作场景。
同步需求:在这个模型中,也需要合理有序的推进。当生产者发现
counter
等于缓冲区大小buff size
,即缓冲区满了,就不能继续放入,必须等待;同理,当缓冲区为空时,消费者也应该停止。
所以,进程同步的关键在于分析进程在哪些地方该停、什么时候该走。在生产者 - 消费者模型中,缓冲区满时生产者停,消费者消费后产生空闲缓冲区,就给生产者发信号让其继续;缓冲区空时消费者停,生产者生产后给消费者发信号。信号的局限性:仅依靠信号存在问题。例如,当缓冲区满时,生产者
p1
尝试放入会因counter
等于buff size
而sleep
,之后另一个生产者p2
进来,同样会sleep
。此时若消费者执行一次循环,取出一个内容,counter
变为buff size - 1
,消费者判断缓冲区未满,认为无人等待缓冲区,不会再发信号唤醒p2
,导致p2
永远无法唤醒。这说明单纯依靠counter
进行语义判断不足,不仅需要知道缓冲区中空闲个数,还需知道有多少进程在睡眠等待。
信号量的引入与原理
为解决上述问题,引入信号量。信号量是一个整数,它能记录更多信息。例如信号量等于 -2
,表示有两个进程在等待 。当消费者执行时,发现信号量为 -2
,就会唤醒阻塞队列头部的进程(如 p1
),同时信号量变为 -1
;再次执行,唤醒 p2
,信号量变为 0
。
- 信号量的语义:信号量为负数时,表示有进程阻塞,其绝对值代表等待进程的数量;为
0
时,表示没有进程等待,但也没有可用资源;为正数时,表示有可用资源,数值代表资源数量 。如消费者执行使信号量变为1
,表示还有一个空闲缓冲区,此时若有新的生产者p3
来,无需睡眠可直接执行,执行后信号量变为0
;再有生产者来,信号量变为-1
,该生产者需阻塞等待。 - 基于信号量的进程决策:进程根据信号量的值决定是否等待或唤醒其他进程。生产者申请使用资源(如空闲缓冲区)时,若信号量为负或零,说明资源不足或已用完,需等待;消费者释放资源(产生空闲缓冲区)时,若信号量为负,说明有进程在等待,需唤醒一个等待进程。
信号量与资源等待的关系
我们可以根据这个习题来回答,对于一种数量为八的资源,思考进程等待的原因。进程等待肯定是因为申请资源时没有可用资源了。在进程同步中,竞争合作体现为走走停停,而“停滞”是其中的核心,所以明确进程何时等待至关重要。当一个进程访问资源却发现没有资源时,就会进入等待状态。
这种资源对应的信号量初值应该设为八,这表示初始状态下可以使用八个该资源。当信号量的值变为零时,意味着没有资源剩余,若值再变为负数,进程就需要等待。当信号量的值为二时,说明还有两个资源可供使用,此时没有进程在等待该资源;而当信号量的值为 -2 时,则表示有两个进程正在等待该资源 。
通过信号量的值,我们能够判断系统中有多少进程在等待资源,以及还有多少资源可供使用。基于这样的判断,我们可以控制进程的执行与暂停,从而实现进程同步,其核心就在于依据信号量的值来判断进程何时该“走”、何时该“停”。
信号量的核心概念与操作
信号量的定义与作用
进程之间的同步是多个进程走走停停的合理有序推进,判断何时停要看信号量的值。当信号量为负值或 0 时,进程申请信号量会变成负值,此时进程等待;其他进程根据信号量的值,若为负,在释放信号量时进行唤醒操作;若为正,直接累加,无需发信号。信号量的实现方式
在编程实现中,判断进程是否需要等待资源是通过调用函数来完成的,这就涉及到信号量的具体实现。信号量的核心是一个整数,它记录着资源的相关信息。为了方便用户操作,我们通过定义P
、V
操作这两个接口函数来实现对信号量的控制。P
操作:当进程想要申请资源,判断自己是否应该暂停时,就调用P
操作。以P(sem)
为例,执行该操作时,首先将信号量sem
的值减 1。这是因为进程申请资源,相当于资源数量减少。如果减 1 后信号量的值小于 0,说明在本次申请之前,资源要么已经没有剩余(值为 0),要么已经处于供不应求的状态(值为负),当前进程无法获得资源,此时进程就会进入睡眠状态,并被放入与该信号量相关联的阻塞队列中。例如,生产者进程每次使用空闲缓冲区时,就需要对空闲缓冲区对应的信号量执行P
操作,以此判断是否有空闲缓冲区可供使用,如果没有则进入等待。V
操作:有进程等待,就需要有唤醒操作,这就是V
操作的作用。当进程释放资源时,会调用V
操作。执行V
操作时,将信号量的值加 1 ,这表示资源数量增加。如果加 1 后信号量的值仍然小于等于 0,说明在释放资源之前,有进程在等待该资源(信号量为负表示等待进程数,为 0 表示刚有进程等到资源),此时就需要调用wake up
函数,从阻塞队列中唤醒一个进程;如果加 1 后信号量的值大于 0 ,则表示没有进程在睡眠等待,无需进行唤醒操作。比如消费者进程产生空闲缓冲区后,就会对相应的信号量执行V
操作。- 系统调用:由于信号量操作涉及到进程睡眠等在内核态完成的操作,所以
P
和V
操作需要做成系统调用,这样上层应用程序就能通过调用系统调用来使用信号量。其中,P
操作源于荷兰语“test”,表示测试是否需要阻塞;V
操作源于荷兰语“increment”,表示增加资源数量,进而实现唤醒等待进程的功能 。
信号量解决生产者 - 消费者问题
利用信号量及其 P
、V
操作,可以有效解决生产者 - 消费者问题,实现进程间的同步与合作。在解决该问题时,关键在于分析生产者和消费者何时会暂停,并据此定义相应的信号量。
- 分析生产者与消费者的等待条件
- 生产者:当缓冲区满时会停,所以定义一个信号量
empty
表示空闲缓冲区个数,初值为buff_size
。生产者每次操作前先执行P(empty)
,测试empty
是否为 0 ,即缓冲区是否满,若满则等待。当消费者释放空闲缓冲区时,执行V(empty)
增加empty
的值 。 - 消费者:当缓冲区没有内容时会停,定义一个信号量
full
表示已生产内容的个数,初值为 0 。消费者每次操作前先执行P(full)
,测试full
是否为 0 ,即是否有内容,若无则等待。当生产者生产内容后,执行V(full)
增加full
的值 。
- 生产者:当缓冲区满时会停,所以定义一个信号量
- 互斥信号量实现共享资源互斥访问
共享缓冲区(可视为文件)的操作需要互斥,即同一时刻只能有一个进程访问。定义一个互斥信号量mutex
,初值为 1 。生产者和消费者在访问共享缓冲区前,先执行P(mutex)
,若mutex
等于 1 ,则变为 0 ,进程进入;访问结束后执行V(mutex)
释放资源,使其他进程可以进入 。
通过上述信号量的设置以及 P
、V
操作的合理运用,依据信号量数值所代表的语义,准确判断进程是否需要睡眠或唤醒其他进程,从而实现了生产者和消费者之间执行过程的合理有序,最终解决了进程同步问题,实现了二者的合作 。