资料来源:《天勤计算机操作系统高分笔记》+《王道计算机操作系统高分笔记》+《计算机操作系统第三版 汤子瀛》
文章目录
同步与互斥 \color{Brown}同步与互斥 同步与互斥
1.1 进程同步的基本概念 \color{ForestGreen}1.1 进程同步的基本概念 1.1进程同步的基本概念
1.1.1 两种形式的制约关系 \color{Fuchsia}1.1.1 两种形式的制约关系 1.1.1两种形式的制约关系
并发执行的进程一般存在两种形式的制约关系:
(1)间接相互制约关系(互斥)
间接相互制约源于进程互斥地共享系统资源,如果系统中某一进程A要求使用系统资源(如打印机),但该资源不允许两个进程同时使用,且它正被另一进程B使用,那么进程A只能等待,当进程B将该资源释放后再使用。进程之间的这种关系称为进程互斥,其基本形式为“进程—资源—进程”
(2)直接相互制约关系(同步)
直接相互制约源于进程间的合作,如果进程A需要进程B给它提供必要信息才能运行(例如输入进程和计算进程),那么当进程B未提供数据前,进程A必须阻塞等待数据到达;当进程A未取走数据前,进程B也必须阻塞等待取走数据。进程之间的这种关系称为进程同步,其基本形式为“进程—进程”
1.1.2 临界资源 \color{Fuchsia}1.1.2 临界资源 1.1.2临界资源
系统中存在一类临界资源,它在一段时间内只允许一个进程使用,如打印机、磁带机、某些变量、队列等都属于临界资源
为了保证临界资源的正确使用,将临界资源的访问过程分为4部分,而执行这4部分过程的代码称为“区”:
do{
entry section; //进入区。用于检查临界资源是否被其他进程占用,如果没有,
//则应在进入区设置“正在访问临界区”标志,以阻止其他进程同时进入临界区
critical section; //临界区。进程中用于访问临界资源的代码,又称临界段
exit section; //退出区。用于将“正在访问临界区”标志清除的部分
remainder section; //剩余区。代码中的其余部分
}while(true);
这四个区均属于要访问临界资源的进程,它们是进程中的一部分代码,每个要访问临界资源的进程均需要有这四个区,临界资源 ≠ \ne = 临界区 ≠ \ne = 临界资源所在地址
每个进程的临界区代码可以不相同,且进程对临界资源做何种操作,与临界资源及互斥同步管理无关。例如有两个进程需要使用磁带机(临界资源),一个对其进行写操作、一个对其进行读操作,那么这两个进程的临界区代码是不同的
∗ ∗ 1.1.3 互斥的要求 \color{Fuchsia}**1.1.3 互斥的要求 ∗∗1.1.3互斥的要求
为防止两个进程同时进入临界区,同步机制应遵循以下准则:
(1)空闲让进。若没有进程处于临界区时(临界资源空闲),则允许一个请求临界资源的进程进入自己的临界区
(2)忙则等待。若已有进程进入其临界区(临界资源忙碌),其他试图进入临界区的进程必须等待
(3)有限等待。对于要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,防止出现“死等”现象
(4)让权等待。当一个进程因某些原因无法进入自己的临界区时,应释放处理器给其他进程,防止陷入“忙等”状态
1.2 互斥实现方法 \color{ForestGreen}1.2 互斥实现方法 1.2互斥实现方法
互斥既可以用软件方法实现,也可以用硬件方法实现
1.2.1 软件方法 \color{Fuchsia}1.2.1 软件方法 1.2.1软件方法
假设有两个进程P0和P1,它们需要互斥地共享某临界资源
(1)算法1:单标志法
设置一个共用整型变量turn,用于表示“允许进入临界区标识”,若turn = 0,则允许P0进入自己的临界区;若turn = 1,则允许P1进入自己的临界区。P0与P1均需要在各自的进入区中循环检查turn值
int turn = 0;
P0: P1:
do{ do{
while(turn != 0); /*进入区,turn不为0则一直循环等待*/ while(turn != 1); //进入区,turn不为1则一直循环等待
critical section; /*临界区*/ critical section; //临界区
trun = 1; /*退出区*/ turn = 0; //退出区
remainder section; /*剩余区*/ remainder section; //剩余区
}while(ture); }while(true);
该算法可以保证“忙则等待”,但是违背“空闲让进”。例如,当P0访问完临界资源后,会将turn置1,但是如果P1暂时不需要访问该临界资源,turn = 1一直成立,P0将无法再次进入临界区
(2)算法2:双标志先检查法
设置一个标志数组flag[],用于表示进程是否进入自己的临界区。P0若想访问临界资源,需要在进入区检查flag[1]是否为true,若为true,则表示P1进入了自己的临界区,P0需等待;若为false,则表示资源空闲,P0可以进入的自己的临界区。P1部分同理
bool flag[2] = {false, false};
P0: P1:
do{ do{
while(flag[1]); /*进入区,若flag[1]为真,则一直循环等待*/ while(flag[0]); //进入区,若flag[0]为真,则一直循环等待
flag[0] = true; /*进入区,置标志为true,表示P0正在访问*/ flag[1] = true; //进入区,置标志为true,表示P1正在访问
critical section; /*临界区*/ critical section; //临界区
flag[0] = false; /*退出区,重置标志位*/ flag[1] = false; //退出区,重置标志位
remainder section; /*剩余区*/ remainder section; //剩余区
}while(ture); }while(true);
该算法可以保证“空闲让进”,但是违背“忙则等待”。例如,两个进程都未进入自己的临界区时,它们的标志位均为false,如果此时两个进程都想进入临界区,并且检查发现彼此的标志位均为false,那么这两个进程会同时进入自己的临界区,发生错误
(3)算法3:双标志后检查法
算法2是先检查对方的标志,再置自己的标志为真。而该算法是先置自己的标志为真(表示本进程准备进入临界区),再检查对方的标志,如果对方标志为真,表示对方也准备进入临界区,则循环等待;如果对方标志为假,则进入临界区
bool flag[2] = {false, false};
P0: P1:
do{ do{
flag[0] = true; /*进入区,置标志为true,表示P0准备访问*/ flag[1] = true; //进入区,置标志为true,表示P1准备访问
while(flag[1]); /*进入区,若flag[1]为真,则一直循环等待*/ while(flag[0]); //进入区,若flag[0]为真,则一直循环等待
critical section; /*临界区*/ critical section; //临界区
flag[0] = false; /*退出区,重置标志位*/ flag[1] = false; //退出区,重置标志位
remainder section; /*剩余区*/ remainder section; //剩余区
}while(ture); }while(true);
该算法可以保证“忙则等待”,但是违背“有限等待”。例如,当两个进程都想进入各自的临界区时,它们会将各自的标志置为true,然后同时去检查对方的标志,发现对方也准备进入临界区,于是两个进程都会循环等待,无法进入各自的临界区,造成“死等”现象
(4)算法4:Peterson算法
本算法是算法1和算法3的结合。在算法3的基础上增设一个变量turn,表示哪个进程可以优先进入临界区。当P0想进入自己的临界区时,先置flag[0]为真,表示准备访问,然后置turn = 1,表示P1可以优先进入。若P1此时也想进入(即flag[1]为真),则让P1优先进入,P0循环等待;若P1此时不想进入(即flag[1]为假),则P0进入临界区。P1部分同理
bool flag[2] = {false, false}; //表示哪个进程准备进入自己的临界区
int turn; //表示哪个进程可以优先进入临界区
P0: P1:
do{ do{
flag[0] = true; /*进入区,置标志为true,表示P0准备访问*/ flag[1] = true; //进入区,置标志为true,表示P1准备访问
turn = 1; /*进入区,表示P1可以优先访问*/ turn = 0; //进入区,表示P0可以优先访问
while(flag[1] && turn == 1); /*进入区*/ while(flag[0] && turn == 0); /*进入区*/
/*若flag[1]为真,表示P1也想访问,此时turn为1,P1可以优先访问*/ //若flag[0]为真,表示P0也想访问,此时turn为0,P0可以优先访问
/*若flag[1]为假,表示P1暂时不访问,此时跳出循环,P0可以访问*/ //若flag[0]为假,表示P0暂时不访问,此时跳出循环,P1可以访问
critical section; /*临界区*/ critical section; //临界区
flag[0] = false; /*退出区,重置标志位*/ flag[1] = false; //退出区,重置标志位
remainder section; /*剩余区*/ remainder section; //剩余区
}while(ture); }while(true);
当P0和P1同时想访问时,均置flag[ ]为true,然后P0先修改turn为1(或P1先修改turn为0),检查发现对方正准备进入,则循环等待,而P1(或P0)则不满足循环条件,因此它可以进入临界区
相较于前三种算法,Peterson算法是最好的,它可以保证“空闲让进”、“忙则等待”、“有限等待”,但是仍然违背“让权等待”(前三种算法也违背)。如果某个进程可以进入自己的临界区(意味着它已经跳出了while()循环),但是因某些事件无法继续执行临界区代码时,那么其他等待的进程会占用处理器一直执行循环语句,陷入“忙等”状态
1.2.2 硬件方法 \color{Fuchsia}1.2.2 硬件方法 1.2.2硬件方法
软件方法存在的一个问题:标志的检查与修改不是一个原子操作(即过程中会被打断),为此引入了硬件方法。硬件方法主要有以下两种:
(1)中断屏蔽法
即通过中断屏蔽来保证“忙则等待”,当进程进入其临界区时,关中断;待进程执行完临界区代码后,开中断。这样就能保证进程在执行临界区代码期间不被中断,其他进程只能等待
...
关中断;
临界区;
开中断;
...
这种方法简单高效,但是由于关中断后,就无法执行进程切换,因此限制了处理机交替执行程序的能力,不适用于多处理机环境;其次,由于关中断、开中断属于特权指令,只能在内核态执行,因此该方法只适用于操作系统内核进程,如果将开、关中断交由用户进程执行,会出现错误
(2)硬件指令法
① TestAndSet指令(TS指令/TSL指令):这条指令是原子操作,即执行期间不允许被中断,该指令的逻辑如下:
bool TestAndSet(bool *lock){ //传入一个bool变量的地址,返回一个bool变量
bool old;
old = *lock; //读取出lock原来的值
*lock = true; //将lock值置为真(上锁)
return old; //返回lock原来的值
}
如果将全局变量lock定为临界资源的状态值,lock为真时表示该临界资源正被占用;lock为假时,表示该临界资源空闲。那么就可以利用TestAndSet指令实现进程互斥(lock初始值为false):
while(TestAndSet(&lock)); //进入区,循环执行TS指令,每次循环都送入lock地址,返回lock的旧值,并置lock为真(上锁)
//若返回的lock旧值为true,表示该资源正被占用,循环等待;若返回的lock旧值为false,跳出循环,同时给资源上锁
critical section; //临界区
lock = false; //退出区(解锁)
remainder section; //剩余区
② Swap指令:该指令也是原子操作,其逻辑如下:
Swap(bool *lock, bool *key){ //该指令的功能就是交换lock和key的值
bool temp;
temp = *lock;
*lock = *key;
*key = temp;
}
同样将全局变量lock定为临界资源的状态值,此外再增设一个局部变量key(程序内有效),用于和lock交换值。那么就可以利用Swap指令实现进程互斥(lock初始值为false):
key = true; //进入区
while(key){ //进入区,循环检查key值,并交换lock和key值
Swap(&lock, &key); //若此时资源被占用,即lock为真,那么交换后lock和key仍为真,循环继续,进程等待
} //若此时资源空闲,即lock为假,那么交换后key为假,lock为真,跳出循环,同时给资源上锁
critical section; //临界区
lock = false; //退出区(解锁)
remainder section; //剩余区
需注意:TestAndSet指令和Swap指令是由硬件实现的,不会被中断,上述代码只是说明它们的逻辑,并不是说它们由软件实现
硬件指令法的优点:实现简单,适用于多处理机环境
缺点:不满足“让权等待”
1.3 信号量 \color{ForestGreen}1.3 信号量 1.3信号量
前面介绍的互斥实现方法都存在这一定的缺点,为此,荷兰计算机科学家Dijkstra于1965年提出了一种同步机制,称为信号量,其基本思想是:在多个互相合作的进程之间使用简单的信号来同步
信号量是一个确定的二元组(s,L)(整型信号量只有一个整型值s):
s 是一个非负初值的整型变量,它表示系统中某类资源的空闲数目,当 s > 0 时,表示当前有多少资源空闲;当 s < 0 时,表示当前有多少进程正在等待该资源。若 s = 1,则表示该类资源为临界资源
L 是一个初始为NULL的链表指针,用于链接等待该类资源的所有进程
除初始值以外,信号量的值只能由P操作(wait操作)和V操作(signal操作)改变。P操作相当于申请资源,V操作相当于释放资源
P、V操作均为原子操作,因此其在对信号量进行操作时不会被中断。且P、V操作在系统中一定是成对出现的
1.3.1 整型信号量 \color{Fuchsia}1.3.1 整型信号量 1.3.1整型信号量
整型信号量只有一个整型值s,其初值非负,通过P、V操作来修改其值:
int s; //整型信号量,初值非负
wait(int s){ /*P操作,向系统请求某类资源*/ signal(int s){ //V操作,进程释放资源
while(s <= 0); /*s <= 0表示系统中暂无该类资源空闲,循环等待*/ s++; //空闲资源数加一
s--; /*若s > 0,s减一,即分配一个空闲资源给进程*/ }
}
由于P、V操作均属于原子操作,执行过程中不允许被中断,因此可以保证“忙则等待”。但是由于P操作中存在循环,需要持续对信号量进行测试,存在“忙等”现象,同样违背“让权等待”
∗ ∗ 1.3.2 记录型信号量 \color{Fuchsia}**1.3.2 记录型信号量 ∗∗1.3.2记录型信号量
为解决“忙等”现象,在整型信号量的基础上添加一个链表指针,用于链接等待该类资源的所有进程,这就形成了记录型信号量,对其进行P、V操作如下:
struct semaphore { //记录型信号量
int count; //整型变量,记录系统中某类资源数目,初值非负
//若其值为负,则代表有多少进程在等待该资源
struct process *L; //链表指针,链接等待该类资源的所有进程,初始为NULL
};
wait(semaphore s){ /*P操作,向系统申请某类资源*/ signal(semaphore s){ //V操作,进程释放资源
s.count--; /*该类资源数减一*/ s.count++; //该类资源数加一
if(s.count < 0){ /*若系统中无该类资源空闲*/ if(s.count <= 0){ //若等待队列中还有进程
阻塞该进程,插入阻塞队列; /*通过block原语实现*/ 取出s.L的第一个元素,即第一个进程指针;
将该进程链接到s.L; 从阻塞队列中唤醒该进程,并改为就绪状态; //通过wakeup原语实现
} }
} }
由于进程在等待资源期间不用循环测试信号量,它会自我阻塞并放弃处理器,因此该机制可以保证“让权等待”
∗ ∗ 1.3.3 利用信号量实现进程同步 \color{Fuchsia}**1.3.3 利用信号量实现进程同步 ∗∗1.3.3利用信号量实现进程同步
假设存在并发进程P1和P2。S1和S2分别为P1和P2中的一条语句,且S2必须要使用S1的运行结果,即S2必须在S1之后执行。实现方法如下(为了简化程序代码,后续的同步/互斥实现演示中均以整型信号量形式,但实际设置的信号量均为记录型信号量,对它们的P/V操作均为记录型信号量的操作且不展示细节,因此请牢记记录型信号量的内容):
semaphore N = 0; //设置信号量,并设置初值为0
P1: P2:
{ {
...; ...;
S1; /*执行S1语句*/ wait(N); //P操作,取走一个结果数据
signal(N); /*V操作,释放一个结果数据*/ //若N < 0,P2阻塞并插入阻塞队列,待P1执行完V操作将其唤醒
/*若N <= 0,则将P2从阻塞队列中唤醒,令其取走数据*/ S2; //执行S2语句
...; ...;
} }
从这里也可以看出,虽然P、V操作在系统中一定成对出现,但是未必在一个进程里
此外,进程之间的前趋关系也是进程同步问题,例如下图所示的进程前趋图,S1、S2、S3、S4是四个最简单的程序段
为保证各程序段的正确执行,应设置多个初值为“0”的信号量。S1 → S2、S1 → S3、S2 → S4、S3 → S4分别设置信号量a、b、c、d,初值均为0,实现方法如下:
semaphore a = b = c = d = 0;
S1: S2: S3: S4:
{ { { {
产出结果数据; wait(a); wait(b); wait(c);
signal(a); 产出结果数据; 产出结果数据; wait(d);
signal(b); signal(c); signal(d); 产出结果数据;
} } } }
∗ ∗ 1.3.4 利用信号量实现进程互斥 \color{Fuchsia}**1.3.4 利用信号量实现进程互斥 ∗∗1.3.4利用信号量实现进程互斥
假设有进程P1和P2,它们需要互斥地访问一个临界资源,同样可以采用信号量机制解决。实现方法如下:
semaphore N = 1; //设置信号量,并设置初值为1,表示该资源为临界资源
P1: P2:
{ {
...; ...;
wait(N); /*向系统申请访问该临界资源*/ wait(N); //向系统申请访问该临界资源
/*若该临界资源空闲,则P1进入其临界区*/ //若该临界资源空闲,则P2进入其临界区
/*若该临界资源被占用,则P1阻塞,并插入阻塞队列*/ //若该临界资源被占用,则P2阻塞,并插入阻塞队列
critical section; /*临界区*/ critical section; //临界区
signal(N); /*释放临界资源*/ signal(N); //释放临界资源
/*若链表指针为空,则P1继续执行*/ //若链表指针为空,则P2继续执行
/*若链表指针非空,则唤醒等待该资源的进程,并继续执行P1*/ //若链表指针非空,则唤醒等待该资源的进程,并继续执行P2
...; ...;
} }
当有多个进程需要互斥地访问一个临界资源时,同样设置信号量N = 1,且每个进程内都包含上述代码(每个进程的临界区代码不一定相同)
1.4 管程 \color{ForestGreen}1.4 管程 1.4管程
虽然信号量机制是一种方便高效的进程同步机制,但是由于每个进程都有各自的临界区,并且每个进程都需要自备P、V操作,不仅给系统的管理带来了麻烦,而且还会因同步操作的使用不当而导致进程死锁。由此产生了一种新的进程同步工具——管程
管程可被视为一个 “共享的(记录型信号量 + 临界区 + P、V操作)封装模块” ,它属于系统,进程通过调用管程,来实现请求或释放资源。管程每次仅允许一个进程访问共享资源
为了实现进程互斥,管程需要包含以下三部分(所谓的“局部于”,是指仅定义在管程内部,其作用范围仅在管程范围内):
(1)局部于管程内部的共享数据结构说明。可以抽象地表示系统中的一个共享资源
(2)对该数据结构进行操作的一组过程。进程可通过这组过程,来对共享数据结构进行操作,以实现对共享资源的申请、释放和其它操作。此外这组过程还需要根据资源的情况,接受或阻塞进程的访问,以实现进程互斥
(3)对局部于管程内部的共享数据结构设置初值的语句
管程具有以下基本特征:
(1)某管程中的数据只能被该管程内的过程所访问
(2)进程只能通过调用管程内的过程才能进入管程访问共享数据
(3)每次仅允许一个进程在管程内执行内部过程,其余想进入管程的进程必须等待,并阻塞在等待队列
管程的互斥访问对程序员是透明的,它完全由编译程序在编译时自动添加,程序员无需关注,而且保证正确
此外,管程中还必须包含若干用于同步的设施。例如,当某进程进入管程后,若该进程请求的资源非空闲,则需要阻塞该进程,并使其离开管程(防止其长时间占用管程,其他进程无法进入);当被阻塞进程请求的资源空闲时,需要将该进程唤醒,使其重新进入管程从断点处继续执行
因此管程中还应包含如下支持同步的设施:
(1)局部于管程内部的若干条件变量,一个条件变量对应一种进程阻塞原因。且每个条件变量都对应一个链表,用于记录因该原因而阻塞的进程
(2)在条件变量上进行操作的wait、signal函数,当资源不足时,前者用于阻塞进程;当资源空闲时,后者用于释放进程
管程的实现逻辑如下:
typedef monitor{
int resource[number]; //共享数据结构,对应共有number种共享资源,数组每个下标表示一种资源,下标对应的元素表示该资源的空闲数目
initial(resource); //对共享数据结构赋初值的语句,初值>0,若初值=1则表示该类资源为临界资源
struct Node* block[number]; //条件变量,链表指针数组每个下标表示因请求哪种资源不得而被阻塞,每个下标对应的元素初值为NULL
//对共享数据结构实现互斥访问的一组操作
take_away(int num){ /*申请num种资源*/ give_back(int num){ //归还num种资源
if(!resource[num]) /*该资源没有空闲*/ ...; //归还该资源的语句
wait(block[num]); /*阻塞请求进程,并将其链接到对应的阻塞链表*/ resource[num]++; //该资源空闲数加一
else{ if(block[num]) //如果该资源的阻塞链表非空
...; /*申请该资源的语句*/ signal(block[num]); //唤醒链表中的一个阻塞进程
resource[num]--; /*该资源空闲数减一*/ }
}
}
//对共享数据结构实现同步访问的一组操作。为了方便,我们将每种共享资源都假定为临界资源,并且只服务于一个读进程或一个写进程
//令它们的初值为0,表示该资源未被写入,若对其写入则对应的元素+1,对其读出则对应的元素-1
write(int num){ /*对num种资源写入*/ read(int num){ //对num种资源读出
if(resource[num]) /*还未被读出*/ if(!resource[num]) //还未被写入
wait(block[num]); /*阻塞自身,待读进程读出*/ wait(block[num]); //阻塞自身,待写进程写入
else{ else{
...; /*写入语句*/ ...; //读出语句
resource[num]++; /*已写入标记*/ resource[num]--; //已读出标记
if(block[num]) /*若有读进程被阻塞*/ if(block[num]) //若有写进程被阻塞
signal(block[num]); /*释放读进程*/ signal(block[num]); //释放写进程
} }
} }
}DEMO; //定义一个名为DEMO的管程
∗ ∗ 1.5 经典同步问题 \color{ForestGreen}**1.5 经典同步问题 ∗∗1.5经典同步问题
1.5.1 生产者 − 消费者问题 \color{Fuchsia}1.5.1 生产者-消费者问题 1.5.1生产者−消费者问题
问题描述:有一组生产者进程和一组消费者进程,它们共享一个初始为空、大小为n的缓冲区。只有当缓冲区未满时,生产者才能将消息放入缓冲区,否则必须等待;只有当缓冲区不空时,消费者才能取出消息,否则必须等待。缓冲区是临界资源,每次仅允许一个进程访问(不管是生产者还是消费者进程)
问题分析:生产者和消费者之间既是同步关系(生产者 → 消费者,消费者 → 生产者),也是互斥关系(生产者/消费者—缓冲区—生产者/消费者)。为此,需要设置三个信号量:
(1)生产者 → 消费者:同步信号量empty,表示缓冲区空闲位数目,其初值为n,当 empty <= 0 时将不再允许生产者向缓冲区内放入消息。生产者对其执行P操作(请求资源),消费者对其执行V操作(释放资源)
(2)消费者 → 生产者:同步信号量full,表示产品(消息)数目,其初值为0,当full <= 0时将不再允许消费者从缓冲区内取出消息。消费者对其执行P操作(请求资源),生产者对其执行V操作(释放资源)
(3)生产者/消费者—缓冲区—生产者/消费者:互斥信号量mutex,表示缓冲区,其初值为1(临界资源)。生产者放消息(或消费者取消息)之前必须确认缓冲区是否被其他生产者/消费者占用
生产者-消费者问题可描述如下:
semaphore empty = n; //缓冲区空闲位数目,初值为n
semaphore full = 0 //产品数目,初值为0
semaphore mutex = 1; //互斥信号量,初值为1,表示临界资源
Producer: //生产者进程
{
while(true) {
产出产品;
wait(empty); //P操作,申请缓冲区空闲位。若无空闲位,则阻塞
wait(mutex); //P操作,申请访问缓冲区。若缓冲区正被其他生产者/消费者占用,则阻塞
将产品放入缓冲区;
signal(mutex); //V操作,释放缓冲区,以供其他生产者/消费者访问。若阻塞队列非空,则唤醒阻塞队列的第一个进程
signal(full); //V操作,释放一个产品。若有消费者因等待产品而阻塞,则唤醒阻塞队列中的第一个进程
}
}
Consumer: //消费者进程
{
while(true) {
wait(full); //P操作,申请一个产品。若无多余产品,则阻塞
wait(mutex); //P操作,申请访问缓冲区。若缓冲区正被其他消费者/生产者占用,则阻塞
从缓冲区取出产品;
signal(mutex); //V操作,释放缓冲区,以供其他消费者/生产者访问。若阻塞队列非空,则唤醒阻塞队列的第一个进程
signal(empty); //V操作,释放一个缓冲区空闲位。若有生产者由于等待空闲位阻塞,则唤醒阻塞队列的第一个进程
使用产品;
}
}
需注意:wait(empty) / wait(full) 必须在wait(mutex) 之前,若颠倒顺序会导致死锁。例如,当缓冲区满时(empty = 0),若生产者先执行wait(mutex),访问缓冲区,再执行wait(empty)时,会被阻塞,它将始终占用缓冲区访问权无法释放,消费者无法取走产品,从而导致生产者、消费者都被阻塞且无法被唤醒(死锁)。但是signal(full) / signal(empty)与signal(mutex)之间的顺序可以颠倒
知识延伸:当有多个信号量同时存在时,必须先对同步信号量进行P操作,再对互斥信号量进行P操作
此外,还需注意:若生产者进程和消费者进程数量均为1,则无需互斥信号量mutex(缓冲区空闲位为1,生产者必须等待消费者取走消息、消费者必须等待生产者放入消息,否则都会被阻塞)。若有多个生产者或多个消费者,则必须设置互斥信号量mutex,否则会导致出错。例如,两个生产者同时访问缓冲区时,它们放入的消息会被彼此覆盖的情况
知识延伸:当有多个同类进程时,必须设置互斥信号量;若同类进程只有一个,则只需要同步信号量即可
1.5.2 读者 − 写者问题 \color{Fuchsia}1.5.2 读者-写者问题 1.5.2读者−写者问题
问题描述:有读者和写者两组并发进程,共享一个文件(共享文件是临界资源)。文件读者只能从共享文件中读出数据,写者只能向共享文件中写入数据。有如下要求:
(1)任意多个读者可以同时读这个文件(读者不互斥)
(2)每次只允许一个写者向文件中写入数据(写者互斥)
(3)写者在进行写入时,禁止读者读文件和其他写者写文件(读者写者互斥)
实现该问题共分三种情况:读者优先、公平情况、写者优先
( 1 )读者优先算法 \color{red}(1)读者优先算法 (1)读者优先算法
当且仅当没有读者读文件时,才允许写者写入。只要有读者在读文件,后续读者即可插队在写者前进行访问。实现该算法需要设置以下三个信号量:
① 写者互斥、写者读者互斥:互斥信号量mutex,表示共享文件,其初值为1(临界资源)。读者读出时必须确认该文件是否被写者占用,写者写入时必须确认该文件是否被其他写者或读者占用
② 读者不互斥:整型变量readcount,表示正在读文件的读者数,其初值为0。读者读文件之前(或者读完文件后)必须执行readcount++(或者readcount–),并且仅当readcount == 0时,读者才需要申请 / 释放共享文件的访问权
③ 互斥信号量rmutex,表示变量readcount的访问权,其初值为1(临界资源),即读者必须互斥地访问readcount,对其值进行修改
semaphore mutex = 1; //表示共享文件,它是临界资源
semaphore rmutex = 1; //表示变量readcount的访问权,它是临界资源
int readcount = 0; //表示正在读文件的读者数
Reader: //读者进程
{
while(true) {
wait(rmutex); //申请访问readcount,若其正被其他读者占用,则阻塞
if(readcount == 0)
wait(mutex); //若当前没有读者在读文件,则需要先申请访问共享文件;否则,读者无需申请即可访问
readcount++; //正在读文件的读者数加一
signal(rmutex); //释放readcount的访问权,若有其他读者因等待访问而阻塞,则唤醒阻塞队列的第一个进程
读文件;
wait(rmutex); //申请访问readcount
if(--readcount == 0)
signal(mutex); //若当前没有读者读文件,则释放共享文件的访问权,若有写者因等待访问而阻塞,则唤醒阻塞队列的第一个进程
signal(rmutex); //释放readcount的访问权
}
}
Writer: //写者进程
{
while(true) {
wait(mutex); //申请访问共享文件,若有其他写者/读者正在访问文件,则阻塞
写文件;
signal(mutex); //释放共享文件的访问权,若有其他写者/读者因等待访问而阻塞,则唤醒阻塞队列的第一个进程
}
}
假设进程到达的顺序是:读者1 → 写者 → 读者2,则读者1读文件时,写者因访问共享文件互斥被阻塞,读者2因读者1释放readcount的访问权被唤醒,与读者1一同访问共享文件;读者1和读者2均读完后,释放共享文件访问权,唤醒写者写文件
( 2 )公平情况算法(按到达顺序进行操作) \color{red}(2)公平情况算法(按到达顺序进行操作) (2)公平情况算法(按到达顺序进行操作)
进程的执行完全按照到达顺序。当某读者正在读出时,如果有写者因等待访问而阻塞,则该读者执行完毕后立即执行该写者,后续读者应等待该写者执行完才能读出。为此,需要在“读者优先算法”的基础上增设一个信号量wmutex:
互斥信号量wmutex,表示共享文件访问预占位,其初值为1,当其值 <= 0 时,后续进程被阻塞。读者 / 写者通过对其进行P操作来完成“占位”,写者需要在执行完之后才能对其进行V操作(执行期间后续进程均阻塞),读者需要在访问完readcount后立即对其执行V操作(防止紧随其后的读者被阻塞)
semaphore mutex = 1; //表示共享文件,它是临界资源
semaphore rmutex = 1; //表示变量readcount的访问权,它是临界资源
semaphore wmutex = 1; //表示文件访问预占位
int readcount = 0; //表示正在读文件的读者数
Reader: //读者进程
{
while(true) {
wait(wmutex); //申请预占位,若预占位已有其他进程,则阻塞
wait(rmutex); //申请访问readcount,若其正被其他读者占用,则阻塞
if(readcount == 0)
wait(mutex); //若当前没有读者在读文件,则需要先申请访问共享文件
readcount++; //正在读文件的读者数加一
signal(rmutex); //释放readcount的访问权,若有其他读者因等待访问而阻塞,则唤醒阻塞队列中的第一个进程
signal(wmutex); //释放预占位,若有其他读者/写者因等待预占位而阻塞,则唤醒阻塞队列中的第一个进程
读文件;
wait(rmutex); //申请访问readcount
if(--readcount == 0)
signal(mutex); //若当前没有读者读文件,则释放共享文件的访问权,若有写者因等待访问而阻塞,则唤醒阻塞队列的第一个进程
signal(rmutex); //释放readcount的访问权
}
}
Writer: //写者进程
{
while(true) {
wait(wmutex); //申请预占位
wait(mutex); //申请访问共享文件
写文件;
signal(mutex); //释放共享文件的访问权
signal(wmutex); //释放预占位
}
}
假设进程到达的顺序是:读者1 → 写者 → 读者2,则读者1读文件时,写者因访问共享文件互斥被阻塞,读者2因写者占用预占位被阻塞;读者1读完文件后,写者被唤醒并写文件,读者2仍因写者占用预占位被阻塞;只有当写者写完文件释放预占位才能唤醒读者2
( 3 )写者优先算法 \color{red}(3)写者优先算法 (3)写者优先算法
当且仅当没有写者写文件时,才允许读者读出。只要有写者在写文件,后续写者便可插队在读者前进行访问(但是仍需遵守写者互斥)。为此,需要在“公平情况算法”的基础上增设两个信号量readable、writecount,以及修改信号量wmutex的含义:
① 整型变量writecount,表示当前正执行和正等待的写者数,其初值为0。写者申请访问文件之前(或写入完成后)必须对其进行writecount++(或writecount–)
② 互斥信号量readable,表示共享文件访问预占位,其初值为1。当其值 <= 0 时,后续进程被阻塞。仅当writecount == 0 时,写者才需要对其执行P / V操作(即只要有写者在写入文件,就会一直占据文件访问优先权);读者在访问readcount之前(或访问完之后)对其执行P操作(或V操作)
③ 互斥信号量wmutex,表示变量writecount的访问权,其初值为1(临界资源),即写者必须互斥地对其访问,修改其值
semaphore mutex = 1; //表示共享文件,它是临界资源
semaphore rmutex = 1; //表示变量readcount的访问权,它是临界资源
semaphore wmutex = 1; //表示变量writecount的访问权,它是临界资源
semaphore readable = 1; //表示共享文件访问预占位
int readcount = 0; //表示正在读文件的读者数
int writecount = 0; //表示正执行或正等待的写者数
Reader: //读者进程
{
while(true) {
wait(readable); //申请预占位,若预占位已有其他进程,则阻塞,待前一个读者访问完readcount或写者写完将其唤醒
wait(rmutex); //申请访问readcount,若其正被其他读者占用,则阻塞
if(readcount == 0)
wait(mutex); //若当前没有读者在读文件,则需要先申请访问共享文件
readcount++; //正在读文件的读者数加一
signal(rmutex); //释放readcount的访问权,若有其他读者因等待访问而阻塞,则唤醒阻塞队列中的第一个进程
signal(readable); //释放预占位,若有其他读者/写者因等待预占位而阻塞,则唤醒阻塞队列中的第一个进程
读文件;
wait(rmutex); //申请访问readcount
if(--readcount == 0)
signal(mutex); //若当前没有读者读文件,则释放共享文件的访问权,若有写者因等待访问而阻塞,则唤醒阻塞队列的第一个进程
signal(rmutex); //释放readcount的访问权
}
}
Writer: //写者进程
{
while(true) {
wait(wmutex); //申请访问writecount,若其正被其他写者占用,则阻塞
if(writecount == 0)
wait(readable); //如果正执行和正等待的写者数为0,则申请预占位
writecount++; //正执行和正等待的写者数加一
signal(wmutex); //释放writecount的访问权,若有其他写者因等待访问而阻塞,则唤醒阻塞队列中的第一个进程
wait(mutex); //申请访问共享文件
写文件;
signal(mutex); //释放共享文件的访问权,若有其他写者/读者因等待访问而阻塞,则唤醒阻塞队列的第一个进程
wait(wmutex); //申请访问writecount
if(--writecount == 0)
signal(readable); //仅当正执行和正等待的写者数为0,才释放预占位
signal(wmutex); //释放writecount的访问权
}
}
假设进程到达的顺序为:写者1 → 读者 → 写者2,则写者1写文件时,读者因写者1占用预占位而阻塞,写者2因访问共享文件互斥而阻塞;写者1写完后,释放共享文件访问权,并唤醒写者2写文件,读者因写者2占用预占位被阻塞;写者2写完后,释放共享文件访问权以及预占位,唤醒读者读文件
1.5.3 哲学家进餐问题 \color{Fuchsia}1.5.3 哲学家进餐问题 1.5.3哲学家进餐问题
问题描述:
问题分析:显然,此问题中筷子是临界资源。每根筷子两边的哲学家属于进程互斥,为此,设置一个信号量数组Fork[5],数组第几号元素表示第几号筷子,初值为1
首先,引用一个错误解法:
semaphore Fork[5] = {1, 1, 1, 1, 1};//5根筷子的的初值均为1(临界资源)
philosopher(int i) { //i表示每位哲学家的编号,从0到4
思考;
想吃饭;
wait(Fork[i % 5]); //第i号哲学家拿起他左边的筷子
wait(Fork[(i + 1) % 5]); //第i号哲学家拿起他右边的筷子
进餐;
signal(Fork[i % 5]); //第i号哲学家放下他左边的筷子
signal(Fork[(i + 1) % 5]); //第i号哲学家放下他右边的筷子
}
这种解法会导致死锁。例如,当5个哲学家同时想进餐时,他们会同时拿起左手边的筷子,但是再想拿右手边的筷子时,都将因没有筷子而无限等待,导致死锁
为了防止死锁的发生,可以采用以下三种方法:
(1)最多只允许4个哲学家同时进餐,这样就可以保证至少有一个哲学家能拿到一双筷子,防止死锁发生
为此,需要设置一个互斥信号量pinum,表示就餐预订席位,其初值为4。哲学家想吃饭时对其P操作,放下筷子后对其V操作
(2)仅当一个哲学家左右两边的筷子同时可用时,他才可以拿起筷子。实现这一方法的最优解是用AND型信号量,这里提供的是用记录型信号量的解法
为此,需要设置一个互斥信号量mutex,表示立即就餐占位,其初值为1。哲学家想吃饭时对其P操作,拿到一双筷子后对其V操作
(3)奇数号的哲学家拿筷子的顺序为从左至右,偶数号的哲学家拿筷子的顺序为从右至左,这样也可以保证至少一个哲学家拿到筷子
此方法无需设置信号量,只需加入判断语句即可
方法一: 最多只允许4个哲学家同时就餐
semaphore pinum = 4; //最多只允许4个哲学家预订就餐
semaphore Fork[5] = {1, 1, 1, 1, 1};
philosopher(int i) { //i表示每位哲学家的编号,从0到4
思考;
想吃饭;
wait(pinum); //申请一个就餐预订席位
wait(Fork[i % 5]);
wait(Fork[(i + 1) % 5]);
进餐;
signal(Fork[i % 5]);
signal(Fork[(i + 1) % 5]);
signal(pinum); //释放一个就餐预订席位
}
/*当五个哲学家按顺序同时想就餐时,0~3号哲学家都得到就餐预订席位,4号哲学家阻塞,且只有3号哲学家能拿到左右两侧的筷子,其他哲学家因缺少右
边的筷子而阻塞;3号哲学家就餐完后,会释放两边的筷子,唤醒2号哲学家就餐,并释放预订席位,唤醒4号哲学家;2号哲学家就餐完唤醒1号……以此类推*/
方法二:仅当一个哲学家左右两边的筷子同时可用时,他才可以拿起筷子
semaphore mutex = 1; //表示立即就餐占位
semaphore Fork[5] = {1, 1, 1, 1, 1};
philosopher(int i) {
思考;
想吃饭;
wait(mutex); //申请立即就餐占位
wait(Fork[i % 5]);
wait(Fork[(i + 1) % 5]);
signal(mutex); //释放立即就餐占位
进餐;
signal(Fork[i % 5]);
signal(Fork[(i + 1) % 5]);
}
/*当五个哲学家按顺序同时想就餐时,0号哲学家先拿到两边的筷子,1~4号哲学家阻塞;0号哲学家拿到筷子后释放立即就餐占位,唤醒1号哲学家,但1号
哲学家因左边筷子互斥而阻塞;0号哲学家就餐完后释放两边的筷子,唤醒1号哲学家;1号哲学家拿到筷子后释放立即就餐占位,唤醒2号哲学家……以此类推*/
方法三:奇数号的哲学家拿筷子的顺序为从左至右,偶数号的哲学家拿筷子的顺序为从右至左
semaphore Fork[5] = {1, 1, 1, 1, 1};
philosopher(int i) {
思考;
想吃饭;
if(i % 2 != 0) { //如果该哲学家编号为奇数
wait(Fork[i % 5]); //先拿左边的筷子
wait(Fork[(i + 1) % 5]);
}
else {
wait(Fork[(i + 1) % 5]); //先拿右边的筷子
wait(Fork[i % 5]);
}
进餐;
signal(Fork[i % 5]);
signal(Fork[(i + 1) % 5]);
}
/*当五个哲学家按顺序同时想就餐时,0、2、4号哲学家先拿到右边的筷子,1、3号哲学家因左边筷子互斥而被阻塞,而后0号哲学家因左边筷子互斥而则阻
塞,2、4号哲学家可同时就餐;2、4号哲学家就餐完后释放两边的筷子,并唤醒0、3号哲学家同时就餐,1号哲学家仍因左边筷子互斥而被阻塞;当0号哲学
家就餐完后再唤醒1号哲学家*/
1.5.4 吸烟者问题 \color{Fuchsia}1.5.4 吸烟者问题 1.5.4吸烟者问题
问题描述:有三个吸烟者和一个供应者,吸烟者从供应者手里获取材料卷成香烟并抽掉它。一根烟总共需要三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草、第二个拥有纸、第三个拥有胶水。供应者每次只提供两种材料,而拥有剩下那种材料的吸烟者则拿起它们卷成香烟抽掉,并告诉供应者抽完了。供应者收到反馈消息后,继续供应两种材料,重复上述过程
问题分析:显然,供应者和三个吸烟者之间属于同步关系,为此,需要设置以下信号量
(1)同步信号量material1,表示纸和胶水,其初值为0,供应给第一位吸烟者
(2)同步信号量material2,表示烟草和胶水,其初值为0,供应给第二位吸烟者
(3)同步信号量material3,表示烟草和纸,其初值为0,供应给第三位吸烟者
(4)同步信号量finish,表示抽烟完成,其初值为0,用于实现供应者和吸烟者之间的反馈同步
semaphore material1 = 0; //表示纸和胶水
semaphore material2 = 0; //烟草和胶水
semaphore material3 = 0; //烟草和纸
semaphore finish = 0; //表示抽烟完成
int random; //随机数,用于控制供应者提供的材料
provider:{ /*供应者*/ smoker(int num):{ //吸烟者进程,传入吸烟者编号
while(true) { while(true) {
random = rand(); /*生成随机数*/ if(((num - 1) % 3) == 0)
if((random % 3) == 0) wait(material1); //申请纸和胶水
signal(material1); /*提供纸和胶水*/ else if(((num - 1) % 3) == 1)
else if((random % 3) == 1) wait(material2); //申请烟草和胶水
signal(material2); /*提供烟草和胶水*/ else
else wait(material3); //申请烟草和纸
signal(material3); /*提供烟草和纸*/ 卷成香烟,抽掉;
wait(finish); /*等待“吸烟完成”反馈*/ signal(finish); //释放“吸烟完成”反馈
} }
} }
1.5.5 理发师问题 \color{Fuchsia}1.5.5 理发师问题 1.5.5理发师问题
问题描述:理发店有一位理发师、一把理发椅和 n 个凳子(供顾客等候时使用)。若没有顾客,则理发师在理发椅上睡觉。当一个顾客到来时,他将叫醒理发师;若理发师正在给顾客理发,且有空凳子,该顾客等待;若没有空凳子,则顾客离开
问题分析:
理发师和顾客之间属于同步关系,当顾客准备好时提醒理发师可以理发,理发师理完发后提醒顾客可以离开
理发椅属于临界资源,当店里没有顾客时,由理发师占用,若有顾客,则由顾客互斥地使用
n个凳子也是临界资源,由顾客互斥地使用
因此,需要设置以下信号量:
(1)同步信号量 ready 和 finish,ready表示顾客准备理发,finish表示理发完成,其初值均为0
(2)互斥信号量 bchair 和 wchair,bchair表示理发椅,wchair表示凳子,bchair初值为1,wchair初值为n
(3)整型变量waitcount,用于记录当前等待理发的人数,初值为0,最大值为n(不算理发椅上的顾客)
(4)互斥信号量mutex,表示waitcount的访问权,其初值为1
semaphore ready = finish = 0; //ready表示顾客是否准备好开始理发,finish表示理发是否完成
semaphore bchair = 1; //理发椅
semaphore wchair = n; //凳子
semaphore mutex = 1; //waitcount的访问权
int waitcount = 0; //等待理发的人数
barber:{ /*理发师进程*/ customer:{ //顾客进程
while(true) { while(true) {
wait(mutex); /*申请访问waitcount*/ wait(mutex); //申请访问waitcount
if(waitcount == 0) { /*如果没有等待顾客*/ if(waitcount < n) { //如果等待的顾客小于n,则还有空凳子
wait(bchair); /*占用理发椅*/ waitcount++; //等待的顾客加一
signal(mutex); /*释放waitcount访问权*/ wait(wchair); //占用一张空凳子
wait(ready); /*等待顾客*/ signal(mutex); //释放waitcount访问权
signal(bchair); /*离开理发椅供顾客使用*/ }
理发; else {
signal(finish); /*告诉顾客理发完成*/ signal(mutex); //释放waitcount访问权
} exit; //离开
else { /*如果还有等待的顾客*/ }
signal(mutex); /*释放waitcount访问权*/ signal(ready); //提醒理发师已准备好
wait(ready); /*等待顾客准备好*/ wait(bchair); //申请理发椅
理发; wait(mutex); //访问waitcount
signal(finish); /*告诉顾客理发完成*/ waitcount--; //等待人数减一(已经申请到理发椅)
} signal(wchair); //释放自己坐的凳子
} signal(mutex); //释放waitcount访问权
} wait(finish); //等待理发师理发完成
signal(bchair); //离开理发椅
}
}