您的位置:首页 > 其它

[操作系统学习笔记(4)] 进程同步,信号量

2016-10-05 11:48 344 查看

进程同步

        让多进程合作更加有序。

        在执行程序的时候,顺序并不是随意的,因为一个进程中间可能出现堵塞,这时需要停下来,并等待别的进程发来信号,再继续执行。

        接下来,我们需要建立生产者 - 消费者模型,来表述两个进程之间的合作,一个进程“放入”数据,一个进程“使用”数据:

#define BUFFER_SIZE 10
typedef struct{...} item;//数据的结构
int in = out = counter = 0;//队尾,队首,计数器

        对于生产者:

while(true) {
while(counter == BUFFER_SIZE);
//如果数据区放满了,停止生产
buffer[in] = item; //在队尾放入item
in = (in + 1)%BUFFER_SIZE;//队尾后移一位
count++;//计数器加一
}


        对于消费者:
while(true) {
while(counter == 0);
//如果数据区空了,等待数据
item = buffer[out];
//从队首取出item
out = (out + 1)%BUFFER_SIZE;
//队首后移一位
counter--;
//计数器减一
}


        以上代码进一步可以改写为:当消费者没有东西可以消费时,沉睡,而生产者生产出第一个物体时,唤醒消费者;当生产者生产的东西已经达到缓存所能容纳量,沉睡,而消费者使用物体使得缓存区不再满时,唤醒生产者。

    对于生产者:

while(true) {
if(counter == BUFFER_SIZE) sleep();
counter++;
if(counter == 1) wakeup(consumer);
}


    对于消费者:
while(true) {
if(counter == 0)sleep();
counter--;
if(counter == BUFFER_SIZE - 1) wakeup(producer);
}

        但是,这样仍然是不够的。我们假设两个生产者的情况:

        缓冲区已满,生产者P1沉睡,生产者P2也沉睡。消费者一次循环后,counter为BUFFER_SIZE - 1,发信号唤醒P1。再消费一次,变为BUFFER_SIZE - 2,所以P2没有按照预期被唤醒。这是存在问题的。

信号量

        在这种情况下,我们需要引入一个概念——信号量(semaphore)。

        信号量本身存储一些信息,并且我们会根据这个信息,来决定相应对象需要被唤醒还是需要睡眠。

        在这个例子中,信号量为n,若n>0,代表当前资源数量为n,若n = 0,则代表没有资源,若n < 0,则代表有n个消费者进程在等待资源。

        这样一来,再看上面的那个例子。对于两个生产者P1,P2,缓冲区满后,生产者P1睡眠,信号量为-1,生产者P2睡眠,信号量为-2。之后消费者唤醒P1,信号量为-1,再唤醒P2,信号量为0。

struct semaphore
{
int value; //记录资源个数
PCB *queue;
//记录等待在该信号量上的进程
}


       与信号量对应的两个函数:生产函数与消费函数。
P(semaphore s);//消费资源
V(semaphore s);//产生资源

P(semaphore s)
{
s.value--;
if(s.value<0){
sleep(s.queue);
}
}

V(semaphore s)
{
s.value++;
if(s.value<=0)
wakeup(s.queue);
}


保护信号量

       但是,这是否代表了使用信号量就不会出任何差错呢?我们需要考虑到可能产生的问题。

       我们列出信号量修改时生产者p1和p2的举动,它涉及到了寄存器。

     

       生产者p1

       register =empty;

       register =register – 1;

       empty =register;

 

       生产者p2

       register =empty;

       register =register – 1;

       Empty =register;

       假设信号量(empty)初始值为-1,那么经过p1和p2分别减一后,它最终的结果应该是-3。

       但是,所有指令并不一定按我们所想的顺序执行,如果它们按如下顺序执行(这是完全可能的):

       

       p1.register =empty;

       p1.register =p1.register – 1;

       p2.register =empty;

       p2.register =p2.register – 1;

       empty =p1.register;

       empty =p2.regster;

       这时,我们得到的信号量(empty)结果为-2。我们注意到,如果多个进程共同操作一个对象,而且对顺序没有约束的话,就很可能造成错误。这相当于一个程序还没有执行完,就被另一个程序打断。

       一个直观的想法是:上锁。也就是说,在一个进程写变量empty的时候,禁止其他进程访问empty。

      在有锁的情况下,我们再尝试按之前的顺序执行以上操作:

