进程同步学习笔记
一、进程同步基本概念*
(一)进程同步定义
-
进程同步定义
- 异步环境下并发进程因直接制约互相发送消息、合作、等待,按一定速度执行的过程,称为进程同步。
- 进程同步机制的主要任务是:在执行次序上对多个协作进程进行协调,使并发执行的诸多协作进程之间能按照一定的规则(或时序)共享系统资源,并能很好地相互合作,从而使程序的执行具有可再现性。
(二)两种制约关系
-
两种制约关系
-
间接相互制约(互斥关系):只允许一个进程运行,多个程序并发执行共享系统资源(如 CPU、I/O 设备)时形成,对像打印机、磁带机等资源需互斥访问,由系统统一分配。
-
直接相互制约(同步关系):在多进程环境下,多个进程为了完成同一任务而相互合作产生的制约关系。
比如在建筑工地上,有搬运工和砌墙工。 **1. 工作流程** - 搬运工负责把砖块搬到砌墙工附近的地方(这就相当于一个 “数据” 提供的过程)。 - 砌墙工则负责用这些砖块砌墙(这相当于对 “数据” 进行处理)。 **2. 同步关系体现** - 当砌墙工附近没有砖块时(相当于 “缓冲区为空”),砌墙工就无法工作,他必须等待搬运工搬来砖块才能继续砌墙。 - 而搬运工也不能无限制地搬运砖块,如果他发现砌墙工附近已经堆满了砖块(相当于 “缓冲区已满”),他就得暂停搬运,等砌墙工用掉一些砖块,腾出空间后,再继续搬运。 这就是一个简单的直接相互制约(同步关系)的例子,两个 “进程”(搬运工和砌墙工)需要相互配合才能完成 “盖房子” 这个任务。 搬运工和砌墙工这两个 “进程” 在某些情况下是可以同时进行的,但也存在相互等待的制约情况。 一、可以同时进行的情况 1. 初始阶段 - 当砌墙工附近有一定量的砖块但还没达到堆满的程度时,搬运工可以继续搬运砖块,同时砌墙工可以用现有的砖块砌墙。 - 例如,一开始砌墙工附近有 10 块砖,砌墙工开始砌墙,同时搬运工继续搬砖过来。 2. 动态平衡阶段 - 当搬运工搬运砖块的速度和砌墙工使用砖块的速度达到一种动态平衡时,两者可以同时进行。 - 比如,搬运工每分钟搬来 2 块砖,砌墙工每分钟用掉 2 块砖,这样他们可以在一段时间内同时工作。 二、需要等待不能同时进行的情况 1. 砖块用完时 - 当砌墙工附近的砖块全部用完(相当于缓冲区为空),砌墙工就必须停下来等待搬运工搬来新的砖块,此时只有搬运工能进行工作。 2. 砖块堆满时 - 当砌墙工附近堆满了砖块(相当于缓冲区已满),搬运工就必须停下来等待砌墙工用掉一些砖块腾出空间,此时只有砌墙工能进行工作。
总体而言,直接相互制约(同步关系)下的进程通常不能完全随意地同时进行,它们的执行顺序需要按照一定的规则进行协调,不过在特定条件下可能存在局部的并行操作。
-
(三)临界资源
-
临界资源
-
临界资源(互斥资源):一次只允许一个进程访问,进程使用时需互斥访问的资源,如打印机、共享变量、文件等。
-
生产者—消费者问题:互斥(缓冲区在某一时刻只能存或取)与同步(生产者存入东西,消费者取东西)同时存在
-
问题描述:
生产者 - 消费者(producer - consumer)问题是一个著名的进程同步问题。它描述的是:有一组生产者进程在生产产品,并将这些产品提供给一组消费者进程去消费。为使生产者进程与消费者进程能够并发执行,在两者之间设置了一个具有 n 个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区中;消费者进程可从一个缓冲区中取走产品并进行消费。尽管所有的生产者进程和消费者进程都是以异步方式运行的,但它们之间必须保持同步,即不允许消费者进程到一个空缓冲区中去取产品;也不允许生产者进程向一个已装满产品且产品尚未被取走的缓冲区投放产品。
-
问题分析:
用数组 buffer 表示有 n 个缓冲区的缓冲池,投入或取出产品时对应指针 in 或 out 加 1(循环缓冲,加 1 需取模 n),(in + 1) % n = out 表示缓冲池满,in = out 表示缓冲池空,还引入初值为 0 的整型变量 counter,生产者投入或取走产品时 counter 相应加 1 或减 1。
生产者进程和消费者进程共享下列变量:
int in=0,out=0,counter=0 item buffer[n]; #定义了一个名为buffer的数组,数组元素类型为item(这里item可能是某种自定义的数据类型,用于表示产品),数组大小为n。这个数组用于模拟生产者 - 消费者问题中的缓冲区,用来存放生产者生产的产品,供消费者消费。
-
-
* 代码实现
指针 in 和 out 初始化为 0。在生产者进程中使用一个局部变量 `nextp`,用于暂时存放每次刚生产出来的产品;而在消费者进程中,则使用一个局部变量 `nextc`,用于存放每次要消费的产品。
下列生产者程序和消费者程序,虽然分开看时都是正确的,而且两者在顺序执行时结果也是正确的,但若并发执行,就会出现差错,问题在于这两个进程共享变量 `counter`。生产者对它做加 1 操作`counter++`,消费者对它做减 1 操作`counter--`
* 生产者
```
void producer() {
while(1) {
// 生产一个产品放在nextp中
produce an item in nextp ;
// 如果缓冲区满,则等待
while (counter == n);
// 将产品放入缓冲区
buffer[ in ] = nextp ;
// 更新输入指针in,实现循环缓冲
in = (in + 1) % n ;
// 缓冲区中的产品数量加1
counter++;
}
}
```
- **`while(1)`循环**:这是一个无限循环,代表生产者会持续生产产品。
- **生产产品**:`produce an item in nextp ;`这里表示生产一个产品,并将其存放在`nextp`变量中。实际代码中可能会有更具体的生产逻辑。
- **缓冲区满的等待机制**:`while (counter == n);`用于检查缓冲区是否已满。如果`counter`(缓冲区中的产品数量)等于`n`(缓冲区大小),生产者将在此循环中等待,直到缓冲区有空闲位置。
- 将产品放入缓冲区:
- `buffer[ in ] = nextp ;`将生产好的产品放入缓冲区`buffer`中,放入位置由`in`指针决定。
- `in = (in + 1) % n ;`更新`in`指针,使其指向下一个可用的缓冲区位置。这里通过取模运算实现循环缓冲区。
- `counter++;`增加`counter`的值,表示缓冲区中的产品数量增加了 1。
* 消费者
```
void consumer() {
while(1) {
// 如果缓冲区空,则等待
while (counter == 0);
// 从缓冲区取出产品
nextc = buffer[ out ];
// 更新输出指针out,实现循环缓冲
out = (out + 1) % n ;
// 缓冲区中的产品数量减1
counter--;
// 消费取出的产品
consumer the item in nextc ;
}
}
```
- **`while(1)`循环**:这也是一个无限循环,表示消费者会持续消费产品。
- **缓冲区空的等待机制**:`while (counter == 0);`用于检查缓冲区是否为空。如果`counter`等于 0,消费者将在此循环中等待,直到缓冲区中有产品。
- 从缓冲区取出产品:
- `nextc = buffer[ out ];`从缓冲区`buffer`中取出产品,取出位置由`out`指针决定,并将取出的产品存放在`nextc`变量中。
- `out = (out + 1) % n ;`更新`out`指针,使其指向下一个可用的缓冲区位置。
- `counter--;`减少`counter`的值,表示缓冲区中的产品数量减少了 1。
- `consumer the item in nextc ;`消费从缓冲区取出的产品,实际代码中可能会有更具体的消费逻辑。
(四)临界区
-
临界区
- 临界资源与临界区
- 无论是硬件还是软件临界资源,多个进程必须互斥地对其进行访问。在每个进程中访问临界资源的那段代码被称为临界区。
- 进程访问临界区的机制
- 为了保证各进程互斥地进入自己的临界区,实现对临界资源的互斥访问,每个进程在进入临界区之前,需要先检查欲访问的临界资源是否正在被其他进程访问。
- 如果该临界资源未被访问,进程可以进入临界区进行访问,并设置访问标志为 “正被访问”;如果该临界资源正在被其他进程访问,本进程则不能进入临界区。
- 进入区、退出区和剩余区
- 在临界区前面用于检查临界资源是否被访问的代码段称为进入区(entry section)。
- 在临界区后面用于将临界区正被访问的标志恢复为未被访问标志的代码段称为退出区(exit section)。
- 进程中除进入区、临界区和退出区之外的其他部分的代码被称为剩余区。
即可将一个访问临界资源的循环进程描述如下:
while (TRUE) { entry section; // 进入区 critical section; // 临界区 exit section; // 退出区 剩余区; // 剩余区(未明确写出代码) }
为实现进程互斥地进入自己的临界区,可使用软件方法,更多的情况是在系统中设置专门的同步机构来协调各进程间的运行。解决临界区问题的同步机制都应遵循下述 4 条准则:
- 临界资源与临界区
(五)方法与准则
一、同步机制实现方法
- 软件方法
- 可以使用软件方法实现进程互斥地进入临界区。
- 通过程序设计逻辑来实现进程同步,不依赖特殊硬件。
- 硬件方法
- 利用计算机硬件提供的特殊指令或机制来实现同步。
- 信息量方法(信号量方法)
- 基于信号量(Semaphore)机制,信号量是一个整型变量,通过 P 操作(wait 操作)和 V 操作(signal 操作)来控制进程对资源的访问。
- 管理方法(管程方法)
- 将共享资源和操作过程封装在管程模块中,外部进程通过调用管程过程访问资源,内部使用条件变量实现进程等待和唤醒。
二、同步机制的 4 条准则
- 空闲让进
- 当没有进程处于临界区时,表明临界资源空闲,此时应允许一个请求进入临界区的进程立即进入,以有效利用临界资源。
- 忙则等待
- 当已有进程进入临界区时,表明临界资源正在被访问,其他试图进入临界区的进程必须等待,以保证对临界资源的互斥访问。
- 有限等待
- 对于要访问临界资源的进程,要保证它能在有限时间内进入自己的临界区,避免陷入 “死等” 状态。
- 让权等待(非必须)
- 当进程不能进入自己的临界区时,原则上应立即释放处理机,避免进程陷入 “忙等” 状态,但这条准则并非强制要求。
二、软件同步机制
三、硬件同步机制
四、信号量机制*
又称PV操作
由荷兰学者迪科斯彻(Dijkstra)在 1965 年提出的一种进程同步机制,其中 P、V 分别代表荷兰语的 Proberen(test)和 Verhogen(increment)。
信号量 - 软件解决方案:
- 保证两个或多个代码段不被并发调用
- 在进入关键代码段前,进程必须获取一个信号量,否则不能运行
- 执行完该关键代码段,必须释放信号量
- 信号量有值,为正说明它空闲,为负说明其忙碌
(一)信号量类型:
- 整型信号量
- 记录型信号量
- AND 型信号量
- 信号量集
(二)信号量机制介绍
-
整型信号量
信号量 S - 整型变量:提供两个不可分割的 [原子操作] 访问信号量。
wait(S): #申请资源(进入区) while s<=0 ; /*do no-op*/ s:=s-1; signal(S): #释放资源(退出区) s:=s+1;
wait (S) 又称为 P (S)。
signal (S) 又称为 V (S)。
缺点:进程忙等。
进程忙等(Busy Waiting),也叫忙式等待,是指当一个进程在申请某个资源(例如想进入临界区访问共享资源)但暂时无法获取到该资源时,它不会主动放弃 CPU 进入阻塞状态,而是不停地循环检测该资源是否可用,也就是一直在占用 CPU 资源不断地去执行检测的指令。就好像一个人一直在原地打转,反复查看自己能不能做某件事一样,在此期间它持续占用着 CPU 时间片,而没有去做其他有意义的工作。
-
记录型信号量:去除忙等的信号量
每个信号量 S 除一个整数值 S.value 外,还有一个进程等待队列 S.list,存放阻塞在该信号量的各个进程 PCB
typedef struct { int value; struct process_control_block *list; } semaphore;
-
信号量只能通过初始化和**两个标准的原语 PV **来访问 —— 作为 OS 核心代码执行,不受进程调度的打断
-
初始化指定一个非负整数值,表示空闲资源总数(又称为 “资源信号量”)—— 若为非负值表示当前的空闲资源数,若为负值其绝对值表示当前等待临界区的进程数
关于
wait
和signal
操作定义wait(semaphores *S) { // 请求一个单位的资源 S->value--; // 资源减少一个 if (S->value<0) block(S->list); // 进程自我阻塞 } signal(semaphores *S) { // 释放一个单位资源 S->value++; // 资源增加一个 if (S->value<=0) wakeup(S->list); // 唤醒等待队列中的一个进程 }
-
-
AND 型信号量:是一种用于解决多个相关临界资源分配问题的信号量机制,进程需要同时获得多个资源才能继续执行。
-
信号量集:是对信号量概念的扩展,它允许进程同时对多个信号量进行操作,常用于解决资源分配和进程同步问题。
(三)信号量的应用
利用信号量实现进程互斥:设置互斥信号量
利用信号量实现前趋关系
利用信号量实现进程同步:设置同步信号量
-
利用信号量实现进程互斥
semaphore mutex; mutex = 1; // 初始化为1 while(1) { wait(mutex); 临界区; signal(mutex); 剩余区; }
-
利用信号量实现前驱关系
main() { Semaphore a, b, c, d, e, f, g; a.value = 0; b.value = 0; c.value = 0; d.value = 0; e.value = 0; f.value = 0; g.value = 0; cobegin {S1; signal(a); signal(b);} {wait(a); S2; signal(c); signal(d);} {wait(b); S3; signal(e);} {wait(c); S4; signal(f);} {wait(d); S5; signal(g);} {wait(e); wait(f); wait(g); S6;} coend }
-
利用信号量实现进程同步
-
实现各种同步问题
-
例子:
P1
和P2
需要代码段C1
比C2
先运行semaphores s = 0;
// 主要用于传递消息
-
五、管程机制
六、经典的进程同步问题*
(一)生产者 - 消费者问题
-
生产者 - 消费者问题
-
生产者 - 消费者问题是相互合作进程关系的一种抽象
-
利用记录型信号量实现:
- 假定,在生产者和消费者之间的公用缓冲池中,具有 n 个缓冲区,可利用互斥信号量 mutex 使诸进程实现对缓冲池的互斥使用;
- 利用资源信号量 empty 和 full 分别表示缓冲池中空缓冲区和满缓冲区的数量。
- 又假定这些生产者和消费者相互等效,只要缓冲池未满,生产者便可将消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息。
-
其它解决方案:AND 信号集、管程
-
问题抽象
-
流程
-
互斥分析
-
临界资源分析
-
步骤分析
- 为临界区设置一个信号量
- 设置信号量初值,一般情况下初值为** 1**
- wait () 操作用于临界区前,相当于进入区即申请临界资源
- signal () 操作用于临界区后,相当于退出区即释放临界资源
- 互斥信号量的 wait ()、signal () 操作必须在同一进程成对出现
-
-
划分临界区
-
增加互斥机制
-
同步分析
两者需要协同的部分
- 生产者:把产品放入指定缓冲区(关键代码 C1)
- 消费者:从满缓冲区取出一个产品(关键代码 C2)
三种运行次序(不同条件下不同运行次序)
- 所有缓冲区空时:
先C1后C2
- 所有缓冲区满时:
先C2后C1
- 缓冲区有空也有满时:
C1C2无先后顺序
-
算法描述
-
同步信号量定义
共享数据
semaphore *full, *empty, *m; //full: 满缓冲区数量 empty: 空缓冲区数量 初始化: full->value = 0; empty->value = N; m->value = 1;
-
解决方法
-
(二)哲学家进餐问题
- 哲学家进餐问题
-
问题描述
| 五个哲学家共用一张圆桌。
他们分别坐在五张椅子上。
圆桌上有五个碗和五支筷子。
哲学家们平时在思考。
当感到饥饿时,他们会尝试拿起自己左边和右边最靠近的筷子。
只有当拿到两支筷子时,他们才能进餐。
进餐完毕后,他们放下筷子,继续思考。 | -
解决方案
记录型信号量
AND 信号量集、管程
-
利用记录型信号量解决
Semaphore chopstick[5] = {1,1,1,1,1}; Philosopher i: do { wait(chopstick[i]); // get left chopstick wait(chopstick[(i + 1) % 5]); // get right chopstick ... // eat for awhile ... signal(chopstick[i]); // return left chopstick signal(chopstick[(i + 1) % 5]); // return right chopstick ... // think for awhile ... } while (true)
-
存在问题及解决方案
问题:
可能引起死锁,如五个哲学家同时饥饿而各自拿起左筷子时,会使信号量 chopstick 均为 0;因此他们试图去拿右筷子时,无法拿到而无限期等待。
解决方案:
① 最多允许 4 个哲学家同时坐在桌子周围
② 仅当一个哲学家左右两边的筷子都可用时,才允许他拿筷子。
③ 给所有哲学家编号,奇数号的哲学家必须首先拿左边的筷子,偶数号的哲学家则反之。
-
(三)读者 - 写者问题
- 读者 - 写者问题
-
前提条件
-
**有两组并发进程:**读者和写者,共享一组数据区。
-
要求:
允许多个读者同时执行读操作。
不允许读者、写者同时操作。
不允许多个写者同时操作。 -
分类:
读者优先(第一类读者写者问题)
写者优先(第二类读者写者问题) -
解决方案:
记录型信号量
信号量集
-
-
读者优先(第一类读者写者问题)
-
如果读者来:
- 无读者、写者,新读者可以读。
- 有写者等,但有其它读者正在读,则新读者也可以读。
- 有写者写,新读者等待。
-
如果写者来:
- 无读者,新写者可以写。
- 有读者,新写者等待。
- 有其它写者,新写者等待。
-
代码实现
// 初始化部分 ,互斥信号量mutex,用于保护readcount的访问,初始值为1;信号量w,用于控制写者的访问,初始值为1 semaphore mutex = 1,readcount = 1; // 记录当前读者数量,初始值为0 int readcount = 0; // 读者函数 void reader() { do { // 使用互斥信号量mutex来保护readcount的访问 wait(mutex); // 如果是第一个读者,需要等待写者完成(如果有写者在等待) if (readcount == 0) wait(w); //当第一个读者进入时,使用 wait(w) 来阻止写者进入。 // 读者数量加1 readcount++; // 释放互斥信号量mutex signal(mutex); // 读者进行读操作 ... // 读 ... // 使用互斥信号量mutex来保护readcount的访问 wait(mutex); // 读者数量减1 readcount--; // 如果是最后一个读者,允许写者进行写操作(如果有写者在等待) if (readcount == 0) signal(w); //当最后一个读者离开时,使用 signal(w) 来允许写者进入。 // 释放互斥信号量mutex signal(mutex); } while (TRUE); } // 写者函数 void writer() { do { // 写者等待,确保没有读者或其他写者在操作 wait(w); // 写者进行写操作 ... // 写 ... // 写者完成,释放写者信号量w signal(w); } while (TRUE); }
-
-
写者优先(第二类读者写者问题)
问题描述:
多个读者可以同时进行读
写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
-
(四)总结
-
关于信号量的讨论
-
信号量的使用:
- 信号量必须置一次且只能置一次初值,初值不能为负数。
- 除了初始化,只能通过执行 P、V 操作来访问信号量。
-
使用中存在的问题:
- 死锁
- 饥饿
-
-
死锁和饥饿
-
死锁:
两个或多个进程无限期地等待一个事件的发生,而该事件正是由其中的一个等待进程引起的。
例如:S 和 Q 是两个初值为 1 的信号量P0 P1
P(S); P(Q);
P(Q); P(S);
V(S); V(Q);
V(Q); V(S); -
饥饿:
无限期地阻塞,进程可能永远无法从它等待的信号量队列中移去(只涉及一个进程)。
-
-
互斥分析基本方法
- 查找临界资源
- 划分临界区
- 定义互斥信号量并赋初值
- 在临界区前的进入区加 wait 操作;
退出区加 signal 操作
-
同步分析
- 找出需要同步的代码片段(关键代码)
- 分析这些代码片段的执行次序
- 增加同步信号量并赋初始值
- 在关键代码前后加 wait 和 signal 操作
-
关于 PV 的操作讨论
信号量的物理含义
- S > 0 表示有 S 个资源可用;
- S = 0 表示无资源可用;
- S < 0 则 | S | 表示 S 等待队列中的进程个数。
- P (S):表示申请一个资源;
- V (S) 表示释放一个资源。信号量的初值应该大于等于 0。
PV 操作的使用
- P.V 操作必须成对出现,有一个 P 操作就一定有一个 V 操作。
- 当为互斥操作时,它们同处于同一进程;
- 当为同步操作时,则不在同一进程中出现。
- 如果 P (S1) 和 P (S2) 两个操作在一起,那么 P 操作的顺序至关重要,一个同步 P 操作与一个互斥 P 操作在一起时同步 P 操作在互斥 P 操作前。
- 而两个 V 操作无关紧要。
-
信号量同步的缺点
- 同步操作分散:
- 信号量机制中,同步操作分散在各个进程中,使用不当就可能导致各进程死锁(如 P、V 操作的次序错误、重复或遗漏)。
- 不利于修改和维护:
- 各模块的独立性差,任一组变量或一段代码的修改都可能影响全局。
- 易读性差:
- 要了解对于一组共享变量及信号量的操作是否正确,必须通读整个系统或者并发程序。
- 正确性难以保证:
- 操作系统或并发程序通常很大,很难保证这样一个复杂的系统没有逻辑错误。
- 同步操作分散:
七、Linux 进程同步机制
八、总结
进程同步是现代操作系统并发运行的重要基础,包括基本概念、临界区问题、多种同步机制(软件、硬件、信号量、管程等)和经典同步问题。通过学习,应了解进程同步概念、解决临界区问题原则和管程等,掌握记录型信号量使用方法,能用信号量实现进程互斥与同步。经典同步问题的解答有助于理解实际编程中的同步问题,提高逻辑思维和实践编程能力。
一、简答题
- 什么是临界资源?什么是临界区?
- 临界资源是一次仅允许一个进程使用的资源,如打印机、共享变量等。
- 临界区是每个进程中访问临界资源的那段代码。
- 同步机制应遵循的准则有哪些?
- 空闲让进:当无进程处于临界区时,允许一个请求进入临界区的进程立即进入。
- 忙则等待:当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
- 有限等待:对要求访问临界资源的进程,应保证在有限时间内进入临界区。
- 让权等待:当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。
- 为什么各进程对临界资源的访问必须互斥?
- 若多个进程同时访问和修改临界资源,会导致数据不一致、程序执行结果错误等问题。例如多个进程同时对一个共享变量进行写操作,会使变量的值出现不可预测的结果。
- 如何保证各进程互斥地访问临界资源?
- 可以采用软件方法(如 Peterson 算法等)或硬件方法(如中断屏蔽法、硬件指令法)以及信号量机制等。例如信号量机制通过 P 操作(wait 操作)和 V 操作(signal 操作)来控制进程对临界资源的访问,当一个进程要进入临界区时,先执行 P 操作,如果信号量的值大于等于 1,则该进程可以进入临界区,同时信号量的值减 1;当进程退出临界区时,执行 V 操作,信号量的值加 1
- 何谓 “忙等”?它有什么缺点?
- “忙等” 是指进程在得不到临界资源时,不释放处理机,而是不断地循环测试等待。
- 缺点:浪费 CPU 时间,因为在等待期间,CPU 一直被占用但却没有做有效的工作;可能导致优先级反转等问题,即低优先级进程占用 CPU 导致高优先级进程无法及时执行。
- 试述采用 Peterson 算法实现临界区互斥的原理。
- 设有两个进程 P0 和 P1,共享两个变量:布尔变量 flag [2] 和整型变量 turn。
- 初始时,flag [0] = flag [1]= false,turn 可以为 0 或 1。
- 进程 Pi (i = 0,1) 进入临界区的算法如下:
- flag [i]= true; // 表示 Pi 准备进入临界区
- turn = j; //j 是另一个进程,即 1 - i
- while (flag [j]&& turn == j); // 循环等待,直到 Pj 不准备进入临界区或者轮到 Pi 进入
- // 临界区代码
- flag [i]= false; // 退出临界区后,将自己的准备标志置为 false
- 哪些软件方法可以解决进程互斥问题?简述它们的用法。
- Dekker 算法:
- 设有两个进程 P0 和 P1,共享变量 flag [0]、flag [1] 和 turn。
- flag [i] 表示进程 Pi 是否有进入临界区的意愿,初始为 false。
- turn 表示当前轮到哪个进程进入临界区。
- 进程 Pi (i = 0,1) 进入临界区的步骤:
- 首先设置 flag [i]= true。
- 然后检查 flag [1 - i],如果 flag [1 - i] 为 true,则根据 turn 的值来决定是否等待。如果 turn == 1 - i,则 Pi 等待,直到 flag [1 - i] 变为 false 或者 turn 变为 i;如果 turn == i,则 Pi 可以进入临界区。
- 退出临界区时,设置 flag [i]= false。
- Peterson 算法(前面已详细说明)。
- Dekker 算法:
- (考研真题) 如果用于进程同步的信号量的 P、V 操作不用原语实现,会产生什么后果?举例说明。
- 如果 P、V 操作不用原语实现,在多进程环境下可能会出现进程并发执行时对信号量操作的混乱。
- 例如,在执行 P 操作时,若不是原子操作,可能出现一个进程在检查信号量值大于 0 后还未来得及将信号量值减 1 时,另一个进程也进行了同样的检查,导致两个进程都认为可以进入临界区,破坏了互斥性。
- AND 信号量机制的基本思想是什么?它能解决什么问题?
- 基本思想:将进程在整个运行过程中需要的所有资源,一次性全部分配给进程,待进程使用完后再一起释放。
- 解决的问题:可以防止出现死锁现象,因为避免了进程占有部分资源等待其他资源的情况。例如,进程 P 需要资源 A 和 B 才能执行任务,AND 信号量机制会同时检查 A 和 B 是否可用,如果都可用则分配给 P,若有一个不可用则 P 等待。
- 利用信号量机制实现进程互斥时,针对互斥信号量的 wait () 和 signal () 操作作为什么要成对出现?
- wait () 操作用于请求资源,当执行 wait () 操作时,如果资源可用(信号量值大于等于 1),则进程获取资源,信号量值减 1;如果资源不可用(信号量值为 0),则进程等待。
- signal () 操作用于释放资源,当执行 signal () 操作时,信号量值加 1,如果有进程在等待该资源(即信号量值加 1 后大于 0),则唤醒等待的进程。
- 如果不成对出现,可能导致资源无法正确分配和释放,例如只执行了 wait () 操作没有执行 signal () 操作,会导致资源一直被占用,其他需要该资源的进程永远等待;若只执行了 signal () 操作没有执行 wait () 操作,可能会导致资源分配错误,多个进程可能同时进入临界区破坏互斥性。
- 什么是进程?它有哪些特性?
- 进程是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
- 特性:
- 动态性:进程是程序的一次执行过程,有其生命周期,包括创建、运行、阻塞、唤醒、终止等状态。
- 并发性:多个进程可以在一段时间内同时执行。
- 独立性:进程是系统分配资源和调度的独立单位,每个进程都有自己独立的运行环境,包括程序、数据和进程控制块(PCB)。
- 异步性:各个进程按各自独立的、不可预知的速度向前推进。
- 试简述进程管理中条件变量的含义与作用。
- 含义:条件变量是用于进程同步的一种机制,它通常与互斥锁一起使用。
- 作用:当一个进程需要等待某个条件满足时,它可以在条件变量上等待(通常会释放互斥锁),当另一个进程使得该条件满足时,可以通过操作条件变量来唤醒等待的进程。例如在生产者 - 消费者问题中,消费者在缓冲区为空时会在条件变量上等待,生产者在生产了产品放入缓冲区后会唤醒等待的消费者。
二、计算题
- 若信号量的初值为 2,当前值为 - 1,则表示有多少个等待进程?请分析。
- 根据信号量的物理意义,信号量的值 S 表示当前可用资源的数量。
- 当 S < 0 时,|S | 表示等待资源的进程数量。
- 已知当前值 S = - 1,所以等待进程数量为 1。
- 有 m 个进程共享同一临界资源,若使用信号量机制实现对某个临界资源的互斥访问,请求出信号量的变化范围。
- 设信号量为 S。
- 初始时,没有进程进入临界区,S = 1(表示临界资源可用)。
- 当所有 m 个进程都请求进入临界区且都在等待时,S = 1 - m(此时有 m 个进程等待)。
- 所以信号量 S 的变化范围是 [1 - m, 1]。
- 若有 4 个进程共享同一程序段,而且每次最多允许 3 个进程进入该程序段,则信号量值的变化范围是什么?
- 设信号量为 S。
- 初始时,没有进程进入程序段,S = 3(因为最多允许 3 个进程进入)。
- 当 4 个进程都请求进入且 3 个进程已经进入,1 个进程在等待时,S = 3 - 3 = 0(3 个进程进入,信号量值减 3)。
- 当 4 个进程都请求进入且都在等待时,S = 3 - 4 = - 1。
- 所以信号量值的变化范围是 [- 1, 3]。
三、综合应用题
- (考研真题) 3 个进程 P1、P2、P3 互斥地使用一个包含 N (N>0) 个单元的缓冲区。P1 每次用 produce () 生成一个正整数,并用 put () 将其送入缓冲区的某一空闲单元中;P2 每次用 getodd () 从该缓冲区中取出一个奇数,并用 countodd () 统计奇数的个数;P3 每次用 geteven () 从该缓冲区中取出一个偶数,并用 counteven () 统计偶数的个数。请用信号量机制实现这 3 个进程的同步与互斥活动,并说明所定义的信号量的含义。要求用伪代码描述。
-
定义信号量:
- mutex:互斥信号量,初值为 1,用于保证对缓冲区的互斥访问。
- empty:表示缓冲区中空闲单元的数量,初值为 N。
- odd:表示缓冲区中奇数的数量,初值为 0。
- even:表示缓冲区中偶数的数量,初值为 0。
-
伪代码如下:
-
进程 P1:
while (TRUE) { int num = produce(); P(empty); P(mutex); put(num); if (num % 2 == 0) { V(even); } else { V(odd); } V(mutex); }
-
-
进程 P2:
while (TRUE) { P(odd); P(mutex); int num = getodd(); countodd++; V(mutex); V(empty); }
-
进程 P3:
while (TRUE) { P(even); P(mutex); int num = geteven(); counteven++; V(mutex); V(empty); }
-
(考研真题) 某银行提供了 1 个服务窗口和 10 个顾客等待时使用的座位。顾客到达银行时,若有空座位,则到取号机上领取一个号,等待叫号。取号机每次仅允许一位顾客使用。当营业员空闲时,通过叫号选取一位顾客,并为其服务。顾客和营业员的活动过程描述如下。
cobegin{ process 顾客 { 从取号机上获得一个号码; 等待叫号; 获得服务; } process 营业员 { while (TRUE) { 叫号; 为顾客服务; } } }coend
请添加必要的信号量和 P、V 操作或 wait ()、signal () 操作,实现上述过程中的互斥与同步。要求写出完整的过程,说明信号量的含义并赋初值。
-
定义信号量:
- mutex:互斥信号量,用于对取号机的互斥操作,初值为 1。
- empty:表示空座位数量,初值为 10。
- full:表示等待服务的顾客数量,初值为 0。
- service:用于营业员叫号服务,初值为 0。
-
顾客进程:
process 顾客{ P(empty); P(mutex); 从取号机上获得一个号码; V(mutex); V(full); P(service); 获得服务; }
-
营业员进程:
process 营业员{ while(TRUE) { P(full); 叫号; V(service); 为顾客服务; } }
- 如图下列过程所示,有 1 个计算进程和 1 个打印进程,它们共享一个单缓冲区,计算进程不断计算出一个整型结果,并将它放入单缓冲区中;打印进程负责从单缓冲区中取出每个结果并进行打印。请用信号量机制实现它们的同步关系。
计算进程→单缓冲区→打印进程
-
定义信号量:
- empty:表示缓冲区是否为空,初值为 1,表示初始时缓冲区为空。
- full:表示缓冲区是否有数据,初值为 0,表示初始时缓冲区无数据。
-
计算进程:
process 计算进程{ while (TRUE) { int result = 计算结果(); P(empty); 将结果放入单缓冲区; V(full); } }
-
打印进程:
process 打印进程{ while (TRUE) { P(full); 从单缓冲区取出结果并打印; V(empty); } }
- 有 3 个进程 P1、P2、P3 协作解决文件打印问题。P1 将文件记录从磁盘读入内存的缓冲区 1,每执行一次读一个记录;P2 将缓冲区 1 中的内容复制到缓冲区 2 中,每执行一次复制一个记录;P3 将缓冲区 2 中的内容打印出来,每执行一次打印一个记录。缓冲区的大小与记录大小一样。请用信号量来保证文件的正确打印。
-
定义信号量:
- empty1:表示缓冲区 1 是否为空,初值为 1。
- full1:表示缓冲区 1 是否有数据,初值为 0。
- empty2:表示缓冲区 2 是否为空,初值为 1。
- full2:表示缓冲区 2 是否有数据,初值为 0。
-
进程 P1:
process P1{ while (TRUE) { 从磁盘读入一个记录到缓冲区1; P(empty1); 将记录放入缓冲区1; V(full1); } }
-
进程 P2:
process P2{ while (TRUE) { P(full1); 从缓冲区1取出记录; V(empty1); P(empty2); 将记录复制到缓冲区2; V(full2); } }
-
进程 P3:
process P3{ while (TRUE) { P(full2); 从缓冲区2取出记录并打印; V(empty2); } }
- 桌上有一个能盛得下 5 个水果的空盘子。爸爸不停地向盘中放苹果和桔子,儿子不停地从盘中取出桔子享用,女儿不停地从盘中取出苹果享用。规定 3 人不能同时向 (从) 盘子中放 (取) 水果。试用信号量机制来实现爸爸、儿子、女儿这 3 个 “循环进程” 之间的同步。
-
定义信号量:
- mutex:互斥信号量,用于对盘子操作的互斥,初值为 1。
- apple:表示盘子中苹果的数量,初值为 0。
- orange:表示盘子中桔子的数量,初值为 0。
-
爸爸进程:
process 爸爸{ while (TRUE) { P(mutex); if (盘子中水果数量<5) { 放苹果或桔子到盘子; if (放的是苹果) { V(apple); } else { V(orange); } } V(mutex); } }
-
儿子进程:
process 儿子{ while (TRUE) { P(orange); P(mutex); 从盘子中取桔子; V(mutex); } }
-
女儿进程:
process 女儿{ while (TRUE) { P(apple); P(mutex); 从盘子中取苹果; V(mutex); } }
- 试用记录型信号量写出一个不会死锁的哲学家进餐问题的算法。
-
定义信号量:
- mutex:互斥信号量,用于对取筷子操作的互斥,初值为 1。
- chopstick [5]:表示 5 根筷子,每根筷子对应的信号量初值为 1。
-
哲学家 i(i = 0,1,2,3,4)的进程:
process 哲学家i{ while (TRUE) { P(mutex); P(chopstick[i]); P(chopstick[(i + 1) % 5]); V(mutex); 进餐; V(chopstick[i]); V(chopstick[(i + 1) % 5]); 思考; } }
用于对盘子操作的互斥,初值为 1。
-
apple:表示盘子中苹果的数量,初值为 0。
-
orange:表示盘子中桔子的数量,初值为 0。
-
爸爸进程:
process 爸爸{ while (TRUE) { P(mutex); if (盘子中水果数量<5) { 放苹果或桔子到盘子; if (放的是苹果) { V(apple); } else { V(orange); } } V(mutex); } }
-
儿子进程:
process 儿子{ while (TRUE) { P(orange); P(mutex); 从盘子中取桔子; V(mutex); } }
-
女儿进程:
process 女儿{ while (TRUE) { P(apple); P(mutex); 从盘子中取苹果; V(mutex); } }
- 试用记录型信号量写出一个不会死锁的哲学家进餐问题的算法。
-
定义信号量:
- mutex:互斥信号量,用于对取筷子操作的互斥,初值为 1。
- chopstick [5]:表示 5 根筷子,每根筷子对应的信号量初值为 1。
-
哲学家 i(i = 0,1,2,3,4)的进程:
process 哲学家i{ while (TRUE) { P(mutex); P(chopstick[i]); P(chopstick[(i + 1) % 5]); V(mutex); 进餐; V(chopstick[i]); V(chopstick[(i + 1) % 5]); 思考; } }