进程同步问题(主要是PV问题)
先了解基本概念
因为在多道环境下,进程之间是并发执行的,而程序之间为了抢夺各种资源,相互制约,如果不对这种制约加以协调,操作系统就很容易进入死锁状态,影响进程们的运行。所以我们需要引入进程同步的概念,来协调这种进程之间的制约关系。
-
临界资源
虽然系统中的资源可以给所有的进程共享,但是对于某些资源而言,同时只能提供给一个进程,这样的资源叫做临界资源,当临界资源已经被占用的时候,我们该如何协调想要访问这个资源的进程之间的关系呢?为此,我们将访问临界资源的过程分为了四个步骤:
- 进入区:检查是否能进入临界区,能进入的情况,对临界区上锁
- 临界区:访问临界资源的那一段代码
- 退出区:解锁
- 剩余区:代码的其余部分
do { entry section; //进入区 critical section; //临界区 exit section; //退出区 remainder section; //剩余区 }
-
同步
=,=感觉自己也没有很懂,为了协调两个进程之间的工作次序而等待,传递信息所产生的制约关系。
大概就是指的,A向缓冲区写数据,缓冲区中写满数据只后,B才能进去读操作。AB之间形成了同步关系。
-
互斥
也叫做间接制约关系,当A访问临界资源时,B必须被阻塞等待,当A释放临界资源了,系统才将B唤醒。AB之间形成互斥关系。
同步机制的四个准则:
- 空闲让进:屁话
- 忙则等待:屁话
- 有限等待:要防止饥饿现象
- 让权等待:进程不能进入临界区的时候要释放CPU
实现临界区互斥的基本方法
-
软件方法
-
单标志法:设置一个公共整型变量turn。当turn=0的时候允许进程P0访问临界资源turn = 1时允许进程P1访问临界资源。但是这样的方法是违反空闲让进原则的。当临界资源空闲时,且turn = 1,这时候P0想要访问临界资源也会被拒绝。
//P0进程 while(turn != 0); critical section; turn = 1; remainder section; //P1进程 while(turn != 1); critical section; turn = 0; remainder section;
-
双标志先检查法:为每个进程设置一个flag[i]数据元素,为true表示正在访问临界资源。有点是可以直接进入空闲的临界区,而不用像单标志法一样交替进入。但问题是按照一定的访问顺序,会导致两个进程都进入临界区。违反“忙则等待”。原因在于,检查标志和修改标志的操作不能一气呵成
//Pi进程 while(flag[j]); flag[i]=true; critical section; flag[i]=false; remainder section; //Pj进程 while(flag[i]); flag[j]=true; critical section; flag[j]=false; remainder section;
-
双标志后检查法:与双标志先检查法相反, 先进行标志修改,再进行标志检查,结果是有可能导致两个进程都无法进入临界区,造成饥饿现象
//Pi进程 flag[i]=true; while(flag[j]); critical section; flag[i]=false; remainder section; //Pj进程 flag[j]=true; while(flag[i]); critical section; flag[j]=false; remainder section
-
Peterson’s Algorithm(皮特森算法):在双标志后检查法的基础上加一个turn标志,来表示谦让避免出现饥饿现象
//Pi进程 flag[i]=true; turn = j; while(flag[j] && turn == j); critical section; flag[i] = false; remainder section; //Pj进程 flag[j] = true; turn = i; while(flag[i] && turn == i); critical section; flag[j]=false; remainder section;
-
-
硬件方法
-
中断屏蔽方法:通过关中断来保证互斥的实现。但是这样的方法限制了处理机交替处理程序的能力,降低了系统效率,同时,对于内核而言,将关中断的权力交给用户也是很不明智的。若进程关中断后不再开中断,就会引起系统的终止。
··· 关中断 临界区 开中断 ···
-
硬件指令方法:①用的是TestAndSet指令(也叫TestAndSetLock,TSL指令)这条指令是一个原子操作。它的功能是,读出标志位后,将其置为true;②用的swap指令
//TSL指令 boolean TestAndSet(boolean *lock){ boolean old; old = *lock; *lock = true; return old; } //互斥实现 while TestAndSet(&lock); critical section; lock=false; remainder section; //swap指令 Swap(boolean *a, boolean *b){ boolean temp; temp = *a; *a = *b; *b = temp; } //swap互斥实现 =,=没看懂,感觉怪怪的 key = true; while(key!=flase) swap(key, lock); critical section; lock = false; remainder section
PS:软硬件的实现代码,并不需要掌握,在实际的考试和应用中不会出现。
硬件实现方法优缺点:
优点:适用于任意数目的进程,适用于单处理机和多处理机,支持进程内有多个临界区的情况;简单,可以保证正确性。
缺点:不满足让权等待的原则,从等待的进程中随机选择一个进程进入临界区,可能有的进程一直不被选中,导致饥饿现象
信号量(本章的超级重难点,也是写这篇博客的意义所在)
信号量机制采用两个标准的==原语(P[wait()]V[signal()]操作)==来解决互斥同步的问题。对于PV操作的描述,在不同类型的型号量中实现的方法有所差异。
- 整型型号量
//P操作 wait(S){ while(S<=0); S = S - 1; } //V操作 signal(S){ S = S + 1; }
- 记录型信号量
typedef struct{ int value; struct process *L;//进程的阻塞队列 } semaphore; //对应P操作 void wait(semaphore S){ S.value --; if(S.value<0){ add this process to S.L; block(S.L);//将进程挂入阻塞队列 } } //V操作 void signal(semaphore S) { S.value ++; if(S.value<=0){ remove a process P from S.L; wakeup(P);//从阻塞队列中唤醒一个进程 } } block的使用实现了让权等待的功能
- 利用信号量实现同步(实现一定的顺序关系)
semaphoe S = 0; P1(){ .... x; V(S); } P2(){ ... P(S); y; ... } 先执行P1才能执行P2.因此是同步关系
- 利用信号量实现互斥
semaphore S = 1; P1(){ ... P(S); 进程P1的临界区 V(S); ... } P2(){ ... P(S); 进程P2的临界区 V(S); ... } 两个进程抢夺一个临界资源,没有特定的先后顺序,是互斥关系
-
利用信号量实现前驱关系(详见P86):①在一个节点运行完成后,对该节点后继的所有节点都进行一次V操作,②在一个节点运行前,对它前驱的节点进行一次P操作
-
分析进程同步和互斥问题的方法步骤
- 关系分析:①找出进程数目;②分析同步互斥前驱关系;
- 整理思路:找出问题的关键点,根据以前做过题目的经验,根据进程的操作流程(PS:你妈的,这也太玄学了)
- 设置信号量,确定初值
管程(互斥访问的特性由编译器来实现)
- 管程的定义:由代表共享资源的数据结构和能为并发进程所执行的一组操作所组成的资源管理程序。这组操作能同步进程和改变管程中的数据。每次只允许一个进程进入管程。
- 管程的组成:
- 名称
- 内部共享结构数据的说明
- 对数据结构的一组操作
- 对共享结构初始化的语句
- 条件变量:条件变量和信号量的区别在于,条件变量只实现排队等待功能,剩余资源数量由管程内部的共享数据结构记录。
经典同步问题
-
生产者-消费者问题
问题描述:
一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满的时候,生产者才能写入数据,只有缓冲区不空的时候消费者才能取出数据,缓冲区是临界资源,同时只允许一个生产者或一个消费者放入消息。
问题分析:
互斥关系:任意两个进程之间都是互斥关系
同步关系:生产者和消费者之间是同步关系,先生产才能进行消费
信号量的设置:设置一个mutex来作为互斥信号量,初始值为1,设置一个full来表示缓冲区中消息的数量,初始值为0,设置一个empty来表示缓冲区中空位的数量,初始值为n
进程描述:
semaphore mutex = 1; semaphore full = 0; semaphore empty = n; //生产者 producer() { while(1){ produce an item in nextp; P(empty);//同步关系,分析进程执行顺序,想要什么P一下 P(mutex);//最靠近临界资源;直接夹住 访问缓冲区; V(mutex); V(full);//提供什么V一下 } } //消费者 consumer() { while(1) { P(full); P(mutex); 取出数据; V(mutex); V(empty); } }
-
生产者-消费者问题(复杂情况)
问题描述:
桌上有个盘子,每次只能放一个水果。爸爸向盘子里放苹果,妈妈放橘子,儿子等着吃橘子,女儿等着吃苹果。
问题分析:
互斥关系:爸爸妈妈对盘子互斥
同步关系:爸爸和女儿同步,妈妈和儿子同步
信号量设置:设置一个互斥信号量plate = 1;apple = 0;orange = 0;
进程描述:
semaphore mutex = 1; semaphore apple = 0; semaphore orange = 0; //本题中缓冲区大小为1,不需要用mutex夹紧,不设置互斥信号量 daa() { while(1) { P(plate); 放苹果; //V(mutex); V(apple); } } mom() { while(1) { P(plate); 放橘子; //V(mutex); V(orange); } } son() { while(1) { P(orange) 拿橘子 V(plate) } } daughter() { while(1) { P(apple) 拿苹果 V(plate) } }
-
读者-写者问题
问题描述:
读者写者两组并发进程,共享一个文件,当两个以丄的读进程同时访问共享数据时不会发生副作用,但某个写进程和其他进程(包括读和写)同时访问共享数据时则出错。因此要求:①多个读者可以同时对文件进行读操作;②只允许一个写进程往文件中写信息;③任一写着在完成写操作前不允许其他读者写者工作;④写着执行操作前,应该让所有的读者和写者全部退出。(何解?抢夺?)
问题分析:
互斥关系:读者和写者互斥,写者和写者互斥
同步关系:读者和写者同步
信号量设置:mutex = 1; count = 0;
进程描述:
int count = 0; semaphore mutex = 1; //写者 writer() { while(1) { P(mutex); 写; V(mutex); } } //读者 reader() { while(1) { //下面的操作无法一气呵成,所以需要增加一个互斥信号量锁住,让其成为一个原子操作 P(mutex_r); if(count == 0) P(mutex); count ++; V(mutex_r); 读; P(mutex_r); count --; if(count == 0) V(mutex); V(mutex_r); } }
-
读者写者问题改进
问题描述:
在上面的处理方式会导致读者不停进入,写者被“饥饿”的现象。所以我们规定,当有写进程请求访问时,应该禁止后续读进程的请求,等到访问共享资源的读进程执行完毕时,立即让写进程执行。
int count = 0; semaphore mutex = 1; semaphore mutex_r = 1; semaphore mutex_w = 1; //写者 writer() { while(1) { P(mutex_w); P(mutex); 写; V(mutex); V(mutex_w); } } //读者 reader() { while(1) { P(mutex_w); P(mutex_r); if(count == 0) P(mutex); count++; V(mutex_r); V(mutex_w); 读; P(mutex_r); count--; if(count == 0) V(mutex); V(mutex_r); } }
-
哲学家进餐问题
问题描述:
五个哲学家,两个之间一根筷子,两根筷子之间一碗饭,哲学家饥饿时,试图拿起左右两根筷子,筷子在别人手上就等待,只有同时拿到两根筷子才能进餐。
问题分析:
互斥关系:五个进程对左右邻居之间的筷子互斥。
信号量设置:设置互斥信号量组chopstick[5]={1,1,1,1,1}
进程实现:
semaphore mutex = 1; semaphore chopsticks[5] = {1, 1, 1, 1, 1}; P1() { do() { P(mutex);//让拿左右筷子的操作一气呵成~不然会死锁的呢 P(chopsticks[i]); P(chopsticks[(i+1)%5]); V(mutex); 恰饭 P(mutex); V(chopsticks[i]); V(chopsticks[(i+1)%5]); V(mutex); }while(1); }
-
吸烟者问题
问题描述:
三个抽烟者进程,一个供应者进程。一共有三种材料(ABC),三个抽烟者进程分别持有其中一种。供应者进程每次会供应两种材料,拥有剩下那种材料的抽烟者取走它。
问题分析:
同步关系:供应者与三个抽烟者是同步关系
互斥关系:三个抽烟者对抽烟这个行为互斥
信号量设置:offer1 = 0, offer2 = 0, offer3 = 0; finish = 0;
进程实现:
int num = 0; semaphore offer1 = 0; semaphore offer2 = 0; semaphore offer3 = 0; semaphore finish = 0; //供应者 process P1() { while(1) { num = num%3; if(num == 0) { V(offer1); } else if(num == 1) { V(offer2); } else V(offer3); 放材料 P(finish); } } //抽烟者 process P2() { while(1) { P(offer1); 抽烟 V(finish); } } process P3() { while(1) { P(offer2); 抽烟 V(finish); } } process P4() { while(1) { P(offer3); 抽烟 V(finish); } }
-
关于同步问题的习题=。=
【2019统考】三个进程P1,P2,P3互斥使用一个包含N个单元的缓冲区。P1每次使用produce()生成一个正整数并用put()送入一个缓冲区某一空单元;P2每次使用getodd()从缓冲区中取出一个奇数并用countodd()统计奇数个数;P3用geteven()取出偶数并用counteven()统计偶数个数。
问题分析:
互斥关系:P1与P2、P3对缓冲区互斥
同步关系:P1与P2、P3分别同步
信号量设置:mutex = 1; empty = N; odd = 0; even =0;
进程实现:
semaphore mutex = 1; semaphore odd = 0; semaphore even = 0; semaphore empty = N; P1() { while(1) { x = produce(); x = x % 2; P(empty) P(mutex); put() if(x == 0 ) { V(even); } else { V(odd); } V(mutex) } } P2() { while(1) { P(odd); P(mutex); getodd(); V(mutex); V(empty); countodd(); } } P3() { while(1) { P(even); P(mutex); getodd(); V(mutex); V(empty); counteven(); } }
【2013年统考】某博物馆最多可容纳500人同时参观,有一个出入口,该出入口一次仅允许一人通过,参观者的活动描述如下:
cobegin 参观者进程i; { ... 进门 ... 参观 ... 出门 ... } coend;
请添加必要的信号量和PV操作,来实现上述过程中的互斥与同步要求写出完整的过程,并且说明信号量的含义并赋初值。
问题分析:
互斥关系:参观者对出入口和博物馆互斥
信号量设置:door = 1;num = 500;
进程实现:
semaphore door = 1; semaphore num = 500; Pi() { P(num); P(door); 进门; V(door); 参观; P(door); 出门; V(door); V(num); }
【2011统考真题】某银行提供一个服务窗口和10个供顾客等待的座位。顾客到达银行时,若有空座位,则取号机领取一个号。取号机每次仅允许一个顾客使用。当营业员空闲时,叫号选取一个顾客,并为其服务。顾客和营业员的活动描述如下:
cobegin { process 顾客 i { 从取号机获取一个号码; 等待叫号; 获取服务; } process 营业员 { while(1) { 叫号; 为客户服务; } } }
请添加必要的信号量和PV操作,实现上述过程中的互斥与同步。
问题分析:
同步关系:营业员和顾客同步
互斥关系:客户之间对服务窗口互斥
信号量设置:mutex = 1; full = 0; empty = 10;
进程实现:
semaphore mutex = 1; semaphore full = 0; semaphore empty = 10; cobegin { process 顾客 i { P(empty); P(mutex); 从取号机获取一个号码; V(mutex); V(full); P(mutex2); 等待叫号; 获取服务; } process 营业员 { while(1) { P(full); V(empty); V(mutex2); 叫号; 为客户服务; } } }
【2014年统考真题】系统中有多个生产者进程和多个消费者进程,共享一个能存放1000个产品的缓冲区(初始为空),缓冲区没满时,生产者进程可以放入其生产的一件产品,否则等待。缓冲区未空时,消费者可以取走一件产品,否则等待。要求一个消费者进程从缓冲区中连续取走10件产品后,其他消费者进程才可以取产品。
问题分析:
互斥关系:消费者和生产者互斥,消费者和消费者互斥
同步关系:生产者和消费者同步
信号量设置:empty = 1000; full = 0; mutex =1; ~~count = 10;~~mutex2 = 1;
进程实现:
semaphore empty = 1000; semaphore full = 0; semaphore mutex = 1; semaphore mutex2 = 1; producer() { while(1) { 生产一个产品; P(empty); P(mutex); 放入缓冲区 V(mutex); V(full); } } consumer() { while(1) { P(mutex2); for(int i=0; i<10; i++) { P(full); P(mutex); 取走一个产品; V(mutex); V(empty); } V(mutex2); } }
【2015年统考真题】有A、B两人通过信箱进行辩论,每个人都从自己的信箱中取得对方的问题。将答案的向对方提出的新问题组成一个邮件放入对方的邮箱中。假设A的信箱中最多放M个邮件,B的信箱中最多放N个邮件。初始时,A的信箱中有x个邮件。B的信箱中有y个邮件。
cobegin A() { while(1) { 从A的信箱中取出一个邮件; 回答问题并提出一个新问题; 将新邮件放入B的信箱中; } } B() { while(1) { 从B的信箱中取出一个邮件; 回答问题并提出一个新问题; 将新邮件放入A的信箱中; } } coend
问题分析:
互斥关系:AB对两个信箱互斥
同步关系:AB互相同步
信号量设置:mutexA = 1; mutexB = 1; emptyA = M-x;emptyB = N-y;fullA = x;fullB = y;
进程实现:
semaphore mutexA = 1; semaphore mutexB = 1; semaphore emptyA = M-x; semaphore emptyB = N-y; semaphore fullA = x; semaphore fullB = y; A() { while(1) { P(fullA); P(mutexA); 从A的信箱中取出一个邮件; V(mutexA); V(emptyA); 回答问题并提出一个新问题; P(emptyB); P(mutexB); 将新邮件放入B的信箱中; V(mutexB); V(fullB); } } B() { while(1) { P(fullB); P(mutexB); 从A的信箱中取出一个邮件; V(mutexB); V(emptyB); 回答问题并提出一个新问题; P(emptyA); P(mutexA); 将新邮件放入B的信箱中; V(mutexA); V(fullA); } }
【2017统考真题】某进程中有三个并发执行的线程thread1, thread2, thread3 其伪代码如下:
//复数的结构定义类型 typedef struct { float a; float b; } cnum; cnum x, y, z;//全局变量 //计算两个复数之和 cnum add(cnum p, cnum q) { cnum s; s a = p.a + q.a; s b = p.b + q.b; return s; } //三个线程 thread1 { cnum w; w = add(x, y); ... } thread2 { cnum w; w = add(y, z); ... } thread3 { cnum w; w.a = 1; w.b = 1; z = add(z, w); y = add(y, w); ... }
问题分析:
互斥关系:
123对y互斥,23对z互斥应该注意到12只是读进程,3才是写进程。因此两个读不会互斥。读和写才会互斥。所以1与3关于y互斥,2与3关于yz互斥 信号量设置:mutexy1 = 1; mutexy2 = 1;mutexz=1;
进程实现:
thread1 { cnum w; P(mutexy1); w = add(x, y); V(mutexy1); ... } thread2 { cnum w; P(mutexy2); P(mutexz); w = add(y, z); V(mutexz); V(mutexy2); ... } thread3 { cnum w; w.a = 1; w.b = 1; P(mutexz); z = add(z, w); V(mutexz); P(mutexy1); P(mutexy2); y = add(y, w); V(mutexy1); V(mutexy2); ... }
【2019年统考真题】有n(n>=3)个哲学家围坐在一张圆桌边上,每个哲学家交替地就餐和思考,在圆桌中心,有m个碗,每两名哲学家之间有1根筷子,每名哲学家取得一个碗和两侧的筷子后才能就餐。
问题分析:
互斥关系:哲学家对碗互斥,相邻哲学家对筷子互斥;
信号量设置:mutex = m; chopsticks[n] = {1};
进程实现:
semaphore mutex = m; semaphore chopsticks[n]; for(int i = 0;i<n;i++) { chopsticks[i] = 1; } mutex = min(mutex, n-1); Pi() { while(1){ P(mutex); P(chopsticks[i]); P(chopsticks[(i+1)%n]); 恰饭; V(chopsticks[i]); V(chopsticks[(i+1)%n]); V(mutex); } }
-
总结:
关于问题分析的环节,感觉出了点问题。不应该围绕进程展开进行分析,而是对资源展开进行分析进程之间的关系,而不是简单的判断进程之间的关系(感觉这样于解题并无太大的作用。)另外没有仔细分析自己的解题思路=。=这就很烦,导致解题的过程很随机,很不规范。加油!
- 先找临界资源
- 从临界资源入手,分析谁互斥,谁同步
- 确定临界资源的数量,特别的,对于缓冲区经常会采用empty和full两对变量来进行操作。一个锁住下限,一个锁住上限
=。=格式问题,第一次写这么长的感觉好乱写的。而且写的很臃肿。没有想好自己的大纲逻辑就开始动笔了,想到哪写到哪。加油szx
写给自己纪念的一篇小玩意儿。