2.3 进程同步 进程互斥
2.3 进程同步 进程互斥
进程具有异步性的特征。异步性是指,各并发执行的进程以各自 独立的、不可预知的速度向前推进。 操作系统要提供“进程同步机制”来解决异步问题。进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免的需要共享一些系统资源(比如内存,又比如打印机、摄像头这样的I/O设备)
我们把一个时间段内只允许一个进程使用的资源称为临界资源。进程互斥指当一个进程访问某临界资源 时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后, 另一个进程才能去访问临界资源。
对临界资源的互斥访问,可以在逻辑上分为如下四个部分
do{
entry section; //进入区:负责检查是否可进入临界区,若可进入,则应设置正在访问临界资源的标志(可理解为“上锁”),以阻止其他进程同时进入临界区
critical section; //临界区:访问临界资源的那段代码
exit section; //退出区:负责解除正在访问临界资源的标志(可理解为“解锁”)
remainder section //剩余区:做其他处理
}
注意: 临界区是进程中访问临界资源的代码段。 进入区和退出区是负责实现互斥的代码段。 临界区也可称为“临界段”
如果一个进程暂时不能进入临界区, 那么该进程是否应该一直占着处理 机?该进程有没有可能一直进不了 临界区?
实现对临界资源的互斥访问需要遵循以下原则:
- 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区;
- 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待
- 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)
- 让权等待。当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。
进程互斥的 软件实现方法
- 单标志法
- 双标志先检查
- 双标志后检查
- Peterson算法
学习提示: 1. 理解各个算法的思想、原理 2. 结合上小节学习的“实现互斥的四个逻辑部分”,重点理解各算法在进入区、退出区都做了什么 3. 分析各算法存在的缺陷(结合“实现互斥要遵循的四个原则”进行分析)
如果没有注意进程互斥? 结局:A、B 的打印内容混在一起了
单标志法
算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程 进入临界区的权限只能被另一个进程赋予
P0进程:
while (turn != 0) ;//不是0就一直卡在这里
critical section;
turn = 1; //好一个孔融让梨
remainder section;
P1进程:
while (turn != 1) ;
critical section;
turn = 0;
remainder section;
该算法可以实现“同一时刻最多只允许一个进程访问临界区”
只能按 P0、P1 、P0 、P1 ……这样轮流访问。这种必须“轮流访问”带来的问题是,如果此时允许进 入临界区的进程是 P0,而 P0 一直不访问临界区,那么虽然此时临界区空闲,但是并不允许 P1 访问。
因此,单标志法存在的主要问题是:违背“空闲让进”原则。
双标志先检查法
算法思想:设置一个布尔型数组 flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如 “flag[0] = ture”意味着 0 号进程 P0 现在想要进入临界区。
每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志 flag[i] 设为 true,之后开始访问临界区。
bool flag[2]; //给出想进入临界区的名额
flag[0] = false;
flag[1] = false; //刚开始设置为都不想进入临界区
P0进程︰
while (flag[1] );flag[0] = true;critical section;flag[0] = false;remainder section;
信号量机制
理解:信号量 mutex 表示 “进入临界区的名额”。P、V操作必须成对出现。
复习回顾+思考:之前学习的这些进程互斥的解决方案分别存在哪些问题? 进程互斥的四种软件实现方式(单标志法、双标志先检查、双标志后检查、Peterson算法) 进程互斥的三种硬件实现方式(中断屏蔽方法、TS/TSL指令、Swap/XCHG指令)
- 在双标志先检查法中,进入区的“检查”、“上锁” 操作无法一气呵成,从而导致了两 个进程有可能同时进入临界区的问题;
- 所有的解决方案都无法实现“让权等待 ----- 一种卓有成效的实现进程互斥、同步的方法——信号量机制
- 整型信号量
- 记录型信号量
一对原语:wait(S) 原语和 signal(S) 原语,可以把原语理解为我们自己写的函数,函数名分别为 wait和 signal,wait、signal 原语常简称为 P、V操作(来自荷兰语 proberen 和 verhogen)。因此,做题的时候常把
wait(S)、signal(S) 两个操作分别写为 P(S)、V(S)
信号量的值 = 这种资源的剩余数量(信号量的值如果小于0,说明此时有进程在等待这种资源)
P( S ) —— 申请一个资源S,如果资源不够就阻塞等待
V( S ) —— 释放一个资源S,如果有进程在等待该资源,则唤醒一个进程
整型信号量
用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。 Eg :某计算机系统中有一台打印机…
int s = 1;/初始化整型信号量s,表示当前系统中可用的打印机资源数
void wait (int s) {l / wait原语,相当于“进入区”一
while (s <= 0) ;
//如果资源数不够,就—直循环等待
s=S-1;
//如果资源数够,则占用一个资源
}
void signal (int s) { l/ signal原语,相当于“退出区”
S=S+1;
/使用完资源后,在退出区释放资源
“检查”和“上锁”一气呵成, 避免了并发、异步导致的问题
实现进程互斥、同步
记录型信号量
整型信号量的缺陷是存在“忙等”问题,因此人们又提出了“记录型信号量”,即用记录型数据结构表 示的信号量。
注:若考试中出现 P(S)、V(S) 的操作,除非特别说明,否则默认 S 为记录型信号量。
问:如果信号量S的当前值为-8, 则表示系统中共有8个等待进程(对)
信号量机制实现进程互斥
typedef struct {
int value;
struct process *L;
}semaphore;
semaphore mutex=1; //若题未强调,可以简写
进程同步:要让各并发进程按要求有序地推进。
生产者消费者问题
系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。
- 生产者、消费者共享一个初始为空、大小为n的缓冲区,缓冲区是临界资源,各进程必须互斥地访问。
- 缓冲区没满 → 生产者生产
- 缓冲区没空→ 消费者消费
需要设定参数
semaphore mutex = 1; //互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n; //同步信号量,表示空闲缓冲区的数量
semaphore full = 0; //同步信号量,表示产品的数量,也即非空缓冲区的数量
producer (){
while(1){
生产一个产品;
P(empty); // P(mutex); 若先申请mutex互斥信号量,若此时缓冲区内已经放满产品,由于已没有空闲缓冲区,因此生产者被阻塞,又因为已经申请了互斥信号量,所以消费者也被阻塞。
P(mutex); // P(empty); 所以,这样的情况下,会导致死锁
把产品放入缓冲区;
V(mutex);
V(full);
}
}
consumer (){
while(1){
P(full); //同样的,若缓冲区中没有产品,若调换顺序,消费者这里也会阻塞
P(mutex); //结论:不能改变相邻P操作的顺序!!V操作不会导致进程阻塞,故两个V操作顺序可以交换
从缓冲区取出一个产品;
V(mutex);
V(empty);
使用产品;
}
}
问: 在生产者—消费者问题中,能否将生产者进程的wait(empty)和wait(mutex)语句互换,为什么?
不能。因为这样可能导致系统死锁。当系统中没有空缓冲时,生产者进程的wait(mutex)操作获取了缓冲队列的控制权,而wait(empty) 导致生产者进程阻塞,这时消费者进程也无法执行。
多生产者-多消费者
桌子上有一只盘子,每次只能向其中放入一个水果。
爸爸专向盘子中放苹果,妈妈专向盘子中放橘子。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。
儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。
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);
吃掉橘子;
}
}
此时,这个题目里面甚至不用出现mutex
因为即使不设置专门 的互斥变量mutex,也不 会出现多个进程同时访 问盘子的现象
原因在于:本题中的缓冲区大小为1,在任 何时刻,apple、orange、plate 三个同步信 号量中最多只有一个是1。因此在任何时刻, 最多只有一个进程的P操作不会被阻塞,并 顺利地进入临界区…
当盘子的容量为2就不一样了
总结:在生产者-消费者问题中,如果缓冲区大小为1,那么有可能不需要设置互斥信号量就可以实现 互斥访问缓冲区的功能。当然,这不是绝对的,要具体问题具体分析。 建议:在考试中如果来不及仔细分析,可以加上互斥信号量,保证各进程一定会互斥地访问缓冲区。 但需要注意的是,实现互斥的P操作一定要在实现同步的P操作之后,否则可能引起“死锁”。
吸烟者问题
本质上这题也属于“生产者-消费者”问题,是“可生产多种产品的单生产者-多消费者”。
假设一个系统有三个抽烟者进程和一个供应者进程。
每个抽烟者不停地卷烟并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。
三个抽烟者中,第一个拥有烟草、 第二个拥有纸、第三个拥有胶水。
供应者进程无限地提供三种材料,供应者每次将两种材料放桌 子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者进程一个信号告诉完成了,供应者就会放另外两种材料再桌上,这个过程一直重复(让三个抽烟者轮流地抽烟)
桌子可以抽象为容 量为1的缓冲区, 要互斥访问
值得吸取的精华是:“轮流让各个吸烟者吸烟”必然需要“轮流的在桌上放上组合一、二、三”,注 意体会我们是如何用一个整型变量 i 实现这个“轮流”过程的。 如果题目改为“每次随机地让一个吸烟者吸烟”,我们有应该如何用代码写出这个逻辑呢? 若一个生产者要生产多种产品(或者说会引发多种前驱事件),那么各个V操作应该放在各自对应的 “事件”发生之后的位置。
semaphore offer1 = 0; //桌上组合一的数量
semaphore offer2 = 0; //桌上组合二的数量
semaphore offer3 = 0; //桌上组合三的数量
semaphore finish = 0; //抽烟是否完成
int i = 0; //用于实现“三个抽烟者轮流抽烟”
provider (){
while(1){
if(i==0) {
将组合一放桌上;
V(offer1);
} else if(i==1){
将组合二放桌上;
V(offer2);
} else if(i==2){
将组合三放桌上;
V(offer3);
}
i = (i+1)%3;
P(finish);
}
}
是否需要设置 一个专门的互 斥信号量?
smoker1 (){
while(1){
P(offer1);
从桌上拿走组合
一;卷烟;抽掉;
V(finish);
}
}
smoker2 (){
while(1){
P(offer2);
从桌上拿走组合
二;卷烟;抽掉;
V(finish);
}
}
smoker3 (){
while(1){
P(offer3);
从桌上拿走组合
三;卷烟;抽掉;
V(finish);
}
}
读者-写者问题
有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致 数据不一致的错误。
因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者 往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写工作;④写者执行写操作 前,应让已有的读者和写者全部退出。
两类进程:写进程、读进程
互斥关系:写进程—写进程、写进程—读进程。读进程与读进程不存在互斥问题。
方案1
semaphore rw=1; //用于实现对共享文件的互斥访问
int count = 0; //记录当前有几个读进程在访问文件
semaphore mutex = 1;//用于保证对count变量的互斥访问
writer (){
while(1){
P(rw);
写文件…
V(rw);
}
}
reader (){
while(1){
P(mutex); //各读进程互斥访问count
if(count==0) //由第一个读进程负责
P(rw); //读之前“加锁”
count++; //访问文件的读进程数+1
V(mutex);
读文件…
P(mutex); //各读进程互斥访问count
count--; //访问文件的读进程数-1
if(count==0) //由最后一个读进程负责
V(rw); //读完了“解锁”
V(mutex);
}
}
思考:若两个读进程并发执行,则 count=0 时两个进程也许都能满足 if 条件,都会执行 P(rw),从而使第二个读进程阻塞的情况。 如何解决:出现上述问题的原因在于对 count 变量的检查和赋值无法一气呵成,因 此可以设置另一个互斥信号量来保证各读进 程对count 的访问是互斥的。
潜在的问题:只要有读进程还在读,写 进程就要一直阻塞等待,可能“饿死”。 因此,这种算法中,读进程是优先的
方案2
semaphore rw=1; //用于实现对共享文件的互斥访问
int count = 0; //记录当前有几个读进程在访问文件
semaphore mutex = 1; //用于保证对count变量的互斥访问
semaphore w = 1; //用于实现“写优先”
writer (){
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);
}
}
结论:在这种算法中,连续进入的多个读者可以同时读文件;写者和其他进程不能同时访问文件;写者不会饥饿,但也并不是真正的“写优先”,而是相对公平的先来先服务原则。
有的书上把这种算法称为“读写公平法”。
读者-写者问题为我们解决复杂的互斥问题提供了一个参考思路。
其核心思想在于设置了一个计数器count用来记录当前正在访问共享文件的读进程数。我们可以用count的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。
另外,对count变量的检查和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量。