1 管程
1.1 管程的定义
管程(Monitor)是关于共享资源的数据结构及在其上操作的一组过程组成。进程只能通过调用管程中的过程来间接的访问管程中的数据结构。
1.2 管程需要解决的两个基本问题
1.2.1 互斥
管程是互斥进入的,有一个进程调用管程时,其他进程将不能再调用管程,这么设计主要是为了保证数据完整性。管程的互斥是由编译器保证的。
1.2.2 同步
管程中设置条件变量及等待/唤醒操作以解决同步问题。当一个进程或线程在条件变量上等待时应先释放管程的使用权,也可以通过发送信号将等待在条件变量上的进程或线程唤醒。
1.3 应用管程遇到的问题
我们已经知道,进程可以将等待在条件变量上的其他进程唤醒,假如P进程进入了管程并唤醒了Q进程,如果P和Q进程同时对管程中的数据操作,那么就会破坏管程中数据的完整性,此时应该如何避免这个问题呢?有三种解决方案:
- P进程等待Q(Hoare管程)
- Q进程等待P进程继续执行(MESA管程)
- 规定唤醒操作是管程中最后一个可执行操作。就是说当任何一个进程在管程中唤醒了别的进程,那么该进程必须退出管程。
2 Hoare管程
2.1 Hoare管程的核心思想
Hoare管程的可以用以下示意图说明:
如上图所示,当某个进程P从入口等待队列进入到Hoare管程中,对资源进行各种操作(此时其他所有管程只能在管程外等待),当执行某一过程n时发现条件cn不足,无法继续执行,此时进程P通过wait操作等在条件变量cn上,此时进程P放弃管程的互斥权使用,门打开让其它进程进入。假设Q进程进入,在进行若干操作之后,条件变量cn满足了,此时Q进程通过signal操作唤醒进程P,而根据Hoare管程的语义,应该由P进程执行,而Q进程等待。Hoare管程为这类因唤醒其他进程而进入等待状态的所有进程在管程内开辟了一个专门的紧急等待队列。
以上是对管程的工作流程做了个详细的介绍,接着对管程补充两点说明:
- 管程是互斥进入的,所以当一个进程试图进入一个已经被占用的管程时,应当在管程的入口等待。为此,管程的入口处设置了一个入口等待队列。
- 如果进程P唤醒了进程Q,则进程P等待进程Q执行;而进程Q又唤醒了进程R,则进程Q等待进程R执行;并且进程P、Q进入紧急等待队列,以此类推。所以管程内部可能出现多个等待进程,但任意时刻最多只能有一个进程对管程进行操作。
紧急等待队列是在管程内部开辟的一个进程等待队列,其优先级高于入口等待队列。
2.2 Hoare管程条件变量的实现
条件变量实际上是在管程内部说明和使用的一种特殊类型的变量。对于条件变量c可以执行wait和signal操作。
表2-1 条件变量的实现机制
操作名 | 实现机制 |
---|---|
wait(c) | 如果紧急等待队列非空,则唤醒第一个等待者;否则释放管程的互斥权,执行此操作的进程进入c链末尾 |
signal(c) | 如果c链为空,则相当于空操作,执行此操作的进程继续执行;否则唤醒第一个等待者,执行此操作的进程进入紧急等待队列的末尾 |
2.3 Hoare管程的应用
本小节主要针对利用管程解决生产者、消费者问题,逻辑和利用信号量的PV操作是一样的,具体逻辑如程序清单2-1的伪代码。
程序清单2-1 Hoare管程对于生产者消费者问题的解决
monitor ProducerConsumer{
/* 定义两个条件变量,full表示缓冲区满了的时候生产者停止生产,empty表示缓冲区空了的时候消费者停止消费 */
condition full,empty;
/* 非空缓冲区的数量 */
integer count;
procedure insert(item :integer);
begin
/* 当缓冲区满了时,生产者进程中止执行,进入条件变量full */
if(count==N) then wait(full);
insert_item(item);count++;
/* 当缓冲区数据量为1时,唤醒等待在条件变量empty上的消费者进程 */
it(count==1) then signal(empty);
end;
function remove:integer;
begin
/* 当缓冲区空了时,消费者进程中止执行,进入条件变量empty */
if(count==0) then wait(empty);
remove=remove_item;count--;
/* 当缓冲区数据量为N-1时,唤醒等待在条件变量full上的生产者进程 */
if(count==N-1) then signal(full);
end;
end monitor;
}
//生产者进程
procedure producer;
begin
while(TRUE){
item=produce_item;
ProducerConsumer.insert(item);
}
end;
//消费者进程
procedure consumer;
begin
while(TRUE){
item=ProducerConsumer.remove;
consume_item(item);
}
end;
关于利用信号量解决生产者消费者问题的思想可参考博客:
【操作系统】同步互斥机制(一):同步互斥机制的介绍及信号量的深入理解
3 MESA管程
本篇第2节重点介绍了Hoare管程,Hoare的思想在于,当P进程唤醒Q进程,P进程切换下CPU由Q进程获取CPU的执行权,之后当条件变量满足之后P进程再次切换到CPU,从这过程可以看出实际上Hoare管程的多了两次额外的进程切换。为了解决这个问题进而引出了MESA管程。
3.1 MESA管程的机制——wait/notify
Hoare管程是基于wait/signal机制,signal唤醒操作特点是唤醒了某一进程并且立刻让出CPU的,而MESA管程是基于wait/notify机制的。所谓的notify,是指当一个正在管程中的进程执行notify©时,它使得c条件队列得到通知,然后发出信号的进程继续执行。关于notify有三点需要注意:
- notify的结果是实现了位于条件队列头的进程在将来合适且当CPU可用时恢复执行(区别于Hoare管程,不是立即获取CPU执行)。
- 但是由于不能保证在该进程之前没有其它进程进入过管程并修改了数据资源,因而这个进程在获取CPU执行前每次都要重新检查条件。所以MESA管程用while循环取代了Hoare管程中的if语句。
- 用while循环去判断带来的影响是:会导致对条件变量至少多一次额外的检测。但不再出现额外的进程切换的问题,并且对等待进程在notify之后何时运行没有任何限制。
- 改进:由于该进程每次被调度时都会由while语句执行相关条件检查,所以在实现时可以为每个条件原语关联一个监视计时器,不论是否被通知,超过一定等待时间的进程将会被直接设置为就绪态(即使此时条件变量没有满足依然不会报错,因为while语句能保证收到这些收到信号的程序检查相关的变量,如果期望的条件没有满足则会继续等待)。
程序清单3-1 MESA管程对于生产者消费者问题的解决
monitor ProducerConsumer{
/* 定义两个条件变量,full表示缓冲区满了的时候生产者停止生产,empty表示缓冲区空了的时候消费者停止消费 */
condition full,empty;
/* 非空缓冲区的数量 */
integer count;
procedure insert(item :integer);
begin
/* 当缓冲区满了时,生产者进程中止执行,进入条件变量full */
while(count==N) then wait(full);
insert_item(item);count++;
/* 当缓冲区数据量为1时,唤醒等待在条件变量empty上的消费者进程 */
cnotify(empty);
end;
function remove:integer;
begin
/* 当缓冲区空了时,消费者进程中止执行,进入条件变量empty */
while(count==0) then wait(empty);
remove=remove_item;count--;
/* 当缓冲区数据量为N-1时,唤醒等待在条件变量full上的生产者进程 */
cnotify(full);
end;
end monitor;
}
3.3 MESA管程的机制——wait/broadcast
notify是每次只通知一个进程,MESA管程中还实现了每次通知多个进程的机制,即broadcast。
broadcast:使所有在该条件上等待的进程都被释放并进入队列。
broadcast使用场景:
- 当一个进程不知道有多少进程将被激活时,这种方式是非常方便的。例如:在生产者/消费者问题中,假设insert和remove函数都适用于可变长度的字符快,此时,如果一个生产者往缓冲区添加了一批字符,它不需要知道每个正在等待的消费者准备消耗多少字符,而broadcast机制可以使得所有正在等待的进程都得到通知并再次尝试运行。
- 当一个进程不难以准确判定将激活哪个进程时,也可以使用广播。
4 进程间通信机制IPC
在上一篇和本篇前几节已经分别介绍了信号量和管程的机制,但是这两种通信方式只能传递很简单的信息,不支持大量信息的传递,另外管程不适合用户多处理器的情况,所以需要引入新的通信机制。
4.1 进程间通信方式
4.1.1 消息传递
消息传递的核心思想如下图所示:
程序清单4-1 用PV操作实现send原语
/* 定义如下四个信号量 */
// emptyBuff:空闲的消息缓冲区,初值为N
// fullBuff:填满的消息缓冲区,初值为0
// mutex1:初值为1
// mutex2:初值为1
send(destination,msg){
// 根据destination找出接收进程;如果没找到直接报错并返回
申请空缓冲区P(emptyBuff);
P(mutex1);
取空缓冲区;
V(mutex1);
把消息从msg处复制到空缓冲区;
P(mutex2);
把消息缓冲区挂到接收进程的消息队列;
V(mutex2);
V(fullBuff);
}
4.1.2 共享内存
如下图所示,两个进程1、2都有各自的内存地址空间,在物理内存空间有一块共享内存,两个进程中都有一块内存空间映射到了同一块物理内存(即共享内存)。共享内存
也包含了一个读者-写者问题:不能多个进程同时写入,但支持同时读取。假设进程1想往共享内存写数据,只需要往本地映射的内存空间写入数据,相当于是写入了共享内存。
4.1.3 管道通信方式PIPE
利用一个缓冲传输介质——内存或文件连接两个相互通信的进程。示意图如下:
管道通信方式需要注意以下三点:
- 是以字符流的方式写入
- 保证了先进先出的顺序
- 管道通信机制必须提供的协调能力:*互斥、同步、判断对方进程是否存在
除了这三种通信方式外,还有套接字、远程过程调用等。
4.2 Linux的进程通信机制
4.2.1 原子操作
不可分割,只执行完之前不会被其他任务或事件中断;
常用于实现资源的引用计数
4.2.2 屏障(Barrier)
1 概念
一种同步机制(又称栅栏、关卡);
用于对一组线程进行协调
2 应用场景
一组线程协同完成一项任务,需要所有线程都到达一个汇合点后再一起向前推进。
实际上Linux中的通信机制还包括:自旋锁、读写自旋锁、信号量、读写信号量、互斥体、完成变量、顺序锁等等。
5 小结
本篇重点介绍了管程的概念及同步互斥机制,重点介绍了Hoare管程和MESA管程;介绍了进程间通信机制——消息传递、共享内存和管道,并介绍了Linux系统种的两种IPC机制。