生产者p1

检查并给empty上锁

p1.register =empty;

p1.register =p1.register – 1;

 

生产者p2

检查empty的锁

 

生产者p1

empty =p1.register;

给empty开锁

 

生产者P2

检查并给empty上锁

       可以看出,在有锁的情况下,P2无法访问到empty,所以错误不会发生,最终得到的结果为正确的-3。

信号量临界区

        所谓信号量临界区,也就是一次只允许一个进程进入的一段代码。

        所以,我们面临的最大问题是:如何找出进程中的临界区代码

       实现好的临界区,有三个基本要求:

       ① 互斥进入:一个进程在临界区,其它进程不能进入。

       ② 有空让进:若干进程要求进入空闲临界区时,应尽快使一进程进入临界区。

       ③ 有限等待:从进程发出进入请求到允许进入,不能无限等待。

轮换法

       轮换法,也就是给每个人一个编号,一个进程执行完后,更换当前编号(turn),交由下一个进程执行。

       一个进程如果想要执行临界区代码,首先它需要等待turn变量等于它自身的编号。

       一个进程如果完成了临界区代码,它需要把turn变量改成其它编号。

        很显然,在轮换法机制下,进程是互斥进入临界区的, 因为只有编号等于turn的进程才能执行。

        但是,它是否满足有空让进呢?假设这样一个情况,turn = 1,而进程P1并不调度。我们知道进程在完成调度后才会修改turn,这时P2恰好又在等待使用临界区,结果因为没有轮到它,它也无法执行。最终的结果是没有人使用临界区。

标记法

        标记法,也就是当一个进程进入临界区时,令自己的标记(flag)为真,等到其它进程标记不为真后,开始执行;等到使用完毕后,再令标记为假。

进程P0:

flag[0] = true;

while(flag[1]);//等待

//进入

flag[0] = false;

进程P1:

flag[1] = true;

while(flag[0]); //等待

//进入

flag[1] = false;
        同样的,因为一个进程在使用时,有不让其他进程进入的标记,它也满足互斥进入。

        那么,我们再来考虑它是否满足有空让进,如果它按照以下顺序执行:

flag[0] = true

flag[1] = true

while(flag[1]);

while(flag[0]);

        由于flag0,flag1都为真,相当于两者都留下了标记,结果两者都无法进入临界区,因为它们都认为对方已经进入了临界区。所以,标记法也不满足有空让进。


非对称标记(Peterson算法)

        一种想法是,能不能结合以上两种方法(轮转法和标记法),实现更好的算法。

        在这种情况下,我们再来看看两个进程p0,p1进入临界区的伪代码:

进程p0:

flag[0] = true;

turn = 1;

while(flag[1]&& turn == 1);//等待

//进入

flag[0] = false;

进程p1:

flag[1] = true;

turn = 0;

while(flag[0]&& turn == 0);//等待

//进入

flag[1] =false;

        首先,它满足互斥进入,因为如果两个同时进入,会出现turn = 0 = 1的现象,这是不可能的。

        其次,它也满足有空让进,考虑进程p1不在临界区的情况,这时有flag[1] = false(也就是还没执行flag[1] = true), 或者 turn = 0(也就是已经执行了turn=0,进入等待),这时p0是可进入的。

        最终,它满足有限等待,如果p0想要进入临界区,p0不会等待很久,因为轮换,p1最多执行一次。

面包店算法

        之前讨论的都是两个进程,如果是多个进程,那么可以简单修改为:

    

        轮转:每个进程都获得一个序号,序号最小的进入

        标记:进程离开时序号为0,不为0即标记。

        它同样也满足互斥进入,有空让进,有限等待。

中断

      临界区只允许一个进程进入,换个角度想,也就是在这个进程执行的时候,我们需要禁止其它进程的调度。因为只有通过调度才能切换。

      换言之,就是在执行临界区时,关闭中断,其它进程无法进入,离开后,再打开中断。

      这个方法仅仅适用于单CPU,因为多CPU有多个中断寄存器。


硬件原子指令

      再换一个角度想,我们实际上是要保证执行临界区代码时,其它进程不切入,也就是说,我们需要把临界区代码作为一个原子操作,它只有两个状态,做与不做,而不存在做了一半。那么我们可以把这个指令设计到硬件中。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: