diff --git a/thu_os/chp18.md b/thu_os/chp18.md index e753d4a..6f14b62 100644 --- a/thu_os/chp18.md +++ b/thu_os/chp18.md @@ -17,7 +17,7 @@ > 什么是信号量? -信号量是操作系统提供的一种协调共享资源访问的方法。它采用一种简明的方法来协调多个资源的访问,即采用信号量来表示当前剩余的系统资源数量。如果有一个进程请求某个资源,则将该资源的信号量递减;反之,如果某个进程释放了资源,就将该资源的信号量递增。这里对信号量的操作都是被操作系统封装起来的,因此可以看到,信号量更多的是一种抽象数据类型,由一个整型变量和两个基本操作组成,通常用`P`操作和`V`操作来表示递减和递增,分别对应了荷兰语里面的`尝试减少`和`增加`。 +信号量是操作系统提供的一种协调共享资源访问的方法。它采用一种简明的方法来协调多个资源的访问,即采用信号量来表示当前剩余的系统资源数量。如果有一个进程请求某个资源,则将该资源的信号量递减;反之,如果某个进程释放了资源,就将该资源的信号量递增。这里对信号量的操作都是被操作系统封装起来的,因此可以看到,信号量更多的是一种抽象数据类型,由一个整型变量和两个基本操作组成,通常用`P`操作和`V`操作来表示递减和递增,分别对应了荷兰语里面的`尝试减少`和`增加`。由于`PV`操作的对象,即`sem`变量实质上也是一个共享变量,对它的访问是需要互斥进行的,因此这里的`PV`操作都需要是原子操作。 ```c class Semaphore{ @@ -34,5 +34,119 @@ public: 为了实现信号量,主要就是需要实现它的两个成员函数`down`和`up`。 -当一个进程请求资源时,首先应该检查`sem`当前的值,如果`sem > 0`,表示系统还有剩余的资源,则执行`--sem`并且将资源分配给请求进程;如果`sem <= 0`,则表示当前系统已经没有这种资源了,仍然执行`--sem`,表示新增了一个进程在等待这个资源。由于当前进程得不到它请求的资源,此时应该释放CPU的控制权,调度其他进程进入运行状态。为了在资源空闲时可以唤醒当前进程,可以对每个信号量新增一个等待队列,并且将等待该信号量的进程加入等待队列中。 +当一个进程请求资源时,首先应该检查`sem`当前的值,如果`sem > 0`,表示系统还有剩余的资源,则执行`--sem`并且将资源分配给请求进程;如果`sem <= 0`,则表示当前系统已经没有这种资源了,仍然执行`--sem`,表示新增了一个进程在等待这个资源。由于当前进程得不到它请求的资源,此时应该释放CPU的控制权,调度其他进程进入运行状态。为了在资源空闲时可以唤醒当前进程,可以对每个信号量设置一个等待队列,并且将等待该信号量的进程加入等待队列中。 +当一个进程释放了它占用的资源时,首先应该执行`++sem`表示将一个资源归还给了操作系统。此时如果`sem <= 0`,则表示还有其他进程在等待这个资源,所以应该从等待队列中挑选一个进程,将其唤醒;否则,如果`sem > 0`,表示当前已经有空闲的资源了,所以没有进程在等待队列中,直接退出就可以了。 + +根据这里的分析,给出上面`Semaphone`类的伪代码实现: + +```c +class Semaphore{ +private: + int sem; + WaitQueue q; + +public: + void up(); + void down(); +} + +void Semaphore::down(){ + --sem; + if(sem < 0){ + add current process to q; + schedule(); + } +} + +void Semaphore::up(){ + ++sem; + if(sem <= 0){ + pick a process in q; + wakeup_proc(); + } +} +``` + +需要指出的是,上面的代码只是示意而已,实际的实现中还有一些细节的部分在这里没有体现。在`lab7_report`里面会有具体的`P`,`V`操作的代码。 + +### 信号量的应用 + +> 利用信号量实现临界区的互斥访问 + +参照前面提过的锁机制,为了利用信号量实现临界区的互斥访问,只需要利用信号量来实现这样一个锁。具体说来,为每个临界区构造一个信号量,其初值为1,表示一次只能有一个进程访问临界区资源。 + +进程在进入临界区之间,首先需要获得锁,即对应了信号量的`P`操作;进程退出临界区之后,需要释放锁,让其他进程也可以进入,这对应了信号量的`V`操作。因此,只需要成对地使用`P`操作和`V`操作,就实现了利用信号量对临界区的互斥访问。其代码如下: + +```c +mutex = new Semaphore(1); + +mutex->down(); +critical_section(); +mutex->up(); +``` + +和锁机制相比较,由于信号量引入了等待队列,等待进入临界区的进程可以不用占用CPU,因此可以提高CPU的使用率。另一方面,引入了等待队列后,可以实现等待信号量的进程先进先出,在一定程度上保证了调度的公平性,而锁机制则不能做到这点,进程获得临界区资源只能是随机的。 + +> 利用信号量实现进程间同步 + +进程间同步,即两个或多个进程之间的某些操作,需要具有一定的先后次序。例如进程1的`prev()`函数必须先于进程2的`next()`函数执行,如果进程2运行到了`next()`函数处时,`prev()`还没有执行,则进程2只能等待。下面就叙述如何利用信号量实现这种关系。 + +从信号量的观点来看,进程2等待的`prev`函数执行,实际上也是等待某种资源,而这种资源只有通过进程1才可以释放,在此之前这种资源的数量都是零。于是可以形成下面的代码: + +```c +condition = new semaphore(0); + +//for process 1 +process1(){ + prev(); + condition->up(); +} + +//for process 2 +process2(){ + condition->down(); + next(); +} +``` + +可以看到,为了实现两进程之间的同步,信号量必须成对地出现在两个不同的进程中,并且其位置也要相互匹配。 + +> 利用信号量实现生产者-消费者问题 + +生产者-消费者问题是指,存在某个有限大小的缓冲区,以及若干个生产者和一个消费者。每个生产者一次可以将一个单位的数据存放在缓冲区中,而消费者一次可以从缓冲区中读出一个单位的数据。每个生产者之间以及生产者与消费者之间,每次都只能有一个进程访问缓冲区。当缓冲区满时,生产者将不能生产数据;相应的,缓冲区为空时,消费者将不能读出数据。下面讨论如何利用信号量机制来实现该问题。 + +通过上面的问题描述,可以抽象出该问题中存在的若干同步互斥关系。 + ++ 任意时刻只能有一个进程进入缓冲区进行访问,即互斥访问缓冲区。 ++ 缓冲区满时,生产者必须等待消费者。 ++ 缓冲区空时,消费者必须等待生产者。 + +可见,问题的关键,就在于利用信号量实现上面三组同步互斥关系。首先,为了实现对缓冲区的互斥访问,需要设置一个二进制信号量`mutex`;为了指示生产者等待消费者的关系,设置二进制信号量`emptybuffer`,表示当前空闲缓冲区的大小;相应的,也设置信号量`fullbuffer`表示当前已被占用的缓冲区的大小。这样,根据上面叙述的利用信号量实现同步互斥的模式,可以形成下面的代码: + +```c +mutex = new semaphore(1); +emptybuffer = new semaphore(n); //buffers are all empty in the begining +fullbuffer = new semaphore(0); +int count = 0; + +//for producer +produce(){ + emptybuffer->down(); + mutex->down(); + //produce + mutex->up(); + fullbuffer->up(); +} + +//for consumer +consume(){ + fullbuffer->down(); + mutex->down(); + //consume + mutex->up(); + emptybuffer->up(); +} +``` + +需要注意的是,无论是生产者还是消费者,一开始的两个`P`操作的顺序是不能颠倒的,否则可能出现这样一种情况,比如生产者成功进入了临界区,却发现没有空闲的缓冲区可供写入了,于是就等待`emptybuffer`信号。然而,此时消费者也无法进入被占用的缓冲区,在等待`mutex`信号。系统就进入了死锁状态。