finish conclusion on monitor.
This commit is contained in:
141
thu_os/chp18.md
141
thu_os/chp18.md
@@ -68,7 +68,7 @@ void Semaphore::up(){
|
||||
}
|
||||
```
|
||||
|
||||
需要指出的是,上面的代码只是示意而已,实际的实现中还有一些细节的部分在这里没有体现。在`lab7_report`里面会有具体的`P`,`V`操作的代码。
|
||||
需要指出的是,上面的代码只是示意而已,实际的实现中还有一些细节的部分在这里没有体现。在[lab7 report](lab7_report.md)里面会有具体的`P`,`V`操作的代码。
|
||||
|
||||
### 信号量的应用
|
||||
|
||||
@@ -150,3 +150,142 @@ consume(){
|
||||
```
|
||||
|
||||
需要注意的是,无论是生产者还是消费者,一开始的两个`P`操作的顺序是不能颠倒的,否则可能出现这样一种情况,比如生产者成功进入了临界区,却发现没有空闲的缓冲区可供写入了,于是就等待`emptybuffer`信号。然而,此时消费者也无法进入被占用的缓冲区,在等待`mutex`信号。系统就进入了死锁状态。
|
||||
|
||||
## 条件变量与管程
|
||||
|
||||
### 管程的基本概念
|
||||
|
||||
信号量机制已经可以很优雅地实现多个进程之间的同步互斥问题了,但是信号量的使用还是存在一些其他问题,例如前面利用信号量实现进程之间的同步,需要不同进程的`P V`操作互相匹配,这使得进程的独立性下降而进程之间的耦合性增强。一旦程序员在编程过程中,忽略或者弄错了这种匹配关系,由于进程调度的不确定性,这种错误要检查起来是非常困难的。
|
||||
|
||||
管程就是为了解决这个问题的。所谓管程,实际上就是把多个进程要同步互斥访问的这些资源全部封装起来,由操作系统统一进行管理,它是一种用于多进程互斥访问共享资源的程序结构,因为采用了面向对象方法,所以简化了进程之间的同步控制。
|
||||
|
||||
在管程的内部,实际上是使用`条件变量`来代表某一类资源,一个管程内部往往有多个条件变量。这里的条件变量,其实就类似于信号量机制中的信号量。每个条件变量都有两个操作,即`wait`操作和`signal`操作——一个进程进入管程后,需要请求某一类的资源,如果不能成功获得资源,则需要调用`wait(cond_var)`表示该进程在等待这个条件变量;当进程使用完资源以后,调用`signal(cond_var)`来释放条件变量。可见,`wait`和`signal`操作分别对应了信号量的`P`操作和`V`操作,当然它们也有一定的区别,比如管程的`wait`操作一定会使进程进入等待状态,而`P`操作则未必。
|
||||
|
||||
和信号量一样,为了使得进程可以等待某一个条件变量,每个条件变量也都设置了一个等待队列。但是和信号量不一样的是,条件变量并不管理空闲资源的数量,而只是维护了等待当前条件变量的进程个数,这样,可以给出条件变量的类定义:
|
||||
|
||||
```c
|
||||
class condvar{
|
||||
private:
|
||||
int num_waiting;
|
||||
WaitingQueue q;
|
||||
public:
|
||||
void wait();
|
||||
void signal();
|
||||
}
|
||||
```
|
||||
|
||||
这里的条件变量,本质上也是属于进程之间的共享资源,对它们的访问也需要互斥地进行。因此,任一时刻至多只能有一个进程可以进入管程执行管程代码,为此,管程还需要一个互斥访问锁。这样,就可以给出管程的类定义了:
|
||||
|
||||
```c
|
||||
class monitor{
|
||||
private:
|
||||
lock mutex;
|
||||
condvar *cv;
|
||||
public:
|
||||
//monitor routines
|
||||
}
|
||||
```
|
||||
|
||||
管程的组成如下图所示:
|
||||
|
||||

