操作系统-进程管理2(进程的同步与互斥)
- 进程的同步与互斥
- 两个或两个以上的进程不能同时使用的资源称为临界资源。临界资源的存在带来了进程之间的互斥访问的问题。
- 进程互斥:逻辑上完全独立、毫无关系的两个进程因为竞争同一块资源而相互制约,称为进程互斥。
- 进程同步:有协作关系的进程不断调整它们之间的相对速度或执行过程,以保证临界资源的合理利用和进程的顺利执行。一般借由中间媒体实现:如信号量操作、加锁操作等。同步机制应遵循的规则:
- 空闲让进
- 忙则等待
- 有限等待:进程等待进入临界区的时间必须是有限的,避免进入忙等状态
- 让权等待:进程不能进入自己的临界区时,应立即释放处理机。
- 锁机制
- 上锁与开锁
锁机制采用锁变量
w
表示临界区是否上锁。w=1
表示已上锁高效率的上锁与开锁原语为:
//加锁原语 Lock w() { while (w == 1) { //表示当前进程进入不了临界区 保护当前进程的CPU现场; 将当前进程放入w的等待队列,将该进程置于"等待"状态; 转进程调度; } } //开锁原语 Unlock() { if (w等待队列不空) { 移出等待队列首元素; 将该进程置于就绪状态,并放入就绪队列; } w = 0; }
所有要访问临界区的进程必须先执行上锁原语,上锁原语顺利通过,则进程可进入临界区;在完成对临界区的访问后,则执行开锁原语,释放该临界资源。
- 上锁与开锁
- 信号量机制
信号量机制中申请和释放临界资源的原语为操作为wait操作和signal操作,也被称为P操作和V操作。信号量:在信号量同步机制中用于实现进程的同步与互斥的有效数据结构。常见的有整型信号量、记录型信号量、AND型信号量及信号量集。
- 整型信号量
整型信号量s表示当前可用的该类临界资源的数量:
s > 0:系统中空闲的该类临界资源的数量
s = 0:系统中临界资源数量为0,且没有进程在等待
s < 0:s的绝对值表示系统中等待的进程的数量
wait(s): while (s <= 0) { 该进程等待; s--; } signal(s): s++;
- 记录型信号量
记录型信号量数据结构为:
struct semaphore{ int value; struct PCB *queue; };
value的值表示系统可用的临界资源的数量,而queue为进程链表指针,指向等待该类资源的PCB队列。
记录型信号量的原语操作为:
semaphore S; //wait操作 wait(S) { S.value--; if (S.value >= 0) { 本进程申请到资源,继续执行; } else{ 调用block原语,将本进程加入阻塞队列,保护CPU现场,释放处理机资源; 转进程调度; } } //signal操作 signal(S) { S.value++; if (S.value <= 0) {//说明PCB队列还有其它进程在等待 在queue唤醒一个阻塞态进程; } 释放本进程占用的该临界资源,继续执行; }
- 整型信号量
- 进程同步问题举例
- 例7.1 同步使用打印机
semaphore s; s.value = 1; void main(){ parbegin(p1,p2,p3,..,pn); } pi(){ //i = 1,2,3,...,n wait(s); 打印; signal(s); }
- 例7.2 有一个缓存区,供多个进程共享,这些进程中有读进程和写进程。写一个多个进程使用同一个缓存区实现进程同步的程序。
semaphore empty,full; empty.value = 1; //empty用来表示缓存区是否是已被写入的状态 full.value = 0; //full表示缓存区是否是已被读出的状态。 reader() { while (true) { wait(full); //读之前先使full置于value为0的状态,避免其它进程也进入读取 读缓存区; signal(empty); //读完后置empty的value为0,表示缓存区的数据已经读完了,可以继续写入了。 } } writer() { while (true) { wait(empty); 写缓存区; signal(full); } } //通过设置两个信号量实现了writer和reader的交替执行 void main() { prebegin(writer,reader); }
- 例7.3 生产者-消费者问题:生产者进程生产产品,放入容量为n的缓存区供消费者拿走。消费者不能上空缓存区拿东西,生产者不能上满的缓存区放东西。这个与上个文件读写的例子有些类似。
/* 用一个数组表示具有n个缓存区的缓存池。设有输入指针in指向一个可存放产品的缓存区,输出指针out指向可取得产品的缓存区。由于数组可以循环放置,所以当输入或输出加一时,可表示为: in = (in + 1) % n, out = (out + 1) % n。当(in + 1) % n = out时,表示缓存池已满,当in = out时,表示缓存池已空。整型counter表示缓存池中满缓存区的数量。 还有以下变量: mutex:互斥使用缓存池信号量,初值mutex = 1 empty:空缓存区的信号量 full:满缓存区的信号量 */ semaphore mutex, empty, full; mutex.value = 1; empty.value = n; full.value = 0; product buffer[n]; int in = 0,out = 0; void main() { prebegin(producer, consumer); } void producer() { while (true) { 生产一件产品; wait(empty); wait(mutex); 将产品放入缓存区; counter++; in = (in + 1) % n; signal(mutex); signal(full); } } void consumer() { while (true) { wait(full); wait(mutex); 拿走一件产品; counter--; out = (out + 1) % n; signal(mutex); signal(empty); } } //注意:在这两个进程中两个wait()信号量的顺序不能改变,必须先要申请到full资源或empty资源才能继续申请mutex的资源,否则可能会造成申请到mutex的资源但却申请不到full或empty的资源的情况,然后进程就会进入等待状态,使得signal(mutex)不能执行,造成缓存池的锁死。
这个问题比较有代表性,一般把系统中使用同一类资源的进程称为该资源的消费者,释放该类资源的进程称为生产者。
- 例7.4 读者-写者问题:文件F可以被多个进程共享,向F中写的称为写进程,读取F的称为读进程。用wait和signal解决进程间的同步问题。
//F可以同时被多个进程读,但不能被多个进程同时写,否则会造成数据的混乱.或者说一个写进程会对其他所有进程造成排斥。 semaphore wmutex,rmutex; wmutex.value = 1; rmutex.value = 1; int readcount = 0; //正在进行读操作的进程数量,因为readcount是一个会被多个读进程访问的资源,所以上文设置了rmutex来控制访问。 void main() { prebegin(writer,reader); } void writer() { while (true) { wait(wmutex); 写操作; signal(wmutex); } } void reader() { while (true) { wait(rmutex); //要想访问readcount必须先申请资源。 if (readcount == 0) { //如果没有同类型进程在访问该资源,则需要进行申请 wait(wmutex); } readcount++; signal(rmutex); 读操作; wait(rmutex); readcount--; if (readcount == 0) signal(wmutex); signal(rmutex); } }
这个问题也是一类问题的典型代表,不同于生产者-消费者问题,这种模式允许任意多个有特定行为的进程进入,但不允许其他类型进入。即同类型进程之间不存在互斥现象。解决这一问题的关键就是当有至少一个同类型的进程正在访问该资源时,本进程便可以不必进行资源申请,直接访问该资源。
- 例7.5 哲学家进餐问题:五个哲学家围着一张餐桌吃饭,桌上有5支筷子,在每个人之间放一支。哲学家只有拿到了左右两支筷子才能进餐,没有拿到则只有等到别人吃完才能拿到。每个哲学家在吃到东西之前不会放下手里的筷子。试描述哲学家吃饭的过程。
假设每一位哲学家拿筷子的方法都是先拿左边的,再拿右边的。则第i位哲学家的拿筷子过程可描述为:
//每一根筷子都是临界资源,为此设立一个筷子信号量数组 semaphore chopstick[5]; //每根筷子的初始值为1,筷子i相邻筷子的索引为(n+i-1)%n 和(i+1)%n。设哲学家i位于筷子i的右边 void main() { prebegin(p1(),p2(),p3(),p4(),p5()); } void pi() { //p表示哲学家的吃饭过程 while (true) { wait(chopstick[i]); wait(chopstick[(i + 1) % n]); 吃饭; signal(chopstick[(i + 1) % n]); signal(chopstick[i]); } }
但是这种做法会造成一个问题,即所有哲学家同时拿起了左边的筷子,导致没有哲学家能拿到右手的筷子。造成了死锁。
解决死锁的方法有多种,如:
1).仅当哲学家的左右手的筷子均可用时才允许拿起筷子
2).最多允许n - 1个哲学家同时去拿左边的筷子,保证总有一个哲学家能够拿到一双筷子。
原文地址:https://www.cnblogs.com/lunar-ubuntu/p/12233515.html