1
0
mirror of https://github.com/Didnelpsun/CS408.git synced 2026-11-02 13:23:47 +08:00

Update 1-process-management.md

This commit is contained in:
Didnelpsun
2022-07-16 23:21:12 +08:00
parent 86274dc23b
commit 3f9ba371b6

View File

@@ -680,7 +680,7 @@ $CPU$繁忙型更接近于长作业,少$I/O$所以少中断。而$I/O$繁忙
3. 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)。
4. 让权等待。当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。
### 进程互斥的软件实现
### 进程互斥软件实现
即在进入区设置并检查一些标志来表明是否有进程在临界区,若有则在进入区通过循环检查进行等待,进程离开临界区后则在退出区修改标志。
@@ -765,7 +765,7 @@ remainder section;
优点:不需要交替进入。
缺点:若按照①⑤②⑥③⑦……的顺序执行,$P_0$和$P_1$将会同时访问临界区,违反忙则等待原则。即在检查对方的$flag$后和切换自己的$flag$前之间有一段时间,结果都会检查通过,检查和修改操作不能一次性进行。
缺点:若按照①⑤②⑥③⑦……的顺序执行,若$P_0$和$P_1$同时检查,发现可以访问,$P_0$和$P_1$将会同时访问临界区,违反忙则等待原则。即在检查对方的$flag$后和切换自己的$flag$前之间有一段时间,结果都会检查通过,检查和修改操作不能一次性进行。
#### 双标志后检查法
@@ -801,13 +801,13 @@ flag[1] = false; // ⑧
remainder section;
```
若进程同时想进入临界区,按照①⑤②⑥③⑦……的顺序执行,则都发现对方标志位是`true`,$P_0$和$P_1$都不能访问临界区。
若进程同时想进入临界区,按照①⑤②⑥③⑦……的顺序执行,则都发现对方标志位是`true`,$P_0$和$P_1$都不能访问临界区都卡死。
因此,双标志后检查法虽然解决了“忙则等待”的问题,但是又违背了“空闲让进”和“有限等待”原则,会因各进程都长期无法访问临界资源而产生“饥饿”现象。
#### Peterson算法
算法思想:双标志后检查法中,两个进程都争着想进入临界区,但是谁也不让谁,最后谁都无法进入临界区。`Gary\,L.Peterson`想到了一种方法,如果双方都争着想进入临界区,那可以让进程尝试“孔融让梨”,主动让对方先使用临界区。每个进程再设置自己的标志后再设置一个变量`turn`不允许进入标志的值,再同时检测另一个进程的状态标志位与不允许进入标志。
算法思想:双标志后检查法中,两个进程都争着想进入临界区,但是谁也不让谁,最后谁都无法进入临界区。$Gary\,L.Peterson$想到了一种方法,如果双方都争着想进入临界区,那可以让进程尝试“孔融让梨”,主动让对方先使用临界区。每个进程再设置自己的标志后再设置一个变量`turn`不允许进入标志的值,再同时检测另一个进程的状态标志位与不允许进入标志。
```cpp
// 表示进入临界区意愿的数组,初始值都是false
@@ -843,7 +843,7 @@ remainder section;
$Peterson$算法用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待的原则。
### 进程互斥的硬件实现
### 进程互斥硬件实现
称为**低级方法**,或**元方法**。
@@ -885,6 +885,7 @@ lock = false; // "解锁""
+ 每个临界资源都有一个共享布尔变量`lock`,`true`表示被占用,`false`表示空闲,是初值。
+ 若刚开始`lock`是`false`,则$TSL$返回的`old`值为`false`,`while`循环条件不满足,直接跳过循环,进入临界区。
+ 若刚开始`lock`是`true`,则执行$TSL$后`old`返回的值为`true`,`while`循环条件满足,会一直循环,直到当前访问临界区的进程在退出区进行“解锁”。
+ $lock$负责唤醒处于就绪态的进程。
相比软件实现方法,$TSL$指令把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作。
@@ -923,15 +924,6 @@ lock = false;
逻辑上来看$Swap$和$TSL$并无太大区别,都是先记录下此时临界区是否已经被上锁(记录在`old`变量上),再将上锁标记`lock`设置为`true`,最后检查`old`,如果`old`为`false`则说明之前没有别的进程对临界区上锁,则可跳出循环,进入临界区。
硬件方法的特点:
+ 优点:
+ 实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞。
+ 适用于多处理机环境
+ 缺点:
+ 不满足“让权等待”原则。
+ 暂时无法进入临界区的进程会占用$CPU$并循环执行$TSL$指令,从而导致“忙等”。
### 信号量
之前软硬件实现的进程互斥都无法解决让权等待问题,所以$Dijkstra$提出实现进程互斥和同步的方法——信号量机制。
@@ -939,7 +931,7 @@ lock = false;
#### 信号量机制的基础概念
+ 用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步。
+ 信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量。
+ 信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为$1$的信号量。
+ 原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的。软件解决方案的主要问题是由“进入区的各种操作无法一气呵成”,因此如果能把进入区、退出区的操作都用“原语”实现,使这些操作能“一气呵成”就能避免问题。
+ 一对原语:`wait(S)`原语和`signal(S)`原语(`S`表示整型量),可以把原语理解为我们自己写的函数,函数名分别为`wait`和`signal`,括号里的信号量`S`其实就是函数调用时传入的一个参数。
+ $wait$、$signal$原语常简称为$P$、$V$操作(来自荷兰语$proberen$和$verhogen$)。因此,做题的时候常把$wait(S)$、$signal(S)$两个操作分别写为$P(S)$、$V(S)$。
@@ -1014,6 +1006,8 @@ void signal (semaphore S) {
#### 信号量机制实现进程互斥
互斥信号量默认设置为$1$。
1. 分析并发进程的关键活动,划定临界区(如对临界资源打印机的访问就应放在临界区)。
2. 设置互斥信号量`mutex`,初值为$1$,表示一次只能有一个进程访问。
3. 互斥信号量取值为$0$或$1$,$0$代表上锁,$1$代表释放。
@@ -1047,6 +1041,8 @@ P2(){
进程同步:要让各并发进程按要求有序地推进。
同步信号量设置应该分为两种情况,如果期望消息未产生则设置为$0$,若已经产生,则应该设为非$0$的正整数。
1. 分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作(或两句代码)。如先$P_1$后$P_2$。
2. 设置同步信号量`S`,初始为$0$。
3. 同步信号量为整数,释放加一,占用减一。
@@ -1154,6 +1150,12 @@ P6() {
}
```
#### 信号量数值
+ $value>0$表示某类可用资源的数量。每次$Р$操作,意味着请求分配一个单位的资源。
+ $value<=0$表示某类资源已经没有,或者说还有因请求该资源而被阻塞的进程。
+ $value<=0$时的绝对值,表示等待进程数目。
### 进程同步与互斥应用
$PV$操作题目分析步骤:
@@ -1171,13 +1173,12 @@ $PV$操作题目分析步骤:
+ 只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。
+ 缓冲区是临界资源,各进程必须互斥地访问。
由缓冲区是临界资源,所以对其访问是互斥关系。
关系分析:
缓冲区满时,生产者要等待消费者取走产品,消费者取必须在生产者放之前,所以是同步关系。
缓冲区空时(即没有产品时),消费者要等待生产者放入产品,消费者取必须在生产者放之后,所以是同步关系。
所以分析得到生产者每次要消耗$P$一个空闲缓冲区,并生产$V$一个产品。消费者每次要消耗$P$一个产品,并释放一个空闲缓冲区$V$。往缓冲区放入/取走产品需要互斥。
+ 由缓冲区是临界资源,所以对其访问是互斥关系。
+ 缓冲区满时,生产者要等待消费者取走产品,消费者取必须在生产者放之前,所以是同步关系。
+ 缓冲区空时(即没有产品时),消费者要等待生产者放入产品,消费者取必须在生产者放之后,所以是同步关系。
+ 所以分析得到一共两个实体缓冲区与产品和两个进程,生产者每次要消耗$P$一个空闲缓冲区,并生产$V$一个产品。消费者每次要消耗$P$一个产品,并释放一个空闲缓冲区$V$。往缓冲区放入/取走产品需要互斥。
所以设置三个变量,不能合并:
@@ -1213,7 +1214,7 @@ consumer() {
}
```
若调换$P$操作顺序会怎么样?($empty$和$full$的$P$操作必然在$mutex$的$P$操作之前)
若调换$P$操作顺序会怎么样?($empty$和$full$的$P$操作必然在$mutex$的$P$操作之前,如果先上锁再获取会有问题)
+ 若此时缓冲区内已经放满产品,则`empty=0`,`full=n`。
+ 则生产者进程执行`P(mutex)`使`mutex`变为`0`上锁,再执行`P(empty)`,由于已没有空闲缓冲区,因此生产者被阻塞。
@@ -1222,7 +1223,7 @@ consumer() {
+ 同样的,若缓冲区中没有产品,即`full=0`,`empty=n`,按先消费者后生产者的顺序执行也会发生死锁。
+ 因此,实现互斥的$P$操作一定要在实现同步的$P$操作之后。可以理解为要先拿到这个空间再上锁,若没有就拿到就上锁,那么其他进程也用不了这个空间。
而由于$V$操作是释放不会导致进程阻塞,所以两个V操作可以交换顺序。
而由于$V$操作是释放不会导致进程阻塞,所以两个$V$操作可以交换顺序。
+ 生产者消费者问题是一个互斥、同步的综合问题。
+ 对于初学者来说最难的是发现题目中隐含的两对同步关系。
@@ -1232,17 +1233,15 @@ consumer() {
桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。用$PV$操作实现上述过程。
所以盘中是一个大小为$1$,初始为空的缓冲区。父母分别为两个生产者进程,子女分别为两个消费者进程,其消费的产品类别不同。
关系分析:
对缓冲区盘子需要互斥使用。
只有父亲放入苹果女儿才能取出,所以是同步关系。
只有母亲放入橘子儿子才能取出,所以是同步关系。
只有盘子空时,父亲或母亲才能放水果,所以也是同步关系。
而儿子女儿之间没有同步和互斥关系,所以不用管。
+ 父母分别为两个生产者进程,子女分别为两个消费者进程。
+ 实体有三个,盘子、苹果、橘子。
+ 盘中是一个大小为$1$,初始为空的缓冲区,对缓冲区盘子需要互斥使用。
+ 只有父亲放入苹果女儿才能取出,所以是同步关系。
+ 只有母亲放入橘子儿子才能取出,所以是同步关系。
+ 只有盘子空时,父亲或母亲才能放水果,所以也是同步关系。
+ 而儿子女儿之间没有同步和互斥关系,所以不用管。
所以对于互斥关系设置一个互斥信号量`mutex=1`,对于苹果设置为`apple=0`,对于橘子设置为`orange=0`,对于向拍子放水果设置`plate=1`(也可以设置为$0$,后面的处理不同)。
@@ -1314,10 +1313,11 @@ son(){
+ 原因在于本题中的缓冲区大小为`1`,在任何时刻,`apple`、`orange`、`plate`三个同步信号量中最多只有一个是$1$。因此在任何时刻,最多只有一个进程的$P$操作不会被阻塞,并顺利地进入临界区,此时互斥关系全部变成同步关系。
+ 而如果缓冲区大于$1$,则父母两个可以同时访问临界区,可能导致数据覆盖,所以必须使用互斥信号量。
+ 解决“多生产者-多消费者问题”的关键在于理清复杂的同步关系。
解决“多生产者-多消费者问题”的关键在于理清复杂的同步关系:
+ 在分析同步问题(一前一后问题)的时候不能从单个进程行为的角度来分析,要把“一前一后”发生的事看做是两种“事件”的前后关系。
+ 比如,如果从单个进程行为的角度来考虑的话,我们会有以下结论:
+ 如果盘子里装有苹果,那么一定要女儿取走苹果后父亲或母亲才能再放入水果如果盘子里装有橘子,那么一定要儿子取走橘子后父亲或母亲才能再放入水果。
+ 如果盘子里装有苹果,那么一定要女儿取走苹果后父亲或母亲才能再放入水果,如果盘子里装有橘子,那么一定要儿子取走橘子后父亲或母亲才能再放入水果。
+ 这么看是否就意味着要设置四个同步信号量分别实现这四个“一前一后”的关系了?
+ 正确的分析方法应该从“事件”的角度来考虑,我们可以把上述四对“进程行为的前后关系”抽象为一对“事件的前后关系”。
+ 盘子变空事件→放入水果事件。“盘子变空事件”既可由儿子引发,也可由女儿引发;“放水果事件"既可能是父亲执行,也可能是母亲执行。这样的话,就可以用一个同步信号量解决问题了。
@@ -1553,19 +1553,21 @@ smoker3() {
```
+ 吸烟者问题可以为我们解决“可以生产多个产品的单生产者”问题提供一个思路。
+ 值得吸取的精华是“轮流让各个吸烟者吸烟”必然需要“轮流的在桌上放上组合一、二、三”,注意体会我们是如何用一个整型变量i 实现这个“轮流”过程的。
+ 若一个生产者要生产多种产品(或者说会引发多种前驱事件),那么各个V操作应该放在各自对应的“事件”发生之后的位置。
+ 值得吸取的精华是“轮流让各个吸烟者吸烟”必然需要“轮流的在桌上放上组合一、二、三”,注意体会我们是如何用一个整型变量$i$实现这个“轮流”过程的。
+ 若一个生产者要生产多种产品(或者说会引发多种前驱事件),那么各个$V$操作应该放在各自对应的“事件”发生之后的位置。
### 管程
此前一般使用信号量机制来完成进程互斥同步,但是编写程序困难,易出错。
管程是进程同步工具,解决信号量机制大量同步操作分散的问题。
#### 管程的概念
管程是一种特殊的软件模块,有这些部分组成:
1. 局部于管程的共享数据结构(临界区)说明。
2. 对该数据结构进行操作的一组过程。
1. 局部于管程的共享数据结构(临界区)与其说明。
2. 对该数据结构进行操作的一组过程或函数。
3. 对局部于管程的共享数据设置初始值的语句。
4. 管程名字。
@@ -1574,10 +1576,11 @@ smoker3() {
1. 局部于管程的数据只能被局部于管程的过程所访问。
2. 一个进程只有通过调用管程内的过程才能进入管程访问共享数据。(即管程中定义的共享数据结构只能被管程定义的函数所修改)
3. 每次仅允许一个进程在管程内执行某个内部过程。(在一个时刻内一个函数只能被一个进程使用)
4. 管程是被进程调用的,管程是语法范围,无法创建和撤销。
#### 条件变量
通过条件变量来实现阻塞进程。由于一个进程被阻塞的原因可能有多个,所以管程中设置多个条件变量,每个条件变量保存一个等待队列,用于记录因该条件变量而阻塞的所有进程。对条件变量只有两个操作:`wait`和`signal`。
通过条件变量来实现阻塞进程。由于一个进程被阻塞的原因可能有多个,所以管程中设置多个条件变量,每个条件变量保存一个等待队列,用于记录因该条件变量而阻塞的所有进程。对条件变量只有两个操作:`wait`和`signal`。所以管程调用这两个操作时都**不用**判断条件,直接阻塞或唤醒。
+ `x.wait`:当`x`对应的条件不满足时,正在调用管程的进程调用`x.wait`将自己插入`x`条件的等待队列,并释放管程。此时其他进程可以使用该管程。
+ `x.signal`:`x`对应的条件发生了变化,则调用`x.signal`,唤醒一个因`x`条件而阻塞的进程。