管程(Monitor)是关于共享资源的数据结构及在其上操作的一组过程组成。进程只能通过调用管程中的过程来间接的访问管程中的数据结构。
管程是互斥进入的,有一个进程调用管程时,其他进程将不能再调用管程,这么设计主要是为了保证数据完整性。管程的互斥是由编译器保证的。
管程中设置条件变量及等待/唤醒操作以解决同步问题。当一个进程或线程在条件变量上等待时应先释放管程的使用权,也可以通过发送信号将等待在条件变量上的进程或线程唤醒。
我们已经知道,进程可以将等待在条件变量上的其他进程唤醒,假如P进程进入了管程并唤醒了Q进程,如果P和Q进程同时对管程中的数据操作,那么就会破坏管程中数据的完整性,此时应该如何避免这个问题呢?有三种解决方案:
Hoare管程的可以用以下示意图说明:
如上图所示,当某个进程P从入口等待队列进入到Hoare管程中,对资源进行各种操作(此时其他所有管程只能在管程外等待),当执行某一过程n时发现条件cn不足,无法继续执行,此时进程P通过wait操作等在条件变量cn上,此时进程P放弃管程的互斥权使用,门打开让其它进程进入。假设Q进程进入,在进行若干操作之后,条件变量cn满足了,此时Q进程通过signal操作唤醒进程P,而根据Hoare管程的语义,应该由P进程执行,而Q进程等待。Hoare管程为这类因唤醒其他进程而进入等待状态的所有进程在管程内开辟了一个专门的紧急等待队列。
以上是对管程的工作流程做了个详细的介绍,接着对管程补充两点说明:
紧急等待队列是在管程内部开辟的一个进程等待队列,其优先级高于入口等待队列。
条件变量实际上是在管程内部说明和使用的一种特殊类型的变量。对于条件变量c可以执行wait和signal操作。
表2-1 条件变量的实现机制
操作名 | 实现机制 |
---|---|
wait(c) | 如果紧急等待队列非空,则唤醒第一个等待者;否则释放管程的互斥权,执行此操作的进程进入c链末尾 |
signal(c) | 如果c链为空,则相当于空操作,执行此操作的进程继续执行;否则唤醒第一个等待者,执行此操作的进程进入紧急等待队列的末尾 |
本小节主要针对利用管程解决生产者、消费者问题,逻辑和利用信号量的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;
关于利用信号量解决生产者消费者问题的思想可参考博客:
【操作系统】同步互斥机制(一):同步互斥机制的介绍及信号量的深入理解
本篇第2节重点介绍了Hoare管程,Hoare的思想在于,当P进程唤醒Q进程,P进程切换下CPU由Q进程获取CPU的执行权,之后当条件变量满足之后P进程再次切换到CPU,从这过程可以看出实际上Hoare管程的多了两次额外的进程切换。为了解决这个问题进而引出了MESA管程。
Hoare管程是基于wait/signal机制,signal唤醒操作特点是唤醒了某一进程并且立刻让出CPU的,而MESA管程是基于wait/notify机制的。所谓的notify,是指当一个正在管程中的进程执行notify©时,它使得c条件队列得到通知,然后发出信号的进程继续执行。关于notify有三点需要注意:
程序清单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;
}
notify是每次只通知一个进程,MESA管程中还实现了每次通知多个进程的机制,即broadcast。
broadcast:使所有在该条件上等待的进程都被释放并进入队列。
broadcast使用场景:
在上一篇和本篇前几节已经分别介绍了信号量和管程的机制,但是这两种通信方式只能传递很简单的信息,不支持大量信息的传递,另外管程不适合用户多处理器的情况,所以需要引入新的通信机制。
消息传递的核心思想如下图所示:
程序清单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);
}
如下图所示,两个进程1、2都有各自的内存地址空间,在物理内存空间有一块共享内存,两个进程中都有一块内存空间映射到了同一块物理内存(即共享内存)。共享内存
也包含了一个读者-写者问题:不能多个进程同时写入,但支持同时读取。假设进程1想往共享内存写数据,只需要往本地映射的内存空间写入数据,相当于是写入了共享内存。
利用一个缓冲传输介质——内存或文件连接两个相互通信的进程。示意图如下:
管道通信方式需要注意以下三点:
除了这三种通信方式外,还有套接字、远程过程调用等。
不可分割,只执行完之前不会被其他任务或事件中断;
常用于实现资源的引用计数
1 概念
一种同步机制(又称栅栏、关卡);
用于对一组线程进行协调
2 应用场景
一组线程协同完成一项任务,需要所有线程都到达一个汇合点后再一起向前推进。
实际上Linux中的通信机制还包括:自旋锁、读写自旋锁、信号量、读写信号量、互斥体、完成变量、顺序锁等等。
本篇重点介绍了管程的概念及同步互斥机制,重点介绍了Hoare管程和MESA管程;介绍了进程间通信机制——消息传递、共享内存和管道,并介绍了Linux系统种的两种IPC机制。