finish conclusion on signals.
This commit is contained in:
118
thu_os/chp18.md
118
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`信号。系统就进入了死锁状态。
|
||||
|
||||
Reference in New Issue
Block a user