临沂外贸网站建设舆情危机公关公司
进程需要与计算机中的其他进程和设备进行协作,因为资源是需要共享的,而且并发可以提高效率,有利于资源利用。
程序可以调用 fork() 创建一个新的进程,操作系统会分配一个新的并且唯一的进程 ID,在内核中,操作系统会调用并运行
new_pid=next_pid++
,翻译成机器指令,就是:
LOAD next_pid Reg1
STORE Reg1 new_pid
INC Reg1
STORE Reg1 new_pid
但是如果有两个进程都执行 fork() 操作,假定next_pid = 100,一个进程得到的 ID 应该是 100,另一个进程的 ID 应该是 101,next_pid 应该增加到 102,但是可能遇到以下错误情况:
需要解决这种不确定性,保证那四条指令不被打断、不被切换——原子操作:是指一次不存在任何中断或失败的操作,要么该执行成功结束,或者根本没有执行,并且不应该发现任何部分执行的状态。
实际上操作往往不是原子的,有些看上去是原子操作,实际上不是,比如 x++ 这样简单的语句,实际上是由三条指令构成的,有时候连单条机器指令都不是原子的。
一些概念:
- 🐱🐉临界区(Critical Section)
是指 进程中的一段 需要访问共享资源并且 当另一个进程处于相应代码区域时 不会被执行的代码区域。
临界区的访问规则:
(1)空闲则入:没有进程在临界区时,任何进程可进入。
(2)忙则等待:有进程在临界区时,其他进程均不能进入临界区。
(3)有限等待:如果一个线程 i 处于入口区,那么在 i 的请求被接受之前,其他线程进入临界区的时间是有限制的。
(4)让权等待:不能进入临界区的进程,应释放CPU(如转换到阻塞状态)。
临界区的实现方法:
(1)禁用中断
没有中断,就没有上下文切换,因此就没有并发。在进入临界区之前,把中断屏蔽了,代码就会是确定的结果,退出临界区时,再重新使能中断。不过这样做的话,没法儿及时和外设交互了,可能导致其他进程处于饥饿状态。而且临界区的时间长短是不确定的,那么可能对系统造成了很大的影响。再有,中断机制对于 多 CPU 系统是无法解决互斥问题的。
(2)软件方法
除了用在操作系统,也用在分布式系统中。假设两个进程的代码都是:
do {enter sectioncritical sectionexit sectionreminder section
} while (1);
需要任何时刻只有一个进程在临界区中执行。
① 方法一
进程 1 的代码:
int turn = 0;//共享变量初始化
turn == 1;//表示进入临界区
do {while(turn != 1)critical sectionturn = 0;reminder section} while(1);
进程 2 应该和它是对称的,也就是 turn 由 0 变为 1,而这样可以满足 互斥【当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并且访问任何相同的共享资源。】,因为 turn 只有一个值嘛,但是有时候不能满足 “前进” 属性,比如 进程 1 做其他的事情了,不再访问临界区了,(这时 turn 已经被赋为 0 了)但是线程 2 想要继续在临界区运行,但是因为不满足 turn !=0 的条件,所以永远进不去代码块。
②方法二
int flag[2];
flag[0] = flag[1] = 0;
i、j 分别表示两个进程,比如flag [i] 为 1 表示 进程 i 想进入临界区执行。
flag[i] == 1 //指示进程是否准备好进入临界区
do {//对于进程 i,如果 flag[j]=1 ,说明进程 j 想进入临界区,//所以需要等待进程 j 执行完,直到 flag[j] 被赋为 0 了,//也就是说 进程 j 执行完了,退出请求了,进程 i 才退出这个 while 循环while (flag[j] == 1) ;flag[i] = 1;critical section//说明进程 i 无请求啦flag[i] = 0; remainder section} while (1);
但是方法二的问题在于:不能满足 “互斥” 属性,因为在初始时, flag[i] 和 flag [j] 都是 0,就意味着,两个进程都会跳过 while 循环,这样的话两个进程都会给各自的 flag 赋值为 1 ,同时都会执行临界区代码,就没有满足 “互斥”咯。
修改代码为:
do {//交换了顺序,把赋值为 1 放在第一步执行,while 循环在后flag[i] = 1;while (flag[j] == 1) critical sectionflag[i] = 0; remainder section} while (1);
但是这样的话,在任何时刻不会有两个进程一起进入 while 循环了,这样就出现了死锁,比如说进程 1 先把 flag[1] 赋了 1 ,然后产生了进程切换,进程 2 把 flag[2] 赋了 1,这样的话,再下来无论轮到哪个进程,都跳不出 while 循环了,就形成了死锁。所以说,这样调换 flag 赋值和 while 的顺序,是无法解决问题的。
③ 方法三:
//使用两个共享数据项
//表示进入临界区
int turn;
//表示进程是否准备好进入临界区
boolean flag[];
进入临界区代码:
flag[i] = true;
turn = j;
while (flag[j] && turn ==j)
退出临界区代码:
flag[i] = false;
进程的代码:
do {flag[i] = true;turn = j;while ( flag[j] && turn == j);CRITICAL SECTIONflag[i] = false;REMAINDER SECTION} while (true);
既能满足互斥,又能满足“前进”、有限等待。
🎭 Bakery 算法
进入临界区之前,进程接受一个数字,得到数字最小的进入临界区,如果进程 i 和 进程 j 收到相同的数字,那么如果 i<j,Pi 先进入临界区,否则 Pj 先进入临界区,编号方案总是按照枚举的增加顺序生成数字。
更高级的抽象,是基于硬件的原子操作指令来实现,比如:
-
-
- 锁🔒,获得锁就是进入临界区;释放锁就是退出临界区;使用锁来编写临界区。可以通过 测试和置位(Test-and-Set )指令 和 exchange 指令【交换内存中的两个值】来实现。
-
Test-and-Set 指令 (从内存中读取值,测试该值是否为 1 ,然后返回真或假,内存值置为 1 。):
boolean TestAndSet (boolean *target){boolean rv = *target;*target = true;return rv:}
exchange 指令:
void Exchange (boolean *a, boolean *b){boolean temp = *a;*a = *b;*b = temp:}
以上两个指令都由硬件保证不会被打断(即不会有中断或上下文切换)。
-
-
-
- 使用 TS 指令实现自旋锁(spinlock)
-
-
class Lock {// 0 表示还没有进程进入临界区int value = 0;
}
想进入临界区需要先获取锁,开始 value 是 0 ,代表还没有进程进入临界区,做完 test-and-set 之后,value 值会变为 1 ,test-and-set 返回值为 0 ,然后就会跳出 while 循环,进入临界区执行;如果另一个进程也想进入临界区,也会调用 Lock::Acquire ,这时 value 值是 1 ,那么 test-and-set 返回值是1 ,就会在 while 循环里,出不来🤦 ,所以就会等待,这种等待叫“忙等” (忙等的话,进程在等待时消耗 CPU 资源较多。怎么办呢?我们知道,进程在等待某个事件时,可以睡眠,我们就让进程进不去临界区时,就阻塞睡眠,挂到等待队列中,让出 CPU 让其他进程去使用,然后还要有相应的唤醒操作,使得进入临界区的进程重新判断是否能跳出循环,能跳出的话就进入临界区了,不能的话就睡眠😴。):而退出临界区,就是锁的 释放,只需要把 value 赋为 0 即可,其他等在 Lock::Acquire 的进程就会跳出 while 循环啦,代码如下:
//如果锁被释放,那么TS指令读取0并将值设置为1
//锁被设置为忙并且需要等待完成——忙等
Lock::Acquire() {while (test-and-set(value)); //spin
}
Lock::Release() {value = 0;
}
-
-
-
- 使用 exchange 实现锁:
-
-
int lock=0; //共享数据初始化为0
int key;
do{key=1;while(key == 1) exchange(lock,key);//执行完 exchange 之后 lock 变为 1 ,key 变为 0,就可以退出 whilecritical section lock = 0;remainder section}
- 死锁:
两个或以上的进程,在相互等待完成特定任务,而最终没法将自身任务进行下去。 - 🤤饥饿:
一个可执行的进程,被调度器持续忽略,以至于虽然处于可执行状态却不被执行。