|
||||
|
||||
这样,当一个进程想要访问管程代码时,首先需要请求管程的互斥访问锁,获得了这个锁后方可进入管程,并且阻止其他进程的进入。一旦进程请求某个资源失败,它将进入等待状态并且放弃CPU的使用权,但是除此以外,它还需要释放管程锁以使其他进程也可以进入管程;当该进程终于获得这个资源时,它将从等待状态进入运行态,并且重新请求管程锁以继续执行。下面给出`wait`和`signal`的伪代码实现:
|
||||
|
||||
```c
|
||||
void condvar::wait(){
|
||||
num_waiting++;
|
||||
q.enqueue(curr_proc);
|
||||
curr_proc.state = SLEEPING;
|
||||
mutex.release(); // release lock so that other processes can enter the monitor
|
||||
schedule();
|
||||
mutex.acquire(); // re-acquire lock
|
||||
}
|
||||
|
||||
void condvar::signal(){
|
||||
if(num_waiting > 0){
|
||||
--num_waiting;
|
||||
pick up one process from the waiting queue
|
||||
wakeup(proc);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
需要注意的是,上面给出的`wait`和`signal`操作只是示意而已,在具体的应用中会根据策略的不同而具有不同的实现方式。但不管怎么样,它们的内涵都是一样的。常见的两种策略有`Hansen`管程和`Hoare`管程,它们之间的区别将在下面涉及。
|
||||
|
||||
### 利用管程实现生产者消费者问题
|
||||
|
||||
根据前面对生产者-消费者问题的分析,为了实现缓冲区的互斥访问,并不需要额外的变量,使用管程的互斥访问锁就可以了。对于生产者而言,如果缓冲区满了,则需要等待`缓冲区有空`条件,为此设置`notFull`条件变量;对于消费者而言,如果缓冲区为空,则需要等待`缓冲区非空`条件,为此设置`notEmpty`条件变量。此外,还需要一个额外的变量`count`来表示当前缓冲区被占用的个数。具体的代码如下:
|
||||
|
||||
```c
|
||||
void monitor_init(){
|
||||
int count = 0; // buffers are empty in the beginning
|
||||
lock mutex;
|
||||
cond_var notFull;
|
||||
cond_var notEmpty;
|
||||
notFull.num_waiting = 0;
|
||||
notEmpty.num_waiting = 0;
|
||||
}
|
||||
|
||||
// for producer
|
||||
void producer(){
|
||||
mutex.acquire();
|
||||
while(count == n)
|
||||
notFull.wait();
|
||||
produce();
|
||||
++count;
|
||||
notEmpty.signal();
|
||||
mutex.release();
|
||||
}
|
||||
|
||||
// for consumer
|
||||
void consumer(){
|
||||
mutex.acquire();
|
||||
while(count == 0)
|
||||
notEmpty.wait();
|
||||
consume();
|
||||
count--;
|
||||
notFull.signal();
|
||||
mutex.release();
|
||||
}
|
||||
```
|
||||
|
||||
当时看到这里,我不禁产生一些疑问——这里的管程访问代码还不是用户自己写的吗,为什么说管程代码是由操作系统管理的呢?实际上,用户编写用户程序的时候,只是写了实质性的操作代码,类似于上面的`produce()`和`consume()`处的代码,前后的请求锁、对资源的请求与释放,释放锁都是由编译器添加的,是毋须由程序员关注的。
|
||||
|
||||
### `Hansen`管程和`Hoare`管程
|
||||
|
||||
管程的实现策略有两种,分别是`Hansen`管程和`Hoare`管程,它们的主要区别在于进程调度策略的不同。
|
||||
|
||||
假定首先有一个进程进入管程执行管程的例程,它在执行过程中需要等待某个资源,因此该进程进入阻塞状态,并且放弃了CPU的使用权。此后,第二个进程得以进入管程,该进程首先是释放了第一个进程请求的资源,第一个进程因而得以被唤醒。在`Hansen`管程的语意下,第二个进程将继续执行,直到它因为等待某个资源或者执行完毕,而让出管程的访问权限,此后第一个进程才有可能被调度。`Hansen`管程执行的流程如下图所示:
|
||||
|
||||

|
||||
|
||||
以上面的生产者-消费者问题为例,此时生产者的代码就和上面给出的一样,需要注意的是这行代码:
|
||||
|
||||
```c
|
||||
......
|
||||
while(count == n)
|
||||
notFull.wait();
|
||||
......
|
||||
```
|
||||
|
||||
这是因为,在`Hansen`管程的语意下,被阻塞的进程需要和其他尚未进入管程的进程,同时竞争管程访问的权限`mutex`。因此,当被阻塞管程被唤醒后,也许有一个另外的生产者进程已经得到执行,使得当前缓冲区再次满了,被唤醒的进程需要再次判断它请求的资源是否为空闲。
|
||||
|
||||
而在`Hoare`管程的语意下,第二个进程释放第一个进程的资源后,将第一个进程唤醒,然后自己立刻进入阻塞状态,此时第一个进程将得到调度执行。该语意的实质是保证已经进入管程的阻塞进程优先于尚未进入管程的进程执行。因此,当第一个进程执行完毕后,它不会释放锁,而是直接将CPU的控制权转交给第二个进程。`Hoare`管程的执行流程如下图:
|
||||
|
||||

|
||||
|
||||
在`Hoare`管程的语意下,`wait`操作和`signal`操作都需要做一定的修改。具体说来,在`signal`操作中,唤醒一个等待进程后,不释放管程锁,将自身加入`signal queue`中进入等待状态,此时只有被唤醒的进程可以得到管程的使用权。一个进程执行管程例程结束后,首先检查是否有处于`signal queue`状态的进程,如果有,也不释放管程锁,直接选择其中一个唤醒即可。`hoare`管程的具体代码在[lab7 report](lab7_report.md)中有所体现。
|
||||
|
||||
此时,生产者-消费者问题中的生产者代码将作出如下改动:
|
||||
|
||||
```c
|
||||
void producer(){
|
||||
......
|
||||
if(count == n)
|
||||
notFull.wait();
|
||||
......
|
||||
}
|
||||
```
|
||||
|
||||
这是因为进程被唤醒后,一定可以优先得到管程的使用权,因此不需要像上面那样对条件做循环判断。
|
||||
|
||||
BIN
thu_os/images/hansen.png
Normal file
BIN
thu_os/images/hansen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
BIN
thu_os/images/hoare.png
Normal file
BIN
thu_os/images/hoare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
BIN
thu_os/images/monitor.png
Normal file
BIN
thu_os/images/monitor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user