操作系统-进程PV操作——生产者消费者问题

Posted Mount256

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统-进程PV操作——生产者消费者问题相关的知识,希望对你有一定的参考价值。

文章目录

生产者消费者问题的万能方法步骤

Step 1. 有几类进程

按题意,区分有几类进程,每类进程对应一个函数。

按题意,若进程是无限循环的,则应当加上while(1)死循环体;如果不是,则不用加。

注意,不是说题目中有几个进程就有几个进程,要按题目意思。

Step 2. 用中文描述动作

按题意,用中文描述每类进程的操作。

Step 3. 添加 PV 操作,用中文描述里面的操作

(1)添加一般的信号量

从第一个进程开始,思考里面的每一步操作之前是否需要 P 操作。若需要,则再思考对应的 V 操作需要添加到哪个地方。因为 PV 操作都是成对出现的,所以这种方法能有效避免漏操作。

一般来说,某操作之前的 P 操作都是检查缓冲区有没有空位,而对应的 V 操作则是缓冲区的空位增加。

定义信号量是在下一个步骤进行的,因此,可以先用中文描述各类 PV 操作,例如:

  • P(缓冲区有空位?若有,空位-1,若无则阻塞);
  • V(空位+1);
  • P(缓冲区有产品?若有,产品-1,若无则阻塞);
  • V(产品+1);

这样写出中文描述的好处是可以知道信号量的作用是什么,更方便我们接下来去定义信号量。

(2)添加互斥信号量

在思考完所有操作之后,还需要注意对缓冲区的互斥访问条件,即所谓的“互斥锁”,也可以先用中文描述互斥的 PV 操作,例如:

  • P(互斥锁加锁);
  • V(互斥锁解锁);

以上两个步骤都是最基本的步骤,下面的第四步需要大家认真读题并思考才能发现其中隐含的互斥条件。

Step 4. 检查是否出现死锁

(1)检查连续 P 操作是否出现死锁

这里所指的连续多个 P 操作是对一般信号量的连续 P 操作,不包括互斥信号量的 P 操作,后文均默认为此种情况。

像这种出现连续 P 操作的题目,进程所需要的资源个数一般大于等于 2。根据死锁的原理,进程获得资源就不会释放,满足请求和保持条件,因而如果两个进程的资源数不够,但是又互相拿着对方所需的资源,那么就会发生死锁。(参见题目 4)

我们要做的,是检查多个信号量的 P 操作连续出现的地方,是否可能产生死锁。解决方案是:连续 P 操作要一气呵成,需要多加一个互斥信号量,在连续 P 操作的头尾加上互斥锁。

(2)即使不是连续的 P 操作,也有可能发生死锁

这种情况发生在一个进程既需要多个资源,又需要进行多个操作的情况,因为获得资源和完成操作是交错进行的,既不能一次性获得所有资源,又不能一次性完成所有操作,这时候就容易导致死锁。(参见题目 8)

解决办法很简单,把所有申请资源的 P 操作都写在一起,转化为连续的 P 操作,这样就变成了先前所述的第一种情况,用先前所述办法解决即可。

(3)检查共享缓冲区是否有隐含的死锁

当多个进程共享同一个缓冲区时,就可能会出现其中一个进程推进过快,而导致缓冲区先行被该进程的产品所占满,其他进程无法放入产品的情况。(参见题目 6)

比如,进程 A 生产 A,进程 B 生产 B,消费者需要的资源是一个 A 和一个 B,缓冲区空位为 N。现在缓冲区已经放满了 A,消费者已经拥有了 A,还需要 B,但是缓冲区已经满了,进程 B 放不了 B,结果就发生了死锁现象。

所以,要想不死锁,就不能让其中一个进程全部占满缓冲区,进程 A 最多只能放 N-1 个 A,多了就肯定死锁;同理,进程 B 最多只能放 N-1 个 B,多了也肯定死锁。

这种问题的解决思路一般是将问题转化为两个缓冲区来解决,方法是构造两个虚拟的缓冲区,一个给进程 A 用,一个给进程 B 用,A 缓冲区最大空位为 N-1,B 缓冲区最大空位为 N-1。放产品的时候,需要同时往真实的缓冲区和虚拟缓冲区放。进程先检测真实缓冲区是否有空位;如果有,则再检测自己的虚拟缓冲区是否有空位,如果也有,才能放进真实缓冲区和虚拟缓冲区。

Step 5. 定义信号量

直接按照 PV 操作中的中文描述去定义信号量以及初始值。

对于互斥信号量,初始值一定为 1。

对于前驱后继的信号量,初始值一般为 0。

