【简记】Operating System——process synchronization

来源:互联网 发布:淘宝卖霉菌龟头炎药品 编辑:程序博客网 时间:2024/04/30 10:15

This memo is based on the course of Dr.Li with Operating System as the reference book.

本章内容:


6.1 背景

进程之间会共享数据,共享数据的过程中就可能出现数据不一致的情况。

如之前提到的生产者-消费者模型,在并发环境下,可能会出现线程(进程)不安全的情况,因为有一些非原子性的操作。例えば、变量counter,counter++和counter–这种看似不会出问题的语句,因为其底层实现的原因,会导致线程不安全的情况。

取指令,计算指令,写指令
这里写图片描述

上面的6个语句可能交叉执行,导致数据问题。


6.2 临界区问题

每个进程中有一个代码段称为临界区,在该区中进程可能会改变一个共同变量,更新一个表,写一个文件等。

这里写图片描述

解决临界区问题必须满足如下要求:

  • 互斥:同一时间内只有一个进程可在自己的临界区执行
  • 前进:如果没有进程在临界区内且有进程需要进入临界区,那么只有在那些不在剩余区内执行的进程可参加选择,且这种选择不能无限推迟
  • 有限等待:从一个进程做出进入临界区的请求,直到该请求允许为止。其他进程允许进入其临界区的次数有上限。

算法1(满足互斥,不满足前进)(why)

do{    while(turn != i);    ctrtical section    turn = j;    remainder section;}while(1)do{    while(turn != j);    ctrtical section    turn = i;    remainder section;}while(1)

算法2(满足互斥,不满足progress)(why)

进程pido{    flag[i] = true;    while(flag[j]);    critical section    flag[i] = false;    remainder section}while(1)进程pjdo{    flag[j] = true;    while(flag[i]);    critical section    flag[j] = false;    remainder section}while(1)

6.3 Peterson算法

两个进程P0和P1

两个进程间共享turn和flag

do{    flag[i] = true;    turn = j;    while(flag[j]&&turn==j);    临界区    flag[i] = false;    剩余区}while(true)do{    flag[j] = true;    turn = i;    while(flag[i]&&turn==i);    临界区j    flag[j] = false;    剩余区}while(true)

这里写图片描述


补充:Lamport面包房算法

n个进程的临界问题

该算法的基本思想源于顾客在面包店中购买面包时的排队原理. 顾客在进入面包店前, 首先抓一个号, 然后按照号码由小到大的次序依次进入面包店购买面包. 这里, 面包店发放的号码是由小到大的, 但是两个或两个以上的顾客却有可能得到相同的号码(使所抓号码不同需要互斥), 如果多个顾客抓到相同的号码, 则规定按照顾客名字的字典次序进行排序, 这里假定顾客是没有重名的. 在计算机系统中, 顾客就相当于进程, 每个进程有一个唯一的标识, 我们用P的下面加一个下标来表示. 例如: 对于 Pi和Pj, 如果有i < j, 则先为Pi服务, 即Pi先进入临界区

