操作系统基本特征
1.并发:操作系统调控多个进程轮流使用计算机资源,不同进程来回切换的速度特别快,看起来就像是在“同时"运行一样。
2.共享:操作系统使多个进程共享计算机资源,每个进程在运行的时候都认为自己获得了cpu,内存,I/O等资源。但实际上可能是不同进程在内存中有不同的独立的一块空间供其访问,看上去就像是不同进程共享资源一样。这种共享称为同时共享。不同进程之间除了有各自独立的空间,可能有彼此公共的内存空间来用于进程间的交流。为了使交流不出差错,一般操作系统规定,在一个进程访问公共空间直到访问结束为止,另一个进程不能访问此空间。这种共享称为互斥共享。
3.虚拟:从前面两个特征能看出,操作系统为每个进程或者用户分配计算机资源,这让每个进程或者用户感觉自己好像独立拥有一台计算机。这种把一台计算机虚拟成多台计算机的特性称为虚拟。
4.异步:操作系统使程序并发运行,不同程序轮流使用cpu,进程A运行一会,进程B运行一会,进程C运行一会,进程A再运行一会……这样每个进程在实际是“走走停停”,每次“走”多久,“停”多久都是由操作系统决定的。所以即使是同一个进程,同样的运行环境,从“微观”角度上看,它的运行过程可能每次都不一样。但是从"宏观"角度,即运行结果每次都是一样的。
多道程序设计和分时系统
多道程序设计是为了提高cpu的利用率而出现的,他在进程需要等待某个任务时(例如I/O)会切换到内存中的另一个进程,等到第一个程序完成等待再切换回来。而分时系统会主动在各个程序之间切换,不会等到进程运行结束在换到下一个进程。
举个例子:进程A是聊天,进程B是处理文件,进程C是大数据计算。在多道程序设计会在A等待聊天输入时切换到B进程处理文件,等到A开始输入时,再切换回来运行A。而分时系统会在A等待聊天输入时,切换到进程B,运行一会再切换到C,并在BC间来回切换运行,直到A开始输入时,再切换到A。
简单来说,多道程序设计是被动在进程之间切换,而分时系统不仅会被动切换还会主动切换进程。
进程,程序,线程相关问题
进程的组成:
- 程序代码
- 程序数据
- 程序计数器
- 寄存器,堆,栈
- 系统资源(内存,文件,I/O等)
注:CPU资源不属于系统资源,进程当然会占用CPU资源,但是人们更关心进程所占用的系统资源,所以没有把CPU资源写入进程的组成。
进程与程序的区别
- 进程是动态的,程序是静态的。
- 进程是暂时的,程序是永久的。
- 进程与程序的组不同(比如进程含有输入的数据)
进程的特点
- 动态性
- 并发性
- 独立性
- 制约性
进程控制块(PCB)
程序 = 算法 + 数据结构
操作系统也可以看作程序,程序控制块就是操作系统的数据结构,主要表示进程的状态,使进程成为一个能独立运行的基本单位。
一个PCB唯一标识一个进程,所以PCB含有一个进程的信息标识。PCB还存储着CPU运行的状态信息。PCB还存储着进程的控制信息,这个信息是操作系统用于控制,调控进程以及进程之间运作,协调,交流而使用的信息。
所有进程可以按其进程状态(就绪,阻塞)将其PCB组织起来。组织方式有链表和索引表。链表:桶一状态的进程连接在同一个链表上,用链表的好处是面对频繁的进程创建和删除,链表操作起来更方便。索引表:同一状态的进程在同一个索引表中,索引就是数组,在进程创建和删除不那么频繁的时候,用索引表不失为 一种高效的方法。
![](/qrcode.jpg)
PID(process ID):进程在创建的时候由操作系统分配其一个ID,这个ID在进程结束之前是不变的。但是同样的程序下次创建进程时操作系统会重新分配PID。在同一时刻,PID和进程是一一对应的关系。
PPID(parent PID):是进程的父进程的PID。
进程的状态
进程主要有 3 种状态,运行,就绪,阻塞。运行态就是可以转为阻塞或者就绪,就绪可以转为运行,阻塞可以转为就绪。 除了这三种,还有一种“挂起”状态,挂起是在某个进程急需运行时而内存中的进程已经满了,这时就要将内存中不急着运行的阻塞的进程转到外存,将急着运行的进程放入内存。除了可以阻塞到挂起, 也可以就绪到挂起(很少出现)。运行可以转变为就绪挂起,就绪可以转为就绪挂起,阻塞可以转为阻塞挂起,阻塞挂起可以转为就绪挂起。
为什么有线程
线程产生的更根本原因就是效率。单单使用进程在某些方面效率低下,甚至无法完成需求。设想一个播放视频的程序,如果用单一的进程实现大致是下面这样。
main() {
while (1) {
read(); //读取视频文件
decoding(); //视频解码
play(); //播放
}
}
如果一个视频播放程序,每次播放前都是将一个视频全部解码之后再放入缓存中开始播放,那用户就得在播放前等上一阵子,跟何况如果视频太大,缓存可能放不下。所以一般是一点一点的读取-解码-播放。那么问题来了,每次播放完了一段视频之后,必须等一会read()和decoding()再播放,视频播放出来的效果是断断续续的。所以人们就想办法让这三个功能同步执行,读取文件的功能会读完文件之后立马去读下一个文件,而不是等上一次播放结束后才开始读下一个文件;同样的,视频解码也是马不停蹄地在解码。这样才能实现流畅的播放。而这分开的三个功能就是三个线程。
什么是线程
回到上面的例子,想一下,read 所读取的文件需要提供给 decoding ,decoding 解码的数据要提供给 play;再者,读取文件的速度是很慢的,这就需要多创建几个线程同时读,这几个read线程就需要访问进程为其打开的同一个视频文件。可以看出,线程之间的资源共享就显得很重要。事实也是如此,一个进程的多个线程是共享这个进程的资源的。这些资源有代码,数据,内存,文件等等。
由以上讨论可以看出,线程是功能执行单元,而进程更像是一个资源管理者。
类似于进程的PCB,线程也有自己的TCB(thread control process),但是由于线程间共享进程的资源,所以TCB中主要内容是进程的状态信息。
线程的优缺点
优点就是高效
1.线程灵活,创建线程和删除线程的系统开销相比于进程来说要小很多。
2.线程之间能共享资源。
3.线程之间切换速度快于进程。
缺点就是容错率低
正是因为资共享,如果一个线程出错,导致共享的资源出错,那么就会有可能引起别的线程出错,甚至整个进程崩溃。
线程和进程的比较
1.进程是资源分配的基本单位,线程是CPU调度的基本单位。
2.各个进程独享完整的资源平台;而各个线程独享必要的资源,寄存器,栈等。注意!虽然线程独享一些资源,但是这些资源也是能被其他线程直接访问的,且无法被阻止。
3.进程和线程同样有三态。
4.线程的空间和时间消耗少;线程创建和结束都很快,因为线程所使用的资源是交给进程管理了。线程之间的切换很快,应为共享资源,再切换的时候不需要切换“页表”。线程之间通信不需要经过内核,因为资源共享。
PS:线程进程各有优劣,在具体使用时,要根据实际情况来选择使用。
线程的实现
-
用户线程:操作系统 “看不见“ 的线程。这种线程用专门的 ”用户线程库“ 实现对线程的管理。操作系统只能看见进程的相关信息,看不见进程里的线程信息。总的来说,线程的调度和管理由库来管理,操作系统不直接参与。
优点:第一,线程切换是由线程库来完成的,无需用户态/内核态的切换,速度非常快。第二,由于线程是由线程库实现的,所以可以在不支持线程技术的操作系统中。第三,每个线程的TCB由线程库来维护。第四,不同进程可以由不同线程调度算法。
缺点:第一,一个线程的阻塞会造成整个进程被阻塞。第二,一个线程除非主动交出CPU使用权,不然进程中的其他线程无法运行,因为线程库没有打断线程运行的特权(操作系统有,通过时钟中断实现)。第三,由于操作系统分配时间片给单个进程,所以每个线程分得的时间相比于内核线程会较少,执行较慢。 -
内核线程:操作系统 “看得见” 的线程。线程控制块放在内核中,线程的控制和调度都是由内核实现的。
优点:第一,一个线程的阻塞不会影响其他内核线程的运行。第二,时间片是分配给线程的,多线程进程会获得更多的CPU。
缺点:进程的创建,终止,切换都是由系统调用实现的,开销很大。
内核线程和用户线程之间有三种对应关系
- 多个用户线程对应一个内核线程
- 一个用户线程对饮过一个内核线程
- 多个用户线程对应多个内核线程
轻量级进程
由内核支持的用户线程,一个进程可有一个或多个轻量级进程,每个量级进程由一个单独的内核线程来支持。
管理起来复杂,但是更灵活。
上下文切换
在进程切换时需要进行上下文切换,上下文切换的目的是为了使进程再次运行时能够恢复到上次运行的状态。上下文切换必须快速,因为整个操作非常频繁。
上下文有哪些?
- 寄存器(PC,SP,……),CPU状态,……
上下文切换如何实现?
因为需要速度块,所以是由汇编实现的 。
操作系统为活跃的进程准备了进程控制块,并且将PCB放在一个合适的队列里,方便进程之间的切换。
- 就绪队列
- 等待I/O队列
- 僵尸队列
进程的创建,加载,等待,终止
创建 :新进程的创建是由已存在的进程通过fork()
函数创建的,这个已存在的进程称为新进程的父进程。fork()
函数将父进程的PCB复制到内存中(除了PID不复制),做为子进程的PCB。
加载 :子进程被创建之后,需要将自己需要的资源加载到自己的PCB中,这时子进程调用exec()
函数来加载自己需要的资源。
等待 :父进程有个wait()
函数等待子进程结束并将其PCB回收,因为子进程难以自己回收自己,所以需要其他进程的帮助。如果子进程结束之前父进程就已经结束了,那么子进程会进入僵尸队列等待被回收,此时init进程会定时扫描进程控制块,检查有无处于僵尸状态的进程并回收。
终止 :进程结束时,调用exit()
函数终止进入僵尸状态,等待父进程来回收。
进程间通信
进程之间可能会有合作的关系,合作关系的进程可能就需要使用一段相同的地址空间或者共享一部分相同的磁盘空间,那么在两个进程对这些共享资源操作的过程中就有可能出现不协调而出错的可能。
打个比方:有两个计算机科学家(A,B)住在一起,A早上起来发现冰箱没面包了,于是出门买面包。A刚出门,B也起床了,B发现冰箱里没有面包了,于是B也出门买面包。最后的结果是A和B都买了面包。这肯定不是AB希望的结果。
计算机的进程之间的合作也会出现类似的情况。这种情况的出现称为竞争条件的出现。
竞争条件:两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序。(全文背诵!)
那么如何避免这种情况的发生呢?
先分析一下出现这种情况的原因。
- A和B都可以对冰箱进行 ”检查冰箱—决定是否买面包“ 的操作,所以才有可能出现两人都买的情况。
- A出门买面包到回来这段时间内,冰箱仍然是空的且B并不知道A是否去买面包了
解法0
看第一个原因。那可否让A和B中只有一个人拥有 ”检查冰箱—决定是否买面包“ 的能力呢?这样就不会出现买多了的情况。想一想,显然是不可以的!A和B是合作关系,如果这个活全部都交给A或者B了,那么还谈何合作?况且如果A没有起床,B就有可能要饿一上午!
解法1
看第二个原因。只要A在买之前,在冰箱上贴一个标签(锁)告诉B自己去买面包了,B就不会去买了。那这个方法行不行呢?看上去好像是没有问题的,但是虽然现实中没问题,但是放在计算机里就不一定了!!
现在回到计算机中,我们用程序来描述一下A和B的行为。
//计算机科学家A
if (冰箱上没有B的标签) {
if (冰箱是空的) {
贴上标签A;
去买面包;
撕标签;
}
}
//计算机科学家B
if (冰箱上没有A的标签) {
if (冰箱是空的) {
贴上标签B;
去买面包;
撕标签;
}
}
我们知道进程是并发执行的,如果A岗判断完冰箱是空的但是还没贴标签,操作系统调度B开始运行,这时由于A还没贴标签,B会去买面包。此时已经出错了!A是先去检查冰箱的,但是B仍然去买了面包,而且继续观察程序,会发现A也会去买面包! 所以贴标签并不能解决这个问题。原因是,检查冰箱和贴标签这两个动作不是原子性的,可能会被打断。
解法2
那想办法让这两个操作结合起来变为原子性吗?这样当然是最好的,但是暂时合不起来。只能想想别的合作机制。之前的方法会发现,冰箱没有标签的状态有可能会被AB同时发现,进而导致后面的问题。如果冰箱的标签状态只有两个,状态0表示A可以去买,B不可以买;状态1表示B可以买,A不可以。那么就不会出现A,B发现同一状态而做同样的事情的可能了。这么说有点奇怪,看一下程序就明白了
//计算机科学家A
while (冰箱状态为1)
等待;
if (冰箱为空) {
买面包;
冰箱状态改为1;
}
//计算机科学家B
while (冰箱状态为0)
等待;
if (冰箱为空) {
买面包;
冰箱状态改为0;
}
以上面这种严格轮流工作的机制,就一定不会出现AB同时去买面包的情况了。但是这个方式合适吗?仔细想想能发现,这个方法有个很不好的地方,如果A买了面包之后将状态改为1,当A下次再去买面包时,就必须得等B买一次面包回来把状态改为0,不然A会永远的等待。这种等待是占用CPU资源的,称为忙等待
讨论到这里发现,多买面包的问题总是得不到完美的解决,每次看似解决了一个问题,马上另外一个问题就冒出来了。所以我们得制定几项原则,可行的解决方案应该要满足以下这几项原则。
- 进程A,B不会同时进入临界区
- 不对CPU速度和数量做任何假设
- 临界区外运行的进程不得阻塞其他进程
- 不得使进程无限制的等待进入临界区
简单解释一下,我们把对共享内存进行访问的程序片段称为临界区,上面的例子里检查冰箱并买面包的程序片段就是临界区。不对CPU速度和数量做任何假设,我的理解就是,我们寻找的解决方法是在软件层面的,那么我们的方法应该是对任何硬件平台适用的,所以不对CPU速度和数量做任何假设。
有了以上的原则约束再来寻找解决办法
解法3:屏蔽中断
在程序进入临界区之前就屏蔽所有的中断信号,这样就不会在运行的时候,因为系统调用而被打断。虽然这个方法确实能解决问题,而且也满足上面的四项原则,但是这种方法还是不被接受的。如果临界区的操作很少,那么这么做几乎不会对系统造成太大影响,但是临界区操作很多的话,那在操作过程中,整个计算机就失去了人机交互功能,一切外界的中断(鼠标,键盘,磁盘)都不会被系统接受,甚至操作系统都被终止。
解法4:Peterson解法
这是一个可以接受的解法!且是一个好方法!
Peterson解法(下文简称P法)是上文所讨论的解法2的升级版本。解法二中因为严格的轮换工作制度,导致当一个进程不想工作之后,另一个进程就一直需要等待。那么很容易想到的解决方法就是让每个进程公开自己想不想工作。这样就不会因为一个进程不想工作而导致另一个进程无法工作。程序的实现就是加一个数组来记录每个人是否想工作。
int turn; //用turn表示冰箱状态,0代表A工作,1代表B工作
int want[2]; //记录AB是否想工作,值为0是不想,值为1是想
//计算机科学家A
{
want[0] = 1; //记录自己想工作
turn = 0; //A进入工作状态
while(turn == 1 && want[1] == 1); //如果B想工作且B进入了工作状态,那就等B工作
临界区;
want[0] = 0; //A结束了临界区的工作,并记录自己不想工作。
}
计算机科学家B的程序也是类似的。
P法满足了四项原则,至于证明的话用反证法,模拟一下程序的执行过程就能证明。而且P法没有什么缺点,唯一美中不足的就是,P法仍然有忙等待,这是一种对cpu资源的浪费。
以上的解法都是在软件层面,下面介绍一种硬件方面的解决方案。
解法5 :TSL和XCHG
TSL RX , LOCK (TSL是指令名,RX和LOCK是指令参数)
在一些计算机中,特别是设计为多处理器的计算机都有TSL指令:测试并枷锁(test and set lock)。该操作一共有两个步骤
- 将内存字lock读到寄存器RX中
- 在该内存地址上存一个非0值
这两个二步骤是不可分割的原子操作,在两个步骤执行完成之前CPU不会被调度执行别的进程。而且执行该指令的CPU会锁住内存总线,禁止其他CPU访问该内存字。
具体使用方法如下,还是用上文中的例子
//计算机科学家A
start:
TSL(rx, lock); //把锁的值赋给rx, 并将锁的值设为非1
if (rx == 0) { //检查锁的值是不是0
买面包; //如果是0,则可以买面包
lock = 0; //临界区操作结束后,将锁的值设为0
} else {
goto start; //如果不是0,则循环
}
TSL本身是汇编语言,整个程序应该也写成汇编,写成C语言的形式是为了方便理解,逻辑上两者都是差不多的。
还有一种指令是XCHG RX,LOCK。同样是一个原子操作,但和TSL不同,XCHG直接交换rx和lock的值,使用方法如下
//计算机科学家A
start:
rx = 1; //将rx的值设为1
XCHG(rx, lock); //交换rx和lock的值,现在lock值为1
if (rx == 0) { //检查锁的值是不是0
买面包; //如果是0,则可以买面包
lock = 0; //临界区操作结束后,将锁的值设为0
} else {
goto start; //如果不是0,则循环
}
不论是XCHG还是TSL(硬件层)还是peterson解法(软件层),他们都是正确的,但是都有忙等待的缺点,这不仅浪费了CPU时间,甚至有可能造成预想不到的后果,比如优先级反转问题。为了找寻更好的办法,计算机科学家Dijkstra发明了信号量
信号量(semaphore)
在讨论信号量之前,想一想解决忙等待的方法有啥?容易想到用阻塞来代替忙等待。《现代操作系统》一书上有“生产者消费者问题”,百度上也能搜到。这个问题就是使用阻塞代替忙等,但是它的阻塞和阻塞唤醒机制有些问题,信号量的出现恰好解决了这个问题。
信号量是Dijkstra在1965年提出的一种方法,它使用一个整型变量来累计唤醒次数,供以后使用。在他的建议中引入了一个新的变量类型,称作信号量(semaphore)。一个信号量的取值可以为0(表示没有保存下来的唤醒操作)或者正值(表示有一个或多个唤醒操作)。对于信号量的操作有down和up(原名是P和V,写成down和up便于理解)。down操作是一个进程在需要某种资源(一般用信号量表示资源的状态)时进行的要进行的操作。down会检查某一个信号量的值,如果值大于0,则信号量减一,该进程继续;如果信号量等于0,该进程睡眠(此时down操作并未结束,因为还没有完成对信号量的减一操作)。up操作对某信号量的值加一,如果有一个或多个进程因为down操作睡眠在此信号量上,则up唤醒其中一个(具体唤醒哪一个由系统决定)来完成自己未完成的down操作。
down和up操作都属于原子操作,不会被中断,如果有多个CPU,则每个信号量需要有一个锁变量进行保护,通过TSL和XCHG指令来确保同一时刻只有一个CPU在对信号量操作。
下文的内容基于 “生产者和消费者问题”
生产者和消费者之间需要进行互斥,生产者和生产者之间需要互斥,消费者和消费者之间需要互斥。这时只需要一个信号量就可以实现:
typedef int semaphore;
semaphore mutex = 1; //用于互斥的信号量,值为0或1
void producer {
int item;
while (1) {
item = produce_item();
down(mutex); //检查mutex是否为0,为0则睡眠,为1,则减一并继续
insert_item(item); //生产操作
up(mutex); //使mutex加一,如果有在mutex上睡眠的进程,则换醒
}
}
void consumer {
int item;
while (1) {
down(mutex); //检查mutex是否为0,为0则睡眠,为1,则减一并继续
item = remove_item(); //消费操作1(在临界区)
up(mutex); //使mutex加一,如果有在mutex上睡眠的进程,则换醒
consume_item(item); //消费操作2(不在临界区)
}
}
生产者和消费者除了存在互斥问题,还有一个重要的问题是同步问题。什么是同步,同步就是线程之间的运行存在一定程度的先后顺序。比如在生产者和消费者问题中,缓冲区如果满了,生产者就必须等消费者从缓冲区中取出一个item之后才能再度运行;同样的如果缓冲区空了,消费者就必须等待生产者向缓冲区中加入一个item之后才能运行。所以在这个问题中有两个需要同步的地方。那么需要两个信号量就可以实现同步:
#define N 100; //缓冲区最大容量为100
typedef int semaphore;
semaphore mutex = 1; //用于互斥的信号量,值为0或1
semaphore full = 0; //用于同步的信号量,代表缓冲区中item数量
semaphore empty = N; //用于同步的信号量,代表缓冲区中空位数量
void producer {
int item;
while (1) {
item = produce_item();
down(&empty); //检查缓冲区中是否有空位,没有则睡眠,有则减一并继续
down(&mutex); //检查mutex是否为0,为0则睡眠,为1,则减一并继续
insert_item(item); //生产操作
up(&mutex); //使mutex加一,如果有在mutex上睡眠的进程,则换醒
up(&full); //使缓冲区中item数加一,如果有在full上睡眠的进程,则换醒
}
}
void consumer {
int item;
while (1) {
down(&full); //检查缓冲区中是否有item,没有则睡眠,有则减一并继续
down(&mutex); //检查mutex是否为0,为0则睡眠,为1,则减一并继续
item = remove_item(); //消费操作1(在临界区)
up(&mutex); //使mutex加一,如果有在mutex上睡眠的进程,则换醒
up(&empty); //使缓冲区中空位数加一,如果有在empty上睡眠的进程,则换醒
consume_item(item); //消费操作2(不在临界区)
}
}
同步的问题也解决了,信号量既能解决互斥也能解决同步!!!
互斥量
互斥量可以看作是信号量的简化版本,比如上文中的mutex就类似于互斥量。如果不需要信号量的计数能力,就可以用互斥量。互斥量实现时既容易又有效,这使得互斥量在实现用户空间线程包时非常有用。
《现代操作系统》p75有互斥量在用户线程包中的应用以及优点,讲的很清楚。
管程
有了信号量和互斥量基本可以说解决问题了,但是这样真的结束了吗?显然没有,信号量的使用是比较复杂的,上文中的代码虽然看起来不难,但是轮到你自己写代码时,你就会发现各种问题。比如上文中的生产者和消费者在进入临界区之前都要进行两个 down 操作,第一个down用于同步,第二个用于互斥,如果这两个操作顺序交换一下,会发生什么结果呢?比方说生产者,假设它先进行down(&mutex),在down(&empty)时它睡眠了,这时它还没有释放mutex,这就使得没有任何进程能进入临界区,没有进程能执行up操作唤醒它,那这个生产者永远睡在了那里。一个简单的例子可以发现信号量的使用必须非常谨慎。
所以为了更易于编写出正确的程序,Brinch Hansen和Hoare提出了管程。在后文能发现,他们两提出的管程略有不同。
首先,何为管程?管程是由局部于自己的若干公共变量及其说明和所有访问这些公共变量的过程所组成的软件模块。 简单的说管程是一种对于同步操作更高级的封装。传统同步方法需要独立管理,管程可以对他们进行统一方式的管理。
管程有一个关键特性,任意时刻管程中只能有一个活跃进程,这使得管程能有效地完成互斥。这个特性是由编译器负责的。
光是互斥还不够,管程还需要实现同步,所以解决的方法是引入条件变量,以及两个相关的操作:wait和signal。当一个管程过程发现它无法继续运行时(例如消费者发现缓冲区为空),它会在某个条件变量上执行wait(如empty)操作。该操作使得进程自身阻塞,并且将另外一个之前等待在管程之外的进程(不是被阻塞的进程可能是就绪的进程)调入管程。signal就比较简单了,比方说生产者,可以唤醒正在阻塞中的消费者,只要通过对消费者阻塞在的条件变量(empty)执行signal操作就ok。wait和signal的实现大致如下:
Class condition{ //condition就是条件变量
int numwaiting = 0; //用来记录睡眠中的进程数量
waitqueue q; //如果有多个进程睡眠了,就会进入睡眠队列中
}
condition::Wait(lock) {
numwaiting++;
add this thread to q;
release(lock); //进程释放锁,不然下面调入的新进程就不能进入管程了
schedule(); //调入新进程, 原进程此时被阻塞了
require(lock); //调入进程结束了,原进程要再次获得锁
}
condition::Signal() {
if (numwaiting > 0) {
remove a thread from q;
wakeup(); //唤醒一个被阻塞的进程
numwaiting--;
}
}
现在有一个问题,signal唤醒进程之后,如果不做任何事情,就会有两个进程在管程之中,这与管程的特性不符。对于这个问题Hansen和Hoare提出了不同的方案。Hansen建议在signal之后原进程立马退出管程。Hoare建议signal之后,原进程挂起,等唤醒的进程结束返回之后,再继续运行原进程。一般采用Hansen的方法,因为概念上更简单,且容易实现。
至于管程的在生产者于消费者问题中的运用,清华大学陈渝老师讲的很好
清华大学->操作系统->管程