1.进程同步与互斥
实现进程同步与互斥的工具主要有两种:
1.锁(加锁、解锁)
2.信号量:PV操作
1.0 信号量
信号量代表系统中资源的数量
1.0.1 整型信号量
整型信号量:用一个整型数代表系统资源数量
缺点:进程会出现“忙等”(即一个进程占用临界资源,另一个就得一直等待)
int S=1; //初始化信号量,代表当前系统资源数量为1,如打印机
//P操作(荷兰语 Passeren--“通过”)
void wait(int S){ //检查和上锁一气呵成
while(S<=0); //若资源数量不足,进程在此阻塞等待,这里会出现“忙等”
S=S-1; //资源数量够用,则进程占用一个资源,系统资源数量减1
}
//V操作(荷兰语 Vrijgeven--“释放”)
void signal(int S){
S=S+1; //进程释放一个资源后,资源数量加1
}
假设场景:当前S=1,进程P0正在访问某个资源【执行wait】,则S=0,若在此时发生了进程切换,进程P1也想访问该资源,执行wait函数时在while处阻塞等待,当进程P0退出临界区并释放资源后,进程P1才可进入临界区
//进程P0
...
wait(S); //进入区,申请资源(P操作)
critical section //临界区,访问资源
signal(S); //退出区,释放资源(V操作)
remainder section //剩余区
//进程P1
...
wait(S); //进入区,申请资源(P操作)
critical section //临界区,访问资源
signal(S); //退出区,释放资源(V操作)
remainder section //剩余区
1.0.2 记录型信号量
记录型信号量:用一个数据结构代表系统资源数量
解决整型信号量出现“忙等”的问题,让等待的进程进入阻塞队列
//记录型信号量的定义
typedef struct{
int value; //剩余资源数
struct process *L; //等待队列
} semaphore;
//一次P操作:申请一个单位的资源
//某进程需要使用资源时,通过wait申请
void wait(semaphmore S){
S.value--; //资源数减一
if(S.value<0){ //资源数不足
//信号量S的等待队列L,比如等待打印机的进程构成的队列
block(S.L); //发现资源不足,进程自己加入阻塞队列,主动放弃处理机,使其从运行态变为阻塞态,遵循让权等待,不会出现忙等
}
}
//一次V操作:释放一个单位的资源
//某进程不需要使用资源时,通过signal释放
void signal(semaphmore S){
S.value++; //资源数加一
if(S.value<=0){ //资源数不足
//信号量S的等待队列L,比如等待打印机的进程构成的队列
wakeup(S.L); //唤醒阻塞队列中的某进程,使其从阻塞态变为就绪态
}
}
例子:如果现在有2台打印机(资源数value=2),4个进程均需要打印机A,进程0通过wait申请使用打印机,value=1,进程1通过wait申请使用打印机B,value=0,此时资源不足,那么进程3(value=-1)和进程4(value=-2)进入阻塞队列进行等待,进程0使用完释放打印机A(value=-2+1=-1),进程1使用完释放打印机B(value=-1+1=0)
1.1 临界资源、临界区
临界资源:一次仅允许一个进程使用的资源
临界区:访问临界资源的那段代码
//进入区和退出区是实现互斥的代码段
do{
entry section //进入区:检查进程能否进入临界区,若能,则“上锁”(此时其他进程无法进入该临界区)
crtitical section //临界区:访问临界资源的代码
exit section //退出区:进程完成访问,“解锁”(此时其他进程可以进入上面的临界区)
remainder section //剩余区:代码中的其余部分
}while(true)
1.2 两种资源共享的方式
1.2.1 同时共享
宏观上“同时”访问某资源,微观上交替访问某资源
例如:某两个进程共享一个物理内存
下图来自《小林coding》
并发与共享的关系
并发与共享共生共存
两个进程交替执行(并发)“同时”访问硬盘资源(两个进程共享了同一份资源)
1.2.2 互斥共享
例如:一个摄像头如果一个APP正在使用,则另一个APP无法使用
1.3 进程同步
同步关系:两个进程存在先后关系,例如A程序的输出是B程序的输入,意味着程序A必须在程序B前执行
1.3.1 为什么需要进程同步?
为提高效率,我们引入了并发性,而并发性带来了异步性,而异步性是指进程以不可预知的速度向前推进,进程何时结束,何时暂停不可预知。这时我们需要一种机制,使得多个进程能够合理有序地共同完成一个任务
1.3.2 实现进程同步的方法
1.3.2.1 信号量机制实现进程同步(PV操作)
P操作(申请资源)
S=S-1; //尝试申请资源(申请并不一定成功)S代表当前可用资源个数
if(S < 0) //表示当前无可用资源,比如 0-1<0,第一个0表示无可用资源
//因在尝试申请时已无可用资源,所以需要将提出申请的该进程阻塞,将此进程挂到该资源的等待队列,调度另一个进程运行
if(S >= 0) //表示当前有可用资源,比如 1-1=0,第一个1表示当前可用资源为1
//因在尝试申请时还有可用资源,提出申请的该进程继续运行
V操作(释放资源)
S=S+1; //尝试归还/释放资源
if(S <= 0) //表示当前有等待该资源的进程,比如 -1+1=0,-1表示当前等待资源的进程为1
//唤醒等待该资源的第一个其他进程,该进程继续运行(释放完资源后继续做自己的事)
if(S > 0) //表示当前没有等待该资源的进程,无需唤醒任何其他进程,该进程继续运行
进程同步:并发进程有序推进,解决异步中无序推进带来的不确定性
//记录型信号量的定义
typedef struct{
int value; //剩余资源数
struct process *L; //等待队列
} semaphore;
同步信号量初始化为0,先V后P,则可以实现同步
刚开始该资源为0,只有某个进程释放该资源后,另一个进程才可使用该资源,这就保证了进程执行的先后次序
//信号量机制实现进程同步
semaphore S = 0;
P1(){
...
...
V(S); //释放资源
...
}
P2(){
P(S); //获得资源
...
...
...
}
进程同步的应用:信号量机制实现前驱关系
下图来自王道考研操作系统
1.4 进程互斥
互斥关系:某资源如摄像头同一时间段仅允许一个进程使用,不允许同一时间段内有两个以上进程使用,存在一种排斥关系,一个进程在访问某个资源时,其他进程如果此时也要访问该资源时,则必须等待
实现进程互斥需要遵循的原则:
- 空闲让进:临界区空闲的话,允许进程进入临界区
- 忙则等待:临界区繁忙的话,临界区外的进程需等待
- 有限等待:保证进程在有限时间内可以进入临界区,保证进程不会出现饥饿(等过长时间)
- 让权等待:当某个进程无法进入临界区时,立即释放处理器,防止进程忙等待
1.4.1 为什么需要进程互斥?
例如:打印两个进程A、B的数据,当进程A正在数据的过程中,进程B也来打印,则打印出来的内容就会是进程A与B的掺杂,所以进程互斥机制的有必要的
1.4.2 实现进程互斥的方法
1.4.2.1 实现进程互斥的软件方法
1.4.2.1.1 单标志法
在进入区只做“检查”,不上锁
int turn=0;//表示当前允许进入临界区的进程号
//进程P0
while(turn != 0); //进入区,检查
critical section; //临界区
turn = 1; //退出区
remainder section; //剩余区
//进程P1
while(turn != 1); //进入区
critical section; //临界区
turn = 0; //退出区
remainder section; //剩余区
实现了同一时刻最多有一个进程进入临界区
缺点:如果刚开始进程P0没有进入临界区,也就意味着随后的退出区内代码也不会执行,这样就导致标志位永远不会被修改,标志位永远为0,则进程P1永远无法进入临界区,临界区一直处于空闲状态.违背了“空闲让进”
1.4.2.1.2 双标志法先检查
在进入区先“检查”,后上锁
bool flag[2];
flag[0]=false; //表示进程0不想进入临界区
flag[1]=false; //表示进程1不想进入临界区
//进程P0
while(flag[1]); //询问进程1是否想要进入临界区 //进入区(检查)
flag[0]=true; //进程0想要进入临界区 //进入区(上锁)
critical section; //临界区
flag[0]=false; //退出区
remainder section; //剩余区
//进程P1
while(flag[0]); //询问进程0是否想要进入临界区 //进入区(检查)
flag[1]=true; //进程1想要进入临界区 //进入区(上锁)
critical section; //临界区
flag[1]=false; //退出区
remainder section; //剩余区
缺点:如果进程P0与进程P1并发执行,就可能导致两个进程同时进入临界区,违背了“忙则等待”
原因:进入区的两句之间,也就是检查与上锁之间可能发生进程切换,这两句并没有一同处理
1.4.2.1.3 双标志法后检查
双标志法前检查中是先检查后上锁
双标志法后检查是对前者的改进,改为了先上锁后检查
bool flag[2];
flag[0]=false; //表示进程0不想进入临界区
flag[1]=false; //表示进程1不想进入临界区
//进程P0
flag[0]=true; //进程0想要进入临界区 //进入区(上锁)
while(flag[1]); //询问进程1是否想要进入临界区 //进入区(检查)
critical section; //临界区
flag[0]=false; //退出区
remainder section; //剩余区
//进程P1
flag[1]=true; //进程1想要进入临界区 //进入区(上锁)
while(flag[0]); //询问进程0是否想要进入临界区 //进入区(检查)
critical section; //临界区
flag[1]=false; //退出区
remainder section; //剩余区
缺点:如果出现中断,则执行顺序会改变,若改变如上图,执行完左侧第一句来了中断,然后执行右侧第一句、第二句、第三句,紧接着又来中断,随后执行左侧第二句、第三句,则会导致两个进程均无法进入临界区,都互相等对方进入。违背了“有限等待”和“空闲让进”
1.4.2.1.4 Peterson算法
单标志算法 + 双标志后检查算法 + turn = Peterson算法
利用 flag 解决了进程互斥访问临界资源
利用 turn 解决了“饥饿”现象(turn有谦让意味,某进程在进入临界区前询问其他进程,你想进入临界区吗?如果想,就先让其他进程执行,这样就能避免长时间等待)
如果进程卡在while语句,则该进程仍在CPU上一直循环判断,无法进入临界区,一直占用着CPU,此时应该释放处理器,让其他进程进入临界区,故该算法不满足“让权等待”,其他三个原则均满足
bool flag[2]; //表示是否想要进入临界区
//初始值 flag={false,false} 进程0和进程1均不想进入临界区
int turn = 0; //优先让进程0进入临界区
//p0进程
flag[0] = true; //进程0想要进入临界区 //进入区
turn = 1; //优先让进程1进入临界区(谦让意味) //进入区
while(flag[1] && turn == 1); //检查进程1是否已经进入临界区
//如果上述不满足,代表 flag[1]=false 进程1不愿进入临界区
critical section; //进程0进入临界区
flag[0] = false; //退出区,访问完临界区后表示不再想进入临界区
remainder section; //剩余区
//P1进程
flag[1] = true; //进程1想要进入临界区 //进入区
turn = 0; //优先让进程0进入临界区(谦让意味) //进入区
while(flag[0] && turn == 0); //检查进程0是否有意愿进入临界区,再次检查turn是否为0(防止进程在执行完该句的上一句就切换到进程0,从而使得turn值为1)
//如果上述不满足,代表 flag[0]=false 进程0不愿进入临界区
critical section; //进程1进入临界区
flag[1] = false; //退出区,访问完临界区后表示不再想进入临界区
remainder section; //剩余区
1.4.2.2 实现进程互斥的硬件方法
1.4.2.2.1 中断屏蔽方法
利用“关中断指令”和“开中断指令”(与原语的实现相同)执行关中断指令后,CPU不再检查中断信号,后序指令不可能被中断(也就不可能在此期间发生进程切换)此时进程进入临界区,直到进程访问完临界区后,执行“开中断指令”
关中断指令
临界区
开中断指令
1.4.2.2.2 硬件指令方法
TestAndSet(TS指令/TSL指令)用硬件逻辑直接实现,执行过程不允许被中断
while不断检测临界区是否上锁了,如果函数返回值为false,则while不再循环,进程可以进入临界区,反之,进程无法进入临界区
TestAndSet将“检查”和“上锁”一起完成
暂时无法进入临界区的进程会占用CPU并一直判断while中的内容,违背了“让权等待”
//lock=true;表示临界区已上锁,其他进程无法进入
//lock=false;表示临界区未上锁,进程可以进入
bool TestAndSet(boolean *lock){
boolean old;
old = *lock; //若lock为false,则old为false,则while不再执行,进程可以进入临界区
*lock = true; //进程进入临界区,将临界区上锁
return old; //若lock为false,返回old=false
}
//使用TSL实现进程互斥
//进程P
while(TestAndSet(&lock)); //若函数返回值为false,则while不再执行,进程可以进入临界区
critical section; //临界区
lock = false; //退出区
remainder section; //剩余区
Swap指令(XCHG指令)
Swap(boolean *a,boolean *b){
boolean temp;
temp = *a;
*a = *b;
*b = temp;
}
boolean old = true;
//old == true 代表有进程正在访问临界区
while(old == true) //循环检查,一旦old为false,循环不再执行,进程进入临界区
Swap(&lock,&old); //将old值与lock值进行交换
critical section; //临界区
lock = false; //退出区
remainder section; //剩余区
1.4.2.3 信号量机制实现进程互斥(PV操作)
//记录型信号量的定义
typedef struct{
int value; //剩余资源数
struct process *L; //等待队列
} semaphore;
P和V操作必须成对存在,否则会出现永远无法唤醒进程的情况
//信号量机制实现互斥
semaphore mutex=1;//表示进入临界区的“入场券”
P1(){
...
P(mutex); //若进入临界区的“入场券”没有了即mutex≤0,进程会进入阻塞态
critical section; //若有进程进入临界区
V(mutex);
...
}
P2(){
...
P(mutex);
critical section;
V(mutex);
...
}