对于一般的信号量,初始值要看题意,一般来说,各个进程开始执行前的状态即为初始值。例如,在生产着消费者问题中,未运行前的缓冲区的空位数为 N,而产品数为 0,因为还没运行,还没生产出产品,缓冲区里自然也没有产品。

信号量的命名习惯一般为:

  • P(缓冲区有空位?若有,空位-1,若无则阻塞);,该信号量命名为 empty
  • V(空位+1);,该信号量命名为 empty
  • P(缓冲区有产品?若有,产品-1,若无则阻塞);,该信号量命名为 full
  • V(产品+1);,该信号量命名为 full
  • P(互斥锁加锁);,该信号量命名为 mutex
  • V(互斥锁解锁);,该信号量命名为 mutex

如果缓冲区空位数为 1,则记录该空位的信号量既是一般的信号量又是互斥信号量,可充当两个作用,这点需要注意了。所以当缓冲区为 1 时,可以只需要一个记录空位的信号量,而不需要另外的互斥信号量。

题目 1:单生产者、单消费者、单缓冲区、单次取出

【题目 1】一组生产者进程和一组消费者进程共享一个初始为空、大小为 N 的缓冲区,只有没其他进程使用缓冲区时,其中的一个进程才能访问缓冲区。对于消费者来说,只有缓冲区不空时才能访问缓冲区并读取信息;对于生产者来说,只有缓冲区不满时才能访问缓冲区并写入信息。

Step 1. 有几类进程

两类:生产者、消费者。

两类进程都是不断重复执行,因而需要加上循环体。

写出代码框架:

生产者()
    while(1)
        
    


消费者()
    while(1)
    
    


Step 2. 中文描述动作

生产者()
    while(1)
        生产产品;
        把产品放入缓冲区;
    


消费者()
    while(1)
        从缓冲区取出产品;
        消费产品;
    


Step 3. 添加 PV 操作,用中文描述里面的操作

(1)生产者进程

  • 生产产品之前不需要 P 操作。
  • 把产品放入缓冲区之前,需要先检查缓冲区是否有空位,如果有空位,那么空位 - 1,表明该产品占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在消费者进程的从缓冲区取出产品之后,产品已被取出,空位 + 1,因此需要一个 V 操作。

//表示新加入的代码,下同)

生产者()
    while(1)
        生产产品;
        P(缓冲区有空位?若有,空位-1,若无则阻塞); //
        把产品放入缓冲区;
    


消费者()
    while(1)
        从缓冲区取出产品;
        V(空位+1); //
        消费产品;
    


(2)消费者进程

  • 从缓冲区取出产品之前,需要先检查缓冲区是否有产品,如果有产品,那么产品 - 1,表明该进程已经取走了一个产品;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要产品 + 1?在生产者进程的把产品放入缓冲区之后,产品 + 1,因此需要一个 V 操作。
  • 消费产品之前不需要 P 操作。
生产者()
    while(1)
        生产产品;
        P(缓冲区有空位?若有,空位-1,若无则阻塞);
        把产品放入缓冲区;
        V(产品+1); //
    


消费者()
    while(1)
        P(缓冲区有产品?若有,产品-1,若无则阻塞); //
        从缓冲区取出产品;
        V(空位+1);
        消费产品;
    


(3)缓冲区

由于缓冲区不能同时进行读写,所以需要一个互斥锁,夹紧访问缓冲区的操作。

生产者()
    while(1)
        生产产品;
        P(缓冲区有空位?若有,空位-1,若无则阻塞);
        P(互斥锁); //
        把产品放入缓冲区;
        V(互斥锁); //
        V(产品+1);
    


消费者()
    while(1)
        P(缓冲区有产品?若有,产品-1,若无则阻塞);
        P(互斥锁); //
        从缓冲区取出产品;
        V(互斥锁); //
        V(空位+1);
        消费产品;
    


Step 4. 检查是否出现死锁

没有连续的 P 操作,因此不会发生死锁。

Step 5. 定义信号量

把所有 PV 操作的中文全部换成英文即可。由于我们写的是中文,所以需要注意 PV 操作对应的信号量是什么,以及是否相同。

信号量的初始值一般为题目中给出的初始值。互斥信号量的初始值一般为 1。

  • 空位信号量:初始值应为 N。
  • 产品信号量:初始值应为 0。
  • 互斥信号量:初始值应为 1。
信号量 empty = N;
信号量 full = 0;
信号量 mutex = 1;

生产者()
    while(1)
        生产产品;
        P(empty);
        P(mutex);
        把产品放入缓冲区;
        V(mutex);
        V(full);
    


