头图

前言

《操作系统概论第九版》管程给我看的云里雾里的,结合一丝丝面向对象的知识,这玩意儿有点像类,然后就没有然后了。看了老半天,终于有点理解了,遂发表一下自己浅薄的见解。

场景

假设通过一个二进制信号s去控制共享变量account的访问。正常情况各个进程按照以下方式访问

wait(s);
//操作account
signal(s)

但是对信号量的操作是没有约束的
如果有进程如下访问account,该进程进入临界区将不受约束

signal(s)
//操作account
wait(s)

将信号量的操作权限暴露给用户进程,使得信号量操作不受控,从而导致信号量机制失效。为了保证信号量的获取和释放保持正确的执行顺序,可以将正确的执行顺序封装成函数,用户进程只能调用按照正确顺序执行的信号量操作代码,而无法直接操作信号量了。

例如有一个给account+1的操作
提供plus方法

plus()
{    wait(s);
    account++;
    signal(s);
}

对account的操作只能通过plus,或者其他的函数,这些函数固定了信号量操作的方式来保证互斥进入临界区。

管程

管程定义

管程就是一种将共享数据和操作封装在一起的结构。其他进程只能访问管程提供的外部函数。管程有以下特点

  • 一次只能有一个进程进入管程。
  • 只能通过外部操作访问管程内部数据。

image.png

管程补充-条件变量

每个方法能实现对共享数据的互斥访问,但不同方法访问的共享数据可能发生重叠。例如p1访问s1,s2;p2访问s1,s3。对所有调用p1方法的进程能保证对s1的互斥访问,但是p2和p1可以并发执行到访问s1的操作,为了防止这种情况的发生,还需要引入信号量来实现方法内部对s1的互斥访问。管程引入了条件变量来处理对单个共享数据的互斥访问问题

有意思的是条件变量,仍然是将数据和操作封装在一起,向外提供了wait()和signal()操作。简单理解就是将一个信号量和wait,signal操作封装起来。注意,实际上绝对不是这样。。。

  • wait挂起一个进程
  • signal唤醒一个进程,如果没有挂起进程,则不执行操作

    唤醒处理

    进程调用管程有两种情况。一种是阻塞在外部操作、一种是阻塞在条件变量上。管程要求管程内部只有一个进程执行,因此我们考虑阻塞在内部条件变量上的进程的唤醒。有两种方案

  • 唤醒并等待(signal and wait),唤醒进程等待,重启进程执行。
  • 唤醒并继续,重启进程等待直到P离开管程或等待另一个条件。

管程处理哲学家就餐问题

信号量简单处理方式
wait(chopsticks[i]);
wait(chopsticks[i+1 %5]);
的方式由于指令可以穿插执行从而导致进程只能获取到一根筷子情况的发生。
利用管程函数一次只能进入一个进程进入的方式,实现同时获取两根筷子的操作

说明

enum {THINKING, HUNGRY, EATING};表示哲学家的3个状态
用邻近哲学家的状态来表示是否能获取到两根筷子
(state[i]!=EATING&&state[(i+1)%5]!=EATING);表示哲学家可以拿到两根筷子

通过管程外部操作的方式获取两根筷子实际上执行了这样的流程

  1. 同一时间只有一个哲学家尝试获取筷子
  2. 获取到之后该哲学家开始吃饭
  3. 其余哲学家尝试获取两只筷子

如果哲学家开始吃饭,邻近的哲学家检查旁边哲学家的状态发现有一人在吃饭,因此需要阻塞自己的执行。通过引入条件变量的方式阻塞自己。

引入条件变量
condition self[5];

实现

管程
//管程
monitor DinnerPhilosophers
{
    condition self[5];

    //拿起筷子
    void pickup(i)
    {
        state[i]=HUNGRY;

        //测试旁边的哲学家是否在用餐
        test(i);
        if(state[i]!=EATING)
        {
            //没有获取到筷子
            self[i].wait();
        }
    }

    void putdown(i)
    {
        state[i]=THINKING;

        //通知旁边的哲学家继续执行
        test((i+4)%5);
        test((i+1)%5);
    }
    
    void test(i)
    {
        if(state[i]==HUNGRY&&state[(i+1)%5]!=EATING&&state[(i+4)%5]!=EATING)
        {
            state[i]=EATING;
            //这一句说明,条件变量的signal和wait操作和直接对信号量的wait和signal操作不一样
            self[i].signal();
        }
    }
}
哲学家进程
DinnerPhilosophers.pickup(i);

//用餐

DinnerPhilosophers.putdown(i);
饥饿问题

任何公平竞争的系统都可能因为运气问题导致进程无法获取到资源。即使是两个哲学家争抢,如果不限定刚吃饭的哲学家需要思考一会儿才能继续吃,都可能导致哲学家饿死呀。

管程的实现

采用信号量的管程实现

引入一个信号量mutex(初始值为1),进入管程前先执行wait(mutex),离开时管程执行sigal(mutex)实现一次只能一个进程进入管程。

进程阻塞后需要退出管程等待其他管程唤醒。阻塞管程需要释放mutex,并阻塞在其他条件上。因此引入新的信号量next和变量next_count

管程中的进程阻塞后,释放mutex,将自己添加到一个阻塞队列,用next_count统计当前阻塞的进程数。next信号量用于控制阻塞队列中进程的唤醒

因此外部函数被替换成如下

wait(mutex);
    body of F
    //在函数里面发生阻塞会执行signal(mutex),然后阻塞在别的条件上

if(next_count>0)
    signal(next);
else
    signal(mutex);

条件变量
对于每个条件变量x,都有一个信号量x_sem和一个整型变量x_count,两者都初始化为0。操作x.wait()

x_count++;
if(next_count>0)
    signal(next);
else
    signal(mutex);
wait(x_sem);
x_count--;

x.signal()

if(x_count>0)
{
    next_count++;
    signal(x_sem);
    wait(next);
    next_count--;
}

修狗
6 声望0 粉丝

而极少数上天的宠儿,他们在年少的时候就能隐约感受到那片天空的微光,这种光诱惑他们仰之弥高,钻之弥坚。多令人向往呀![链接]


« 上一篇
信号量经典题