目录
进程同步
进程有异步性。异步性是指,各并发执行的进程都在以不可预知的速度向前推进,并且互不干扰。
但是进程之间也需要对一些公共资源进行访问、占用,由于进程的异步性,导致各个进程的指令执行有着多种情况并且不可控制,那么共享资源可能分配不合理导致程序不能正常执行。
进程同步:也称为直接制约关系。指的是一些(俩个以上)的进程在某些位置上需要按规定的次序执行,以防止资源访问不合理。因为对进程的执行次序有规定,所以同步也是对进程的一种制约。进程之间的之间制约关系就是源于他们之间的相互合作。
进程互斥
进程”并发“需要有“共享”的支持。各个并发执行程序不可避免的共享一些公共资源(磁盘、内存、摄像头等)。
资源共享的俩种方式
互斥共享:系统中虽然给多个进程提供共享资源,但是有些资源在同一时间只能被一个线程访问。这些资源的这种访问方式就是互斥的。
同时共享:系统中的某些资源同一时间允许多个进程同时访问,这种方式为同时共享方式。
临界资源:通过互斥的方式才能访问的资源。
进程互斥:当一个进程访问临界资源时,其他进程如果也想访问,就需要等待直到当前访问的进程结束并且释放资源后,其他进程才能访问(只允许一个进程访问,其他进程仍需排队等待)。
互斥:面对临界资源时进程之间相互排斥,一个进程”霸占“了临界资源后,就不允许其他进程使用了,只能“惦记着”。
进程互斥:
对临界资源的互斥访问,可以在逻辑上分为四个部分:进入区、临界区、退出区、剩余区。
do {
entry section; //进入区 在此区内对临界资源进行锁定(上锁),阻止其他进程访问。
critical section //临界区 访问临界资源的执行代码
exit section; //退出区 在此区内释放临界资源,可供其它进程访问了。
remainder section; //剩余区
}while (true)
思考:如果一个进程暂时进不去访问临界资源,那么该进程是否要一直占着处理机?该进程有没有可能一直访问不了临界资源?
为了实现临界资源的互斥访问,同时保证系统的性能,应遵循以下原则
-
空闲让进:如果临界资源没有被任何一个进程访问,那么就让一个请求访问临界资源的进程立即进入临界区。
-
忙则等待:如果临界资源的锁没有被释放,那么请求访问的进程必须等待。
-
有限等待:对于所有请求访问临界资源的进程,应该保证它们能够访问到临界资源。
-
让权等待:当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。
权:CPU执行权;
忙等待:是指CPU执行的进程在等待访问临界资源,无法执行其他工作。这时CPU在为该进程而服务,在一直等待,无法为其他进程服务。俗称“干等着”。
软件实现方法
单标志法
算法思想:一个进程在访问临界资源后,将访问权限交给另一个进程。也就是说进程的访问权限只能是另一个进程给它的。
代码示例:
int turn = 0; //表示允许访问的进程号
//p0进程
while(turn != 0); //进入区
critical section; //临界区
turn = 1; //退出区
remainder section; //剩余区
//p1进程
while(turn != 1); //进入区
critical section; //访问临界区
turn = 0; //退出区
remainder section; //剩余区
turn为访问权限,turn的值代表拥有权限的进程号。
因此,该算法可以实现“同一时间只允许一个进程访问临界区”。
如果现在turn = 0,代表此时临界资源的访问权限是属于0号进程的,但此时处理机将执行权分配1号进程,那么1号进程会一直轮询尝试进入临界区,由于他的权限只能是由具有权限的0号进程赋予,而0号进程没有执行权,那么1号只能一直轮询无法进入临界。只有当处理机将执行权重新分配给0号进程,而0号进程访问完临界区后将访问权限赋予1号进程,1号进程在执行的时候才能访问。
因此,该算法无法实现“空闲让进”。
双标志先检查法
算法思想:设置一个boolean数组,数组中的元素该号(用下标表示)代表进程是否有意愿访问临界资源。每个进程在访问临界资源时先检查(名字的由来)是否有其他进程想要访问临界资源,如果没有,将告诉其他进程自己想要访问临界区(flag[小标/进程号] = true,也就是上锁),然后进入临界区。
代码示例:
bool flag[2];
flag[0] = false, flag[1] = false;
//p0进程
while(flag[1]); 1
flag[0] = true; 2
critical section; 3
flag[0] = false; 4
remainder section;
//p1进程
while(flag[0]); 5
flag[1] = true; 6
critical section; 7
flag[1] = false; 8
remainder section;
这种算法相比较于单标志法,在进入区时,不仅要检查是否有其他进程在访问临界区,同时也表达自己的意愿,解决了单标志法的"空闲让进"的问题。
如果P0先执行1号指令检查是否有进程在访问临界区,因为没有进程有意愿访问,所以P0进程就可以表达自己的意愿执行2号指令,但是在执行之前处理机调度至P1进程执行,P1进程就可以执行5号指令,由于P0进程当时还没有执行2号指令,因此此时也没有进程有意愿访问临界区,那么P1进程就可以继续向下执行,当P1访问进入临界资源时,处理机又调度至P0进程执行,那么P0进程也向下执行,那么俩个进程将同时进入临界区。(按照152637...)
因此,这种算法违反了“忙则等待”的原则。
上面这种情况能出现的原因就是1、2条指令不是原子性的(没有“‘一气呵成”),也就导致违反了“忙则等待”。
双标志后检查法
算法思想:是双标志先检查法的改版。双标志先检查法由于检查和上锁没有一气呵成,并且是先检查后上锁,导致违反了“忙则等待”的问题。而双标志后检查法是通过先上锁后检查来保证“忙则等待”。
代码示例:
bool flag[2];
flag[0] = false, flag[1] = false;
//p0进程
flag[0] = true; 1
while(flag[1]); 2
critical section; 3
flag[0] = false; 4
remainder section;
//p1进程
flag[1] = true; 5
while(flag[0]); 6
critical section; 7
flag[1] = false; 8
remainder section;
处理机还是按照刚才的调度按照(152637...)顺序执行,在1号指令执行后调度,另一个进程执行5号指令后再切回来,那么此时这俩个进程谁也别想进入临界区了.....
双标志后检查法虽然保证了"忙则等待"原则,但是有违反了"空闲让进"和"有限等待"原则。
Peterson算法
算法思想:将单标志法和双标志法结合起来。在进程争抢资源互不让步的时候,要谦让对方,让对方先访问。
代码示例:
bool flag[2];
flag[0] = false, flag[1] = false;
int turn = 0;
//p0进程
flag[0] = true; 1 //表达自己的意愿
turn = 1; 2 //谦让对方
while(flag[1] && turn == 1); 3 //如果1号进程想要使用临界区,而且我还让给它了,就等他一会吧
critical section; 4
flag[0] = false; 5
remainder section;
//p1进程
flag[1] = true; 6
turn = 0; 7
while(flag[0] && turn ==0 ); 8 //如果0号进程想要使用临界区,而且我还让给它了,就等他一会吧
critical section; 9
flag[1] = false; 10
remainder section;
在0、1号进程都想要访问临界区时,都会互相谦让,哪个进程最后表达了谦让,哪个进程就会先丧失访问权,进行等待。
总结:Peterson算法解决了进程互斥问题,也遵循了“空闲让进”、“忙则等待”、“有限等待”的原则,但依旧没有保证“让权等待”。
硬件实现方式
中断屏蔽方式
利用开/关中断指令实现 与原语中的实现思想相同,即在某进程开始访问临界区到访问结束为止都不允许中断 也就是不能发生进程切换,因此不可能发生两个同时访问临界区的情况
进程切换是通过中断实现的,屏蔽中断也就意味着进程不会切换了。
优点:简单高效。
缺点:
-
不适合多处理机。
中断屏蔽只对执行关中断指令的处理机生效,多处理机系统中,使用中断屏蔽来阻止进程调度的目的就达不到了。
-
不适合用户进程
开/关中断指令是内核指令,只有在内核态才有权限执行,用户进程没有权限执行这个指令。
TestAndSet指令
简称TS指令或者是TSL指令 使用硬件实现的 执行的过程不允许被中断 只能是一气呵成 下面用C语言描述一下(只是描述一下逻辑,是有硬件实现这套逻辑的)
//bool类型共享变量lock表示当前临界区是否被加锁
//true表示被加锁 false表示未加锁
bool TestAndSet(bool *lock){
bool old;
old=*lock;//old用来存放lock原来的值
*lock=true;//无论之前是否被加锁 都把lock设置为加锁状态
return old;//返回原来的值
}
//下面是TLS指令实现互斥算法的逻辑
while(TestAndSet(&lock));//上锁 并 检查
临界区代码段
lock=false;//临界区执行完毕 解锁
剩余区代码段
TestAndSet(bool *lock)这个方法既检查了所得状态,同时也上了锁。在这段逻辑代码中,上锁并检查时调用这个方法代表着上锁和检查是一气呵成的。在软件实现方式中,双标志法的那俩种情况都是由于上锁和检查是分开执行的。
优点:简单高效,无需像软件实现方法那样严格检查是否会有逻辑漏洞,适用于多处理机环境。
缺点:不满足“让权等待”。
暂时无法进入临界区的进程会占用CPU并且执行TS指令
Swap指令
也叫Exchange指令 或者简称XCHG指令 从逻辑上看Swap指令和TSL指令没有太大的区别
//Swap指令的作用就是交换两个变量的值
bool Swap(bool *a ,bool *b){
bool temp;
temp=*a;
*a=*b;
*b=temp;
}
//下面是TLS指令实现互斥算法的逻辑
bool old=true;
while(old==true)
Swap(old,&lock);
临界区代码段
lock=false;//临界区执行完毕 解锁
剩余区代码段
优点与缺点与TS指令类似。
互斥锁
特性:
-
需要忙等,进程时间片用完才下处理机,违反“让权等待”。
-
优点:等待期间不需要进行进程的上下文切换,在多处理机上,若上锁时间段,则等待的代价很低。
-
常用于多处理机,忙等的进程只占用一个处理机,其他处理机正常运行处理工作,那么锁就会很快被释放。
-
不适用于单处理机,因为忙等的进程占用了唯一的一个处理机,那么占用临界区进程的作业无法处理,那么锁在短时间内不会释放,代价过大。
信号量机制
为什么要引入信号量?
1.之前的双标志法中,检查和上锁两个操作没有一气呵成,导致进程互斥、异步的过程中按照特定序列执行的话会出问题。
2.之前所有的方法都不能解决不遵循“让权等待”的问题。
信号量:其实就是一个变量,用这个变量来描述系统剩余资源的数量。一个信号量代表一种资源的剩余数量。
这个变量可以是一个整数,也就是下文的整型信号量,
也可以是一个结构体,也就是下文的记录型信号量。
用户进程可以使用系统提供的一对原语来对信号量进行操作,从而实现进程的互斥与同步。
一对原语:wait(P)和signal(S)。
wait、signal也简称为P、V操作。P、V来自于荷兰语proberen和verhogen。
信号量机制是1965年,荷兰学者Dijikstra提出的。
整型信号量
用一个整数形的变量来表示系统中某种资源的数量。
//eg:比如系统中有一台打印机.
int S = 1; //用它作为信号量,来表示打印机的数量。
void wait(S){ //P操作
while(S <= 0); //s<=0代表系统中可用的打印机数量没有了,那么while循环一直等待
s -= 1; //到这里说明有可用的打印机资源了,但是这个进程要使用一个,所以S -= 1
}
void signal(S){//V操作
s += 1;
}
//进程
wait(S); //进入区,检查并上锁
使用打印机 //临界区,使用资源
signal(S); //退出区,释放资源
优点:检查和上锁的俩个操作都在wait原语中,是不可中断的,那么就不会有双标志法中的特殊序导致的问题。
缺点:暂时不能进去临界区的进程依然在while里面自旋,不能遵循“让权等待”。
记录型信号量
使用数据结构来记录的信号量。
typedef struct {
int value; //剩余资源数
struct process *L; //等待队列,在V操作中可以通过等待队列唤醒进程
}semaphore;
记录型信号量中的P、V操作
void wait(S){
S -= 0;
if(S.value < 0){
block(S.L); //是一个原语,是进程从运行态变为阻塞态
}
}
void signal(S){
S += 1;
if(S.value <= 0){
wakeup(S.L); //是一个原语,用来唤醒进程,是进程从阻塞态变成就绪态。
}
}
记录型信号量机制的wait操作检查是否有可用的资源,如果没有则主动让自己阻塞(通过block原语实现),也就是说主动放弃了处理机的执行权。
signal中检查有可用资源的话,就唤醒进程。
注意:P、V操作必须成对的出现,缺一不可。
如果没有P操作,那么临界资源的互斥访问就没法保证了;
如果没有V操作,那么会导致资源永不释放,阻塞的进程将无法被唤醒。
总结:记录型信号量不仅能够保证进程互斥的访问临界资源,也能够实时记录系统资源的情况,然后动态的阻塞进程唤醒进程,从而遵循了前面方案中都没有解决的不遵循“让权等待”问题。
管程
为什么引入管程?
信号量机制虽然能够实现进程互斥与同步,也遵循"让权等待"的原则。但是对于不同资源,在申请这些资源时的顺序也要合理严谨,不然很可能会导致死锁。因此加大了程序程序员的编码难度。
管程的组成和基本特征
管程是一中特殊的软件模块,有这些部分组成:
-
在管程内部的共享数据结构。
-
对管程内部共享数据结构进行操作的一组过程。
-
对管程内部共享数据设置初始值的语句。
-
管程要有一个名字。
学习tip:管程与Java中的类概念很像。
第1个模块相当于类中的私有属性。
第2个模块相当于类中的get和set方法。
第3个模块相当于类中的构造方法或者静态代码块。
第4个模块相当于类名。
管程的基本特征:
-
管程内部的数据结构能能被管程内部的过程所访问。
-
每次仅允许一个进程在管程内执行某个内部过程。