消费者()
    while(1)
        P(full);
        P(mutex);
        从缓冲区取出产品;
        V(mutex);
        V(empty);
        消费产品;
    


题目 2:双生产者、双消费者、单缓冲区、单次取出

【题目 2】桌子上有一个盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等吃盘子中的橘子,女儿专等吃盘子中的苹果。只有盘子为空时,爸爸或妈妈才可向盘子中放一个水果;仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出。

Step 1. 有几类进程

四类:爸爸、妈妈、儿子、女儿。

四类进程都是不断重复执行,因而需要加上循环体。

写出代码框架:

爸爸()
    while(1)
        
    


妈妈()
    while(1)
    
    


儿子()
    while(1)
        
    


女儿()
    while(1)
    
    


Step 2. 中文描述动作

爸爸()
    while(1)
        往盘子里放苹果:
    


妈妈()
    while(1)
        往盘子里放橘子;
    


儿子()
    while(1)
        从盘子取出橘子;
        吃橘子;
    


女儿()
    while(1)
        从盘子取出苹果;
        吃苹果;
    


Step 3. 添加 PV 操作,用中文描述里面的操作

(1)爸爸和妈妈进程

  • 爸爸进程的放苹果之前,需要先检查盘子是否有空位,如果有空位,那么空位 - 1,表明苹果占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要盘子空位 + 1?在女儿进程的从盘子取出苹果之后,苹果已被取出,盘子空位 + 1,因此需要一个 V 操作。
  • 妈妈进程的放橘子之前,需要先检查盘子是否有空位,如果有空位,那么空位 - 1,表明苹果占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要盘子空位 + 1?在儿子进程的从盘子取出橘子之后,橘子已被取出,盘子空位 + 1,因此需要一个 V 操作。
爸爸()
    while(1)
        P(盘子有空位?若有,空位-1,若无则阻塞); //
        往盘子里放苹果;
    


妈妈()
    while(1)
        P(盘子有空位?若有,空位-1,若无则阻塞); //
        往盘子里放橘子;
    


儿子()
    while(1)
        从盘子取出橘子;
        V(盘子空位+1); //
        吃橘子;
    


女儿()
    while(1)
        从盘子取出苹果;
        V(盘子空位+1); //
        吃苹果;
    


(2)儿子和女儿进程

  • 儿子进程的从盘子取出橘子之前,需要先检查盘子是否有橘子,如果有橘子,那么橘子 - 1,表明该进程已经取走了一个橘子;否则,若盘子里没东西,或者装的是苹果,则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要橘子 + 1?在妈妈进程的往盘子里放橘子之后,橘子 + 1,因此需要一个 V 操作。
  • 吃橘子前不需要 P 操作。
  • 女儿进程的从盘子取出苹果之前,需要先检查盘子是否有苹果,如果有苹果,那么苹果 - 1,表明该进程已经取走了一个苹果;否则,若盘子里没东西,或者装的是橘子,则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要苹果 + 1?在爸爸进程的往盘子里放苹果之后,苹果 + 1,因此需要一个 V 操作。
  • 吃苹果前不需要 P 操作。
爸爸()
    while(1)
        P(盘子有空位?若有,空位-1,若无则阻塞); 
        往盘子里放苹果;
        V(苹果+1); //
    


妈妈()
    while(1)
        P(盘子有空位?若有,空位-1,若无则阻塞);
        往盘子里放橘子;
        V(橘子+1); //
    


儿子()
    while(1)
        P(盘子是否有橘子?若有,则橘子-1,若无则堵塞); //
        从盘子取出橘子;
        V(盘子空位+1);
        吃橘子;
    


女儿()
    while(1)
        P(盘子是否有苹果?若有,则苹果-1,若无则堵塞); //
        从盘子取出苹果;
        V(盘子空位+1); 
        吃苹果;
    


(3)缓冲区

盘子是个缓冲区,由于缓冲区不能同时进行读写,所以需要一个互斥锁,夹紧访问缓冲区的操作。

爸爸()
    while(1)
        P(盘子有空位?若有,空位-1,若无则阻塞);
        P(互斥锁); //
        往盘子里放苹果;
        V(互斥锁); //
        V(苹果+1);
    


妈妈()
    while(1)
        P(盘子有空位?若有,空位-1,若无则阻塞);
        P(互斥锁); //
        往盘子里放橘子;
        V(互斥锁); //
        V(橘子+1);
    


儿子()
    while(1)
        P(盘子是否有橘子?若有,则橘子-1,若无则堵塞);
        P(互斥锁); //
        从盘子取出橘子;
        V(互斥锁); //
        V(盘子空位+1);
        吃橘子;
    