/*boolean choosing[n];表示进程是否在取号int number[n];记录每个进程取到的号码这些数据结构分别初始化为false0,为了方便,定义如下符号:若a<c或a==c和b<d同时成立,(a,b)<(c,d)即如果两个进程的排队号相同,则按进程的标识号进行比较*/do{choosing[i] = true;number[i] = max{number[0],number[1],…,number[n-1]}+1;//选号码,因为这里不是原子操作,所以可能会得到相同的号码choosing[i] = false;for(j = 0; j<n; j++){while (choosing[j]); // while ((number[j] != 0) && (number[j],j)<(number[i],i)); // 当number[j]==0,说明该进程j并没有进临界区的请求,即可跳出while;当number[j]==number[i],两个进程的排队号相同,则进程号更大的会被留在while循环中};//临界区number[i] = 0;//其余部分}while(1);
理解:第一个试图进入临界区的进程Pi在没有其它进程竞争时顺利进入其临界区。满足了有空就进的要求。当有竞争者Pk(i<k),且选的号码相同时,比较进程的名称,通过语句while ((number[j] != 0) && (number[j],j)<(number[i],i));来选择进入的进程。在Pi进程中j=i时,number[j]=number[i],且j=i所以(number[j],j)<(number[i],i)不成立,退出while语句。j==k时,number[k]=number[i],且k>i所以(number[j],j)<(number[i],i))不成立,退出while语句。对与其它非i和k的j值,number[j]!=0不成立,退出while语句。在Pk进程中j=i时,number[j]=number[k],且j<k,所以(number[j],j)<(number[i],i))成立,故进程Pk陷在该语句中,直到Pi退出临界区执行语句number[i]==0时才允许Pk进程进入其临界区(因为此时(number[j] != 0)这个条件不满足)。这里也有可能发生Pi退出临界区后继续获得CPU使用权并得到了一个新的排队号,但此时它所得到的排队号是max加1,所以Pi的排队号大于Pk,可以跳出while循环。所以满足了互斥和有限等待的要求。
为什么choosing数组是必要的?https://www.zhihu.com/question/23336411/answer/44756007著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。可以想像这样一个例子: 现在排号已经排到了n,这个时候来了两个人i和j (i < j),他们都想领取n+1这个编号。如果去掉choosing,可能出现这样一种情况:虽然这两个人同时看到了现在的最大编号为n,也都想把自己的编号设置为n+1,但是j眼疾手快,抢在i之前去检查。i反应太迟钝了,以至于j检查到他的时候,他手上的编号纸还是空白的(number[i]=0),于是j顺理成章优哉游哉进店买面包了。这个时候慢吞吞的i终于在编号纸上写上了n+1,于是他也开始检查别人。当它检查到j的时候,发现他们两个的编号一样(number[i]=number[j]=n+1),而且自己的优先级还比j高(i<j),于是他也理直气壮地进店买面包了。一店不可容二主,于是杯具了。为什么choosing就可以阻止杯具的发生呢?我们穿越回j眼疾手快开始检查别人的时刻,当他检查到i的时候,发现choosing[i]=true(因为这个时候i还在慢吞吞没有写编号,没写编号choosing[i]就一直是true啦),所以j就不敢进店了,他必须等着choosing[i]被设成false。过了很久很久,i终于在自己的编号纸上写了编号n+1,也随之把choosing[i]设成了false。这个时候j高兴了,因为至少choosing[i]=false这道坎跨过去了。。。但是天不遂人愿,他随即发现虽然他们两个的编号一样,但是i的优先级比他的高(i<j),所以他又得乖乖等着。于是这个时候i逆袭的机会来了有木有!i火速检查别人,检查到j的时候发现choosing[j]=false,两个人的编号相同且自己的优先级更高,所以果断进去买面包。等i买完了出了店,他把自己的number[i]设成0,而这也正是j苦苦等待的。。。于是j也进店买了面包。至此,i和j一前一后买到了面包,很和谐很有爱,over.

6.4 硬件同步

之前的临界区问题产生的原因,都在于代码在执行的过程中会被CPU的调度所打断,所以会出现不同步的问题。

可以从软件和硬件的角度来解决,体现原子操作。

许多现代计算机系统提供了特殊硬件指令以允许原子地(不可中断地)检查和修改字的内容或交换两个字的内容。

下面的代码用TestAndSet解决了互斥,但是没有解决有限等待。(有可能某个线程一直得到CPU)全局布尔变量lock初始为false

// TestAndSetboolean TestAndSet(boolean target){    boolean rv = target;    target = true;    return rv}// 任意进程数量都可以满足do{    while(TestAndSet(lock));    //ctrical section;    lock = false;    //remainder section;}while(true);

swap函数,全局布尔变量lock为false,每个进程有一个局部boolean变量

do{    key = true;    while(key==true)        swap(lock,key);    //critical section;    lock = false;    //remainder section;}while(true);

6.5 信号量

信号量是一个整数变量,除了初始化外,它只能通过两个标准原子操作:wait()和signal()来访问

当信号量是一个二进制数时,值只有0和1两种,有的系统也将二进制信号量称为互斥锁。
这里写图片描述
这里写图片描述
要求无法进入临界区的进程忙等待,这种类型的信号量也被称为自旋锁。自旋锁的优点是在等待锁时不进行上下文切换,在多处理器系统中,一个线程在处理器自旋时,另一个线程可以z在另一处理器上在其临界区执行。此种情况下信号量的值不会为负。

另一种信号量定义如下:
每个信号量都有一个整型值和一个进程链表,当一个进程必须等待信号量时,就加入到进程链表里。

wait(S){    S--;    if(S<0){    add this process to waiting queue;    block();    }}signal(S){    S++;    if(S<=0){    remove a process P from the waiting queue    wakeup(P) // 即上面的wait函数    }}

S的值可以小于0,绝对值为等待的线程数

Semaphore S;do{    wait(S);    //critical section;    signal(S)    //remainder section;}while(1);

===
多CPU下实现wait和signal的原子性

单CPU可以“关中”操作(关闭中断),多CPU要借助于锁


6.5 死锁和饥饿
这里写图片描述


6.6 经典同步问题

6.6.1 生产者消费者问题
6.6.2 读者写者问题
6.6.3 哲学家进餐问题


6.7 管程