第二章 Part3 进程同步与互斥的实现
第一节 进程同步和进程互斥
什么是进程同步
- 进程具有
异步性
,并发进程的状态独立、不可预知需要保证并发执行的进程按预期的顺序执行
OS需要提供
进程同步
的功能
同步又称
直接制约关系
用于协调进程的工作次序
什么是进程互斥
进程的
并发
需要共享
的配合两种共享
同时共享:一个时间段允许多个进程访问
互斥共享:一个时间段内只允许一个进程访问该资源
此类资源称作
临界资源
对此类资源的访问需要互斥的进行
- 所以进程互斥也称作
简介制约关系
实现方式:进入区和推出区完成
进程互斥
的实现。临界区是访问临界资源的代码段四个部分
进入区
临界区
退出区
剩余区
四个原则
空闲让进:临界区空闲,需要允许程序访问
忙则等待:有程序正在访问时,其他程序需要等待
有限等待:需要保证不会饥饿
让权等待:进入不了临界区的进程,需要释放处理机
第二节 进程互斥的软件实现
- 算法思想
- 伪代码演示
- 优缺点
单标志法
算法思想:进程访问完临界区之后,将临界区使用权交给另一个进程,每个进程进入临界区的权限由另一个进程来赋予
(伪)代码演示:
int turn = 0; // turn表示当前进程编号 /* * P0进程 */ void P0_start() { while (turn != 0); // 进入区,不是0号则一直阻塞 critical section; // 临界区,读取和写入 turn = 1; // 交还使用权 reminder section; // 剩余区 } /* * P1进程 */ void P1_start() { while(turn != 1); // 不是1,阻塞 critical section; // 临界区,读取和写入 turn = 0; // 交还使用权 reminder section; // 剩余区 }
优缺点:
优:能实现进程互斥
缺:
若P0执行完之后,P1尚不需要访问临界区,则此时P0也不能访问临界区
- 违反了“空闲让进”的原则
双标志先检查:先检查后上锁
算法思想:设置一个
bool
型数组,标志各进程进入临界区的“意愿”,每个进程进入临界区之前先检查是否之前还有程序想进入临界区,没有则自己进入伪代码表示:
bool flag[2]; flag[0] = false; flag[1] = false; // 开始时均无意愿进入临界区 /* * P0进程 */ void P0_start() { while(flag[1]); // 检查P1是否在访问临界区 flag[0] = true; // 标记P0想进入 critcal section;// 访问临界区 flag[0] = false;// P0访问完成 reminder section; } /* * P1进程 */ void P1_start() { while(flag[1]); // 检查P0是否在访问临界区 flag[0] = true; // 标记P0想进入 critcal section;// 访问临界区 flag[0] = false;// P0访问完成 reminder section; }
优缺点:
优:能实现进程互斥
缺:可能违反“忙则等待”,多个进程一起访问临界区
问题在于
while(flag[1]); // 检查P1是否在访问临界区 flag[0] = true; // 标记P0想进入
进入去检查和上锁不是同步完成,检查后上锁前仍然有可能发生切换
双标志后检查:先上锁后检查
算法思想:大致类双标志先检查
伪代码表示:
bool flag[2]; flag[0] = 0; flag[1] = 0; //初始状态,两个进程均无意愿 void P0_start() { flag = true; //先表示访问的意愿 while(flag[1]); // 若已经有进程在使用,阻塞 critical section; flag[0] = false; reminder section; } void P1_start() { flag[1] = true; while(flag[0]); // 有进程在访问,阻塞 critical section; flag[1] = false; reminder section; }
优缺点:
优:能完成进程互斥的功能
缺:可能违反
空闲让进
和有限等待
的原则flag[0] = true; flag[1] = true; // 进程0表达意愿之后紧接着进程1也表达 while(flag[1]); // 此时进程0、1均无法进入临界区
Peterson算法
算法思想:主动让给对方使用临界区
伪代码表示
bool flag[2]; int turn = 0; // 表示让那个(标号的)进程优先使用临界区 void porcess_0() { flag[0] = true; turn = 1; // 主动让1进入 while(flag[1] && turn == 1); // 若都想进入,则阻塞当前进程 crtical section; flag[0] = false; reminder section; } void process_1() { flag[1] = true; turn = 0; while(flag[0] && turn == 0); critical section; flag[1] = false; reminder section; }
优缺点:
优:遵循空闲让进、忙则等待、有限等待
缺:未能遵循让权等待
第三节 进程互斥的硬件实现方法
- 原理
- 优缺点
中断屏蔽
原理
在一个进程访问临界区前将中断暂时关闭
不可能在访问临界区时发生中断
优缺点
优:简单高效
缺
不适用多核处理器(可能多个核心中的进程同时访问一个临界区)
不适用于用户进程(开关中断指令只能运行在内核态)
TestAndSet(简称TS或TSL)
原理
使用硬件标示对临界区“上锁”
优点
优:保证上锁和检查一步完成(由硬件保证不被中断)
缺:导致忙等
Swap指令(Exchange指令或XCHG指令)
原理与优缺点类TSL(均为硬件实现)
第四节 信号量机制
使用一对原语
来对信号量进行操作
-
信号量是一个变量
-
表示系统中某个资源的数量
-
一对原语:
wait(S)
和signal(S)
原语,S代表信号量参数,常称作P、V操作,也简写做P(S)、V(S)
整型信号量
用一个整数变量作为信号量
与普通整型变量的区别,仅有初始化、P、V三种操作
伪代码示例
int S = 1; // 初始化信号量S void wait(int S) { // wait原语,相当于进入区 while(S <= 0); // 资源不足,阻塞 S = S -1; // 资源足够,占用其中一个 } void signal(int S) { // Signal原语,相当于退出区 S = S + 1; // 释放资源 }
不满足让权等待
记录型信号量
使用数据结构表示信号量
伪代码表示
struct { int value; // 剩余资源数 struct process *L; // 指向等待队列 } semaphore; /* *wait原语,某进程需要使用资源 */ void wait(semaphore S) { S.value--; if(S.value < 0) { // 若无系统资源 block(S.L); // 使用block原语使进程进入阻塞态,并将信号挂在S的等待(阻塞)队列中 } } /* *signal操作 */ void signal(semaphore S) { s.value++; if (S.value <= 0) { // 此时仍有进程处于等待队列未被分配到资源 wakeup(S.L); //从等待队列中唤醒一个进程 } }
S.value()
小于0时绝对值就是阻塞队列进程中进程的数量
第五节 信号量机制的应用
信号量机制实现进程互斥
划定临界区
- 设置互斥信号量
mutex
,初始值为1(实现进程同步是设为0)伪代码表示(见第四节:记录型信号量)
semaphroe mutex = 1; // 初始化信号量 P1() { // 略 P(mutex); // 加锁,mutex-- // 临界区操作 V(mutex); // 解锁,mutex++ // 略 } P2() { // 略 P(mutex); // 临界区操作 V(mutex); // 略 }
- 不同临界资源设置不同的信号量
- 临界区前进行
P()
,临界区后进程V()
信号量机制实现进程同步
并发进程执行存在异步性,需要实现进程同步保证代码的执行次序
- 设置同步信号量S = 0
- 在“前操作”之后执行
V(S)
- 在“后操作”之前执行
P(S)
信号量机制实现进程的前驱关系
- 为每一对前驱关系设定一个同步变量
- 前操作之后
V(S)
(S++),后操作之前P(S)
(S–)
第六节 生产者-消费者问题
系统进程存在消费者和生产者进程
生产者每次生产一个产品,放入缓冲区,缓冲区已满时,需要等待(被阻塞)
消费者每次从缓冲区里拿走一个产品,缓冲区为空时,需要等待(被阻塞)
共享使用一个初始为空,大小为n的缓冲区
使用P、V操作来实现生产者、消费者的互斥运作分析
几类进程?
两类进程:生产者和消费者
那些进程存在同步和互斥关系?
同步:特别注意多对的同步关系
缓冲区满,生产者等待
缓冲区空,消费者等待
互斥
生产者和消费者不能同时访问临界区
为每一对关系设置信号量
互斥信号量设置为1,同步信号量根据资源总数确定
semaphore mutex = 1; // 互斥信号量,实现对缓冲区的互斥访问 semaphore empty = n; // 同步信号量,表示缓冲区的数量(缓冲区空间大小) semaphore full = 0; // 同步信号量,表示非空缓冲区的数量(产品数量)
生产者每次需要P一次,消费者每次V一次
生产者-消费者问题的具体实现
producer() { while(1) { 生产一个产品; P(empty); // 消耗一个缓冲区 P(mutex); 放入缓冲区; V(mutex); V(full); // 产品数量+1 } }
consumer() { while(1) { P(full); // 消耗一个非缓冲区 P(mutex); 取出一个产品; V(mutex); V(empty); 使用产品; } }
先进行进程同步的P操作,然后进行进程互斥的P操作
V操作的进程可以交换
例如:
empty = 0; //缓冲区已满 full = n;
此时
producer { ………… P(mutex); P(empty); // 被阻塞 ………… }
若此时再执行
consumer { P(mutex); // 此时消费者进程也被阻塞 }
导致了死锁的现象
第七节 多生产者-多消费者问题
例子:两对生产者-消费者关系
父亲 - 苹果 - 女儿
母亲 - 橘子 - 儿子
一个盘子(缓冲区)
分析如下
几类进程
生产者:父亲、母亲
消费者:儿子、女儿
临界资源:苹果、橘子
临界区大小:1
那些进程存在同步和互斥关系
同步:
父亲放 - 女儿拿
母亲放 - 儿子拿
盘子为空才能放水果
容易忽略
儿子女儿都能触发
互斥:
父亲放 - 母亲放
儿子拿 - 女儿拿
定义信号量
/*同步信号量*/ semaphore apple = 0; semaphore orange = 0; semaphore plate = 1; /*互斥信号量*/ semaphore mutex = 1;
具体实现
dad() { while(1) { 准备一个苹果(生产一个资源); P(plate); // 占用一个资源 P(mutex); // 占用临界区 放入一个苹果; V(mutex); V(apple); } }
mom() { while(1) { 准备一个橘子(生产一个资源); P(plate); P(mutex); 放入一个橘子; V(mutex); V(orange); } }
daughter() { while(1) { P(apple); // 检查资源 P(mutex); 取出苹果; V(mutex); V(plate); 吃掉苹果; } }
son() { while(1) { P(orange); // 检查资源 P(mutex); 取出苹果; V(mutex); V(plate); 吃掉橘子; } }
临界区大小为1时,可以不设置互斥信号量
mutex
第八节 吸烟者问题
问题描述
三个抽烟者进程,一个供应者进程
三个吸烟者分别拥有烟草、纸、胶水
供应者可以供应上述三种材料,一次供应三种中的两种,此时有剩下一种材料的用户将取走这两种材料,并卷成烟抽掉
供应者需要让吸烟者轮流的吸烟
关系分析
几个元素?
三个吸烟者、一个供应者
三种物品组合
对组合的容量为1
同步和互斥
桌上有
组合1 - 吸烟者1
组合2 - 吸烟者2
组合3 - 吸烟者3
完成信号 - 供应者放上下一组材料
缓冲区大小为1,可以不设置互斥信号量
设置信号量
semaphore offer1 = 0; // 组合1的数量 semaphore offer2 = 0; semaphore offer3 = 0; semaphore finish = 0; // 完成信号 int i = 0; // 指示供应给哪一位吸烟者,实现轮流让吸烟者吸烟
具体实现
provider() { while(1) { if (i = 0) { 放上组合1; V(offer1); } else if(i == 1) { 放上组合2; V(offer2); } else if(i == 2) { 放上组合3; V(offer3); } i = (i++) % 3; P(finish); } }
smoker1() { while(1) { P(offer1); // 取走组合1 V(finish); // 完成信号 } }
smoker2() { while(1) { P(offer2); // 取走组合2 V(finish); } }
smoker2() { while(1) { P(offer3); // 取走组合2 V(finish); } }
第九节 读者 - 写者问题
读者进程不会改变数据,所以可以多个读者同时访问共享数据
问题概述
读者、写者两组并发进程共享一个文件
- 允许多个读者同时对文件进行操作
- 只允许一个写者向文件中写信息
- 写者操作时,不允许其他单位同时操作
问题分析
几个元素
读者
写者
几组关系
互斥关系
写进程 - 写进程
写进程 - 读进程
设置信号量
semaphore rw = 0; // 表示当前是否有进程在访问共享文件 semaphore mutex = 1; // 保证对conut变量的互斥访问 semaphore w = 1; // 保证写进程优先,否则可能导致写进程饥饿 int count = 0; // 记录当前有几个读进程正在访问文件
具体操作
writter() { while(1) { P(w); P(rw); 写文件; V(rw); V(w); } }
reader() { while(1) { P(w); P(mutex); if (count == 0) { // 此时,该读进程是第一个要访问临界区的读进程 P(rw); // 给文件“加锁”,阻塞写进程 } count++; V(mutex); V(w); 读文件; P(mutex); count--; if(count == 0) { // 此时,该读进程是最后一个访问临界区的读进程 V(rw); // 为文件解锁 } V(mutex); } }
设置
mutex
的目的:在第一个读进程P(rw)
前切换到第二个进程,则进程二的检查也能通过,但是P(rw)
被阻塞导致进程二被阻塞。此时进程一和进程二无法同时访问文件
第十节 哲学家就餐问题
问题描述
圆桌上五位哲学家
思考 || 进餐
桌上5支筷子,需要拿起左右两侧的筷子才能进餐
问题分析
元素分析
五个哲学家
每人两个临界资源
几组关系:每个哲学家和对应的两个资源
信号量设置
semaphore chopstick[5] = {1, 1, 1, 1, 1}; semaphore mutex = 1; // 互斥的取筷子,一个时刻只能由一个哲学家取筷子
对哲学家编号为0~4,哲学家i左侧的筷子编号为 i i i ,右侧的编号为 ( i + 1 ) % 5 (i + 1) \% 5 (i+1)%5
应当保证哲学家拿筷子的动作是互斥的
具体实现
pi() { // i号哲学家进程 while(1) { P(mutex); P(chopstick[i]); // 拿左 P(chopstick[(i + 1) % 5]); // 拿右 V(mutex); 吃饭; P(chopstick[i]); P(chopstick[(i + 1) % 5]); } }
其他解决方案
- 最多4名哲学家吃饭
…………
第十一节 管程
为什么引入管程
通过信号量机制编写程序困难、易出错
管程是一种高级的同步机制
管程的定义和基本特征
管程的本质是用
class
或者struct
对PV
操作进行封装定义
- 对应共享数据结构的说明(如缓冲区)
- 对该数据结构进行操作的一组过程(function)
- 对于共享数据设置初始值的语句(初始化数据结构)
- 管程的名字
特征
- 管程中的数据结构只能由管程中的函数修改
- 每次仅允许一个进程使用管程中的函数
拓展
用管程解决生产者-消费者问题
Java
中类似管程的机制