OS(四):进程管理之 进程的同步
进程同步的主要任务是对多个相关进程在执行次序上进行协调,以使并发执行的诸进程之间能有效的共享资源、相互合作,使程序的执行具有可再现性。
1、进程同步的基本概念
1.1、进程间的制约关系
OS中的进程存在两种形式的制约关系。
一种是源于进程间的合作的 直接相互制约关系,例如管道通信方式,写入共享文件(Pipe)的进程进行write操作时,读进程阻塞;反之,读进程进行read操作时,写进程阻塞;
一种是同一个系统中的进程,共享某种系统资源的简接相互制约关系,进程排它性地访问共享资源。
1.2、临界资源与临界区
1.2.1、临界资源
共享资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。
常见临界资源:输入机、打印机、磁带机;消息缓冲队列、变量、数组、缓冲区。
访问方式:进程间应采取『互斥』方式,实现对临界资源的访问。
几个进程若共享同一临界资源,它们必须以『互斥』的方式使用这个临界资源,即系统中同时存在有许多进程,它们共享各种资源,然而有些资源每次只能让一个进程所使用。
1.2.2、临界区
每个进程中访问临界资源的代码被称为临界区(critical section)。若能保证诸进程互斥地进入临界区,便可实现诸进程对临界资源的互斥访问。
临界区访问过程:
1 repeat 2 entry section; // 进入区 3 critical section; // 临界区 4 exit section; // 退出区 5 remainder section; // 剩余区 6 until false;
1、进入区:进入临界区前检查权限,否则等待/阻塞;设置正在访问临界区的标识,成功则加锁(lock)
2、临界区:访问临界资源的代码
3、退出区:将正在访问临界区的标识清除,解锁(unlock),唤醒其它阻塞进程
4、剩余区:代码剩余部分
1.3、同步机制应遵循的规则
互斥机制下临界区访问原则:
1.3.1、空闲让进:临界区空闲
可以允许一个进程进入临界区
1.3.2、忙则等待
已有进程进入临界区,其它试图进入的进程应等待
1.3.3、有限等待
确保等待的进程,进入临界区前的等待时间有限
1.3.4、让权等待
不能进入临界区时,应让出CPU执行权,防止忙等待(即占用CPU执行权的等待)
2、信号量机制
2.1. 整型信号量
在记录型信号量机制中的wait操作,只要是信号量S<=0,就会不断地测试。
违背“让权等待”,会发生忙等。
1 // 整型信号量,表示可用资源数 2 int S = 1; 3 // wait原语,相当于进入区 4 void wait(int S) { 5 // 资源不够,循环等待 6 while (S <= 0); 7 S = S - 1; 8 } 9 // signal原语,相当于退出区 10 void signal(int S) { 11 S = S + 1; 12 }
wait(S)和signal(S)是原子操作,执行时不可中断。当一个线程在修改某信号量时,其他进程不可对该信号量进行修改。
1 // 进程Pn 2 ... 3 wait(S); // 进入区,申请打印机 4 访问共享资源; // 临界区,访问打印机 5 signal(S); // 退出区,释放打印机 6 ...
PV操作:P操作:wait原语,进程等待;V操作:signal原语,唤醒等待进程。
2.2. 记录型信号量
进程进入阻塞状态,不会忙等,采取了"让权等待"的策略。
1 // 记录型信号量定义 2 typedef struct { 3 int value; // 剩余资源数量 4 struct process *L; // 进程等待队列 5 } semaphore; 6 void wait(semaphore S) { // 申请资源 7 S.value--; 8 if (S.value < 0) { 9 // block原语阻塞进程 10 block(S.L); 11 } 12 } 13 void signal(semaphore S) { // 释放资源 14 S.value++; 15 if (S.value <= 0) { 16 // wakeup原语唤醒进程 17 wakeup(S.L); 18 } 19 }
2.3. AND型信号量
AND型信号量并不是针对各进程之间只共享一个临界资源,而是一个进程需要先获得两个或更多的共享资源后才能执行其他任务。
AND同步机制:要么把它所请求的资源全部分配给进程,要么一个也不分配。
2.4、信号量的应用
1. 利用信号量实现同步
2. 利用信号量实现进程互斥
3. 利用信号量实现前驱关系
3、管程机制
3.1、管程要解决的问题
信号量机制虽然是方便有效的进程同步机制,但每个访问邻接资源的进程都必须自备同步操作,使大量的同步操作分散在各个进程中,会给系统的管理带来麻烦,也会因为同步操作的使用不当造成系统死锁。
为解决上述问题,产生了新的进程同步工具 - 管程。
3.2、管程的定义
管程 - 进程同步工具。
管程代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,共同构成了一个操作系统的资源管理模块。
管程被请求和释放资源的进程所调用。
3.3、管程的组成
管程名称
局部于管程内部的共享数据结构
对该数据结构操作的一组过程(函数)
局部于管程内共享数据设置初始值的语句
3.4. 管程的基本特性
模块化:是一个模块化的基本程序单位,可以单独编译
抽象数据类型:是一种抽象数据类型,包含数据和操作
信息掩蔽:共享数据只能被管程内的过程访问
3.5、管程与进程的异同
1、都定义了数据结构,进程定义的是私有数据结构PCB、管理定义的公共数据结构,如消息队列;
2、都存在对数据结构的操作,进程由顺序程序执行相关的操作,管程主要是进行同步操作和初始化操作;
3、设置进程的目的在于实现系统并发性,管程的设置是解决共享资源的互斥使用问题;
4、进程通过调用管程中的过程对共享数据结构实行操作,管程是被动工作方式,进程是主动工作方式;
5、进程之间能并发执行,管程不能并发执行;
6、进程具有动态性,由创建而生,由撤销而亡;管程是操作系统的一个资源管理模块,供进程调用。
3.6. 条件变量/条件对象(condition)
3.6.1、条件变量的概述
条件变量是一种抽象数据类型,每个条件变量保存了一个链表,用于记录因该条件变量而阻塞的所有进程。
对条件变量的操作作用于 wait和signal 原语。
声明条件变量,形式为:
Var x , y : condition
x.wait:正在调动管程的进程因 x 条件需要被阻塞或挂起,则调用 x.wait 将自己插入到 x 条件的等待队列上,并释放管程,直到 x 条件变化。此时 其他进程可以使用该管程。
x.signal:正在调用管程的进程发现 x 条件发生变化,则调用 x.signal,重新启动一个因 x 条件而阻塞或挂起的进程。
3.6.2、总结
进入管程的进程可能由于条件不满足而阻塞
此时该进程释放管程以便其它进程调用管程
进程被阻塞的条件(原因)有多个,移入不同的条件队列
进程被移入条件队列后,应释放管程
4、经典同步问题
4.1. 生产者消费者问题
一组生产者进程和一组消费者进程
它们共享大小为n的缓冲区
未满可放,非空可读,否则阻塞
每次只允许一个生产者/消费者放/读,
4.2. 读者写者问题
读者进程和写者两组并发进程,它们共享同一个文件。
要求:
允许多个读者同时读;
只允许一个写者写
任一写者完成前,不可读写
写者操作前,已有读者写者退出
4.3. 哲学家进餐问题
5名哲学家,每2人间有一根筷子,每2根筷子间有一碗米饭,哲学家要么思考,要么进餐
要求:
拿起左右两根筷子才能进餐
一根一根地拿起,若其中任一筷子在他人手上,等待
进餐完成后,放下筷子,继续思考
4.4. 吸烟者问题
三个抽烟者进程,一个供应者进程,抽烟者需要三种材料卷烟:烟草、纸和胶水,三个抽烟者分别拥有烟草、纸、胶水
要求:
1.供应者无限提供三种材料,但每次只提供两种
2.拥有第三种材料的抽烟者卷烟成功并抽掉
3.抽烟成功者给供应者发信号:已完成
供应者收到完成信号后再提供两种材料放到桌子上。