一、进程的互斥
1.临界资源和临界区
在某段时间内只允许一个进程使用的资源称为临界资源
使用临界资源的那一部分程序称之为临界区
为了提供对互斥的支持,系统必须满足以下条件:
- 互斥:一次最多一个进程能够进入临界区,当有进程在临界区执行时,其他进程如果想要进入临界区,需要等待
- 有限等待:不能让一个进程无限制地在临界区执行,即任意进入临界区的进程必须在有限的时间内退出临界区。
- 空闲让进:如果某进程退出临界区,而又其他进程正在等待进入临界区时,应当让这个进程进入
使用硬件实现互斥
- 中断禁用
while{
// 禁用中断
临界区;
// 启用中断
}
作用:
保证了临界区不会发生中断,所以可以保证进程在进入临界区后不会被打断执行,因此有效地实现了互斥
缺点:该方法代价太高,由于禁止了中断,系统执行效率会有明显的降低。该方法不能用于多处理器解构
专用机器指令
compare_ & swap_指令
专用机器指令
优点:
- 适用范围广。适用于在单处理器或者共享内存的多处理器上的任意数目进程
- 使用简单。指令设置简单,容易验证其正确性。
- 可以支持多个临界区。
缺点: - 导致CPU空耗。当一个进程等待进入临界区时,会不断持续地检测,消耗处理器时间
- 导致饥饿进程。由于各种原因,当有多个进程都在等待进入临界区时,在某些极端情况下有可能某个进程永远无法进入临界区,发生饿死现象
信号量实现互斥
两个或多个进程可以通过简单的信号进行合作,一个进程可以被迫在某一位置停止,,直到他接受到一个特定的信号。
如果信号量的初始值为1,表示仅仅允许一个进程访问临界区,此时的信号量转换为互斥信号量。wait()操作和signal()操作分别置于进入区和退出区,如果定义mutex为互斥信号量,其初始值为1,则其应用模板为
车辆自动计数系统如果使用信号量机制,就能够很好地解决Observer进程和Reporter进程都计数器的互斥访问
int count;
semaphore S; S.value = 1;
void Observer(){
while(true){
Observer a lorry;
wait(S);
count := count +1;
signal(S);
}
}
void Reporter(){
while(true){
wait(S);
print count;
count := 0;
signal(S);
sleep(3600);
}
}
void main(){
count = 0;
parbegin(Observer,Reporter);
}
信号量实现同步
并发进程A与进程B共享一个缓冲区,假设缓冲区大小为N(最多可以同时存放N个数据),A进程将数据存入缓冲区,而进程B从缓冲区中将数据取出。此时两个进程的推进会受到某种关系的制约。
semaphore Bufempty, Buffull
Bufempty.value = n;Buffull.value = 0;
void A(){
wait(Bufempty);
// 按照FIFO方式选择一个空闲缓冲区
save(data);
signal(Buffull);
}
void B(){
wait(Buffull);
// 按照FIFO选择一个装满数据的缓冲区
retrieve(data);
signal(Bufempty);
}
void main(){
parbegin(A,B);
}
同步问题
生产者/消费者问题
semaphore mutex,mutex.value = 1;
semaphore empty,empty.value = n;
semaphore full,full.value = 0;
int i,j;
ITEM Buffer[n];
ITEM data_p,data_c;
void producer(){
while(true){
Producer an item in data_p;
wait(empty);
wait(mutex);
BUffer[i] = data_p;
i=(i+1)%n;
signal(mutex);
signal(full);
}
}
void consumer(){
while (true)
{
wait(full);
wait(mutex);
data_c = Buffer[j];
j = (j+1)%n;
signal(mutex);
signal(empty);
consume the item in data_c;
}
}
读者写者问题
有一个 多个进程共享的 数据区,这个数据区可以是一个文件或者是一块内存空间,甚至可以是一组寄存器,读者进程只读这个数据区的数据,而写着进程只往数据区里面写数据;此外还必须满足以下条件:
- 任意多的读进程可以同时读取这个文件
- 一次只有一个写进程可以写文件
- 如果有一个写进程正在写文件,则进制任何读进程读文件
int readcount; readcount = 0;
semaphore mutex_counter,mutex_writer;
mutex_counter.value = 1; mutex_writer.value = 1;
void reader(){
while (true)
{
wait(mutex_counter){
readcount++;
if(readcount == 1){
wait(mutex_writer);
signal(mutex_counter);
Read file;
wait(mutex_counter);
readcount--;
if(readcount == 0){
signal(mutex_writer);
signal(mutex_counter);
}
}
}
}
}
void writer(){
while (true)
{
wait(mutex_writer);
write file;
signal(mutex_writer);
}
}
二、进程的同步
2.1同步问题
独木桥问题
当行人需要经过一座独木桥时,同一方向的行人可连续过桥,每次只允许一个人过桥,当某一方有人过桥时,另一方向的行人必须等待;当某一方向无人过桥时,另一方向的行人可以过桥。
饭桌上有一个碟子可以盛装水果,但是一次只能盛装一个水果。父亲一次向碟子上放上一个橘子,母亲一次向碟子放上一个苹果;女儿只吃苹果,儿子只吃橘子。
在这个场景里,显然,碟子是一个临界资源。父亲,母亲,女儿,儿子可以看做是并发执行的四个进程,这四个进程的推进既需要实现互斥,同时也需要实现同步关系,仔细分析一下,这四个进程要满足以下几个条件:
(1)父亲、母亲、儿子、女儿必须互斥地使用碟子。
(2)只有当碟子里的水果被人取走时,父亲或母亲才能重新放入水果。
(3)只有当碟子上有水果时,儿子或女儿才能取走自己喜欢的水果。
进程的前驱后继问题
三、进程之间的通信
进程之间的高级通信机制可以大体分为以下几类:
- 共享内存方式
- 管道通信机制
- 消息传递通信
3.1 共享内存方式
3.2管道通信
管道是用于连接一个读进程和一个写进程实现进程之间通信的一种共享文件又称为pipe文件。向管道提供输入的是发送进程或者又称为写进程;而接受管道数据的进程称为读进程。管道通信机制必须提供以下几个方面的协调能力
- 互斥:当一个进程正在对管道进行读或者写操作时,另一个进程必须等待。
- 同步:管道的大小是有限的。所以当管道满时,写进程必须等待,直到读进程把他唤醒为止。同理当管道没有数据时,读进程也必须等待,直到写进程将数据写入管道后,读进程才被唤醒。
- 确认对方是否存在。只有确认对方存在时才能进行通信。
3.3消息传递通信
在消息传递系统中,进程之间的数据交换以消息为单位,根据实现方式的不同又可以分为直接通信方式和间接通信方式
-
在直接通信方式中,发送进程直接将消息发送给接受进程,接受进程可以接受来自任意发送方的消息,并且在读出消息的同时得知发送者是谁
-
在间接通信方式中,消息不是直接从发送方送到接受方,而是发送到临时消息队列(信箱)这种方式在使用上具有很大的灵活性,发送和接受方的关系可以是一对一、多对一、多对多的关系
消息传递的实际功能以send原语和receive原语的形式提供
邮箱通信
若干进程都可以向同一个进程发送信件,接收信件的进程可以设立一个信箱。信箱的大小决定了信箱中可以容纳的信件数。信箱由“信箱说明”和“信箱体”两部分组成
遵循规则: -
若发送信件时邮箱已经满,应该把发送信件的进程设置为“等信箱”状态。直到信箱有空时才被释放。
-
若取信件是信箱中没有信件,则应该把接受信件的进程设置为“等信件”的状态,直到信箱中有信件时才被释放
假设某操作系统中启动磁盘的工作由一个称为“磁盘管理”的进程统一来做,那么任意一个要访问磁盘的进程就只要向磁盘管理进程发一封信。磁盘管理进程只要逐封处理信箱中的信件就能使各个进程得到从磁盘上读出的信息或者把信息写到磁盘上。
四、管程
虽然信号量提供了一种方便并且十分有效的进程同步控制,但是他们的错误使用可能导致一些时序错误,这些错误很难被检测到。
引入了管程的概念,把分散在各进程中的临界区集中起来管理;放置进程有意无意的违法同步操作;便于高级语言来书写程序,也便于程序正确性验证。
一个管程定义了一个数据结构和在该数据结构上能为并发进程所执行的一组操作,这组操作能够同步进程和管程中的数据。管程在结构上由以下三部分组成:
- 管程所管理的共享数据结构,这些数据结构是对应临接资源的抽象
- 建立在该数据结构上的一组操作
- 对上述数据结构进行初始化的语句
-