前言

本文基于《操作系统概念第9版》整理信号量部分。笔者在阅读过程中,很难理解其意,故本文通过梳理内容,并以加深理解为目的。

信号量

情景

描述

我们用双标志法实现两个进程的互斥方式时,会出现两个进程同时进入临界区或两个进程都无法进入临界区的问题。原因是:当进程检测到另一个进程的标志位为false之后,将自己的标志位设置为true。在检测完成、设置标志位之间,进程由于并发执行可能出现中间执行其他进程指令的情况。这就使得另一个进程的下一次检测检测到未设置值的false,从而再次进入临界区。

p0:                 p1:
1 while(flag[1]);   2 while(flag[0]);
3 flag[0]=true      4 flag[1]=true

//临界区            //临界区

image.png

解决方法

如果p0和p1的检测和设置两个步骤无法拆分,则不存在在p0能访问临界区的情况下,flag[0]还为false的情况,p1只能没有“空隙”插入自己指令。这种几个步骤执行的时候,不会被拆开执行的操作是原子操作。这不是原子操作的定义,只能说这种操作可以看成原子操作。

将检测和设置的步骤实现成原子操作。可以从软件和硬件层面实现。

硬件
提供机器指令完成两个步骤。
如test_and_set和compare_and_swap。
因为两条指令实际在底层被翻译成多条指令,问题的原因是并发使得不同进程指令可能交替执行,从而导致错误。如果只用一条机器指令则问题解决。

引入

定义一个整型变量S。它只能通过两个原子操作waitsignal修改。
wait(S)定义如下

wait(S)
{
    while(S<=0);
    S--;
}

signal(S)定义

signal(S)
{
    S++;
}

简单理解原子操作,就是wait里的所有语句不会和其他进程调用的wait穿插执行。不同进程的wait调用并行执行。再单独理解wait内部的逻辑:检查能否减一,不能则等待,等待完成后减一。

定义

信号量可以当作用于控制访问具有多个实例的某种资源。信号量的初值为可用资源数量。当进程需要使用资源时,对信号量执行wait操作(减少信号量计数,表示使用了一个),当进程释放资源时,对信号量执行signal操作(增加信号量计数,表示归还一个)。当资源数量为0时,等待资源。

信号量的实现

问题

引入中定义的wait,有以下问题

  1. 使用忙等,浪费CPU时间
  2. 只有S值只可以取0和1才是原子操作。如果S=2;支持两个进程同时修改S的值,由于S--不是原子操作,结果不一定为S-2后的值

    实现方式

    定义如下信号量
    typedef struct
    {
     int value;
     struct process *list; //等待进程链表
    }semaphore;
    PV操作定义
    wait(semaphore*S)
    {
     S->value--;
     if(S->value<0)
     {
         //添加到阻塞链表
         block();
     }
    }
    
    signal(semaphore*S)
    {
     S->value++;
     if(S->value<=0)
     {
         //从阻塞链表移除一个进程P
         wakeup(p);
     }
    }
说明

使用结构体表示信号量
包含一个整型变量和等待队列。当进程无法获取资源的时候,阻塞进程,并加入到阻塞列表。等其他进程使用完释放资源后,从阻塞队列中选出一个进程唤醒继续执行。这样就解决了忙等的问题。
是否是原子操作
wait
有一个变化,先执行减一操作,然后检查是否满足继续执行的条件。咋一看会认为是

wait(S)
{
    S--;
    while(S<0);
}

分析这种情况S为1的时候,两个进程同时执行S--,S最终结果可能是-1或0。-1的时候,会有一个进程等待,都为0的时候,两个进程都继续执行。
很遗憾,阻塞方式也没法避免S->value--的并发问题,只能说信号量这里默认把value的修改当作原子的了,也许原子变量在这里能用上
signal
image.png

唤醒进程的条件是S->value<=0。考虑value=0的情况,这时候value初始值为-1,说明有1个进程等在当前信号量上,所以也需要唤醒

理解

可以以餐厅场景理解信号量。蟹宝王里有10个座位,顾客来了看到有位置则选一个坐下。如果没有空位了,则排队等待。等有顾客吃完,腾出位置之后通知排队的顾客,“伙计,有位置了,你们看谁先去吃”。当前先到先服务是最容易想到的调度算法,但是实际可能比这复杂。

死锁与饥饿

死锁

看过《斗破》的小伙伴,对净莲妖火一定印象极深。寻找净莲妖火的地图被分成了很多块,分散在斗气大陆。最后由多个不同的大势力持有。每个势力都要凑齐完整地图,才能进入秘境。而且每个势力都想自己独自探寻秘境,因此不会拿出自己的残图给其他势力。因此,大家都得不到完整地图,也无法收取异火。

当多个进程需要同时拿到某些资源,且资源分散在各个进程上时,若没有外界调控,这些进程将永久等待!这就是死锁。理解死锁从结果和原因一起思考。

饥饿

饥饿在进程调度就有描述。一直拿不到某个资源的进程,处于饥饿状态。进程调度的时候进程获取不到cpu资源,信号量这里指进程一直获取不到某个信号量,与调度算法有关。采用LIFO(后到先服务)调度,第一个等待的人饿死算了。

经典同步问题

理解不了,也要清楚大致流程吧!

有界缓冲问题

用PV操作解决生产者、消费者问题
假设缓冲池中有n个缓冲区,生产者消费者以缓冲区大小为单位取放数据。

定义变量
int n
semaphore empty=n 空缓冲区剩余个数
semaphore full=0 有效缓冲区的个数
semaphore mutex=1

image.png

几个错误情况
为什么要用三个信号量,只用一个mutex,把整个缓冲池作为一个资源不行吗?

只用一个mutex,进入临界区之后,还要检查是否空闲空间。不用信号量就是用while死等

mutex的访问顺序放到empty和full之外是什么情况?

mutex放在外面,导致死锁咯。例如消费者读数据要拿到mutex和full。假如只拿到了mutex,就需要等生产者增加full。但是生产者要往缓冲池放数据得拿到mutex和empty,这时候消费者又不释放mutex,两个锁住了。





修狗
6 声望0 粉丝

而极少数上天的宠儿,他们在年少的时候就能隐约感受到那片天空的微光,这种光诱惑他们仰之弥高,钻之弥坚。多令人向往呀![链接]