女儿()
    while(1)
        P(盘子是否有苹果?若有,则苹果-1,若无则堵塞);
        P(互斥锁); //
        从盘子取出苹果;
        V(互斥锁); //
        V(盘子空位+1);
        吃苹果;
    


Step 4. 检查是否出现死锁

没有连续的 P 操作,因此不会发生死锁。

Step 5. 定义信号量

把所有 PV 操作的中文全部换成英文即可。由于我们写的是中文,所以需要注意 PV 操作对应的信号量是什么,以及是否相同。

信号量的初始值一般为初始值,即各进程未开始运行时的初始值。互斥信号量的初始值一般为 1。

  • 盘子空位信号量:初始值应为 1。
  • 苹果信号量:初始值应为 0。
  • 橘子信号量:初始值应为 0。
  • 互斥信号量:初始值应为 1。
信号量 empty = 1;
信号量 apple = 0;
信号量 orange = 0;
信号量 mutex = 1;

爸爸()
    while(1)
        P(empty);
        P(mutex);
        往盘子里放苹果;
        V(mutex);
        V(apple);
    


妈妈()
    while(1)
        P(empty);
        P(mutex);
        往盘子里放橘子;
        V(mutex);
        V(orange);
    


儿子()
    while(1)
        P(orange);
        P(mutex);
        从盘子取出橘子;
        V(mutex);
        V(empty);
        吃橘子;
    


女儿()
    while(1)
        P(apple);
        P(mutex);
        从盘子取出苹果;
        V(mutex);
        V(empty);
        吃苹果;
    


然而,我们之前已经提及,当一般的信号量的初始值为 1 时,该信号量又可以充当互斥信号量。信号量 empty 恰好符合这种情况,因此上面答案中的互斥信号量 mutex 其实是多余的。不过多添加一个互斥信号量也没有错,不确定的时候还是写上吧,以防万一。

信号量 empty = 1;
信号量 apple = 0;
信号量 orange = 0;

爸爸()
    while(1)
        P(empty);
        往盘子里放苹果;
        V(apple);
    


妈妈()
    while(1)
        P(empty);
        往盘子里放橘子;
        V(orange);
    


儿子()
    while(1)
        P(orange);
        从盘子取出橘子;
        V(empty);
        吃橘子;
    


女儿()
    while(1)
        P(apple);
        从盘子取出苹果;
        V(empty);
        吃苹果;
    


题目 3:双生产者、单消费者、双缓冲区、单次取出

【题目 3】工厂有两个生产车间和一个装配车间,两个车间分别生产 A、B 两种零件,装配车间的任务是把 A、B 两种零件组装成产品。两个生产车间每生产一个零件都要分别把它们送到装配车间的货架 F1 和 F2 上,F1 存放 A,F2 存放 B,F1 和 F2 均只能容纳一个零件。每当能从货架上取到一个 A 和一个 B 后就可以组装成一件产品。整个过程是自动进行的,使用 P、V 操作进行管理,使各车间相互合作、协调工作。

Step 1. 有几类进程

三类:A 生产车间、B 生产车间、装配车间。

三类进程都是不断重复执行,因而需要加上循环体。

写出代码框架:

A生产车间()
    while(1)
        
    


B生产车间()
    while(1)
    
    


装配车间()
    while(1)
    
    


Step 2. 中文描述动作

A生产车间()
    while(1)
        生产零件A;
        把零件A放到货架F1上;
    


B生产车间()
    while(1)
        生产零件B;
        把零件B放到货架F2上;
    


装配车间()
    while(1)
        从货架F1上取出零件A;
        从货架F2上取出零件B;
        零件A和零件B组装产品;
    


Step 3. 添加 PV 操作,用中文描述里面的操作

(1)A 生产车间进程

  • 生产零件A之前不需要 P 操作。
  • 把零件A放到货架F1上之前,需要先检查货架 F1 是否有空位,如果有空位,那么空位 - 1,表明该零件占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在装配车间进程的从货架F1上取出零件A之后,零件已被取出,空位 + 1,因此需要一个 V 操作。
A生产车间()
    while(1)
        生产零件A;
        P(货架F1有空位?若有,空位-1,若无则阻塞); //
        把零件A放到货架F1上;
    


B生产车间()
    while(1)以上是关于操作系统-进程PV操作——生产者消费者问题的主要内容,如果未能解决你的问题,请参考以下文章

生产者与消费者

用C语言实现--生产者与消费者的问题(PV操作)

实验四生产者和消费者

实验四生产者和消费者

实验四

PV操作--demo test