信号量机制

思考

在前一讲中的进程互斥解决方案中,哪两个问题最突出?

1、在双标志先检查法中,由于进入区的“检查”和“上锁”操作不是一气呵成的原子操作,这样就容易导致死锁。

2、所有的解决方案都未实现“让权等待”的准则。

荷兰学者Dijkstra提出了卓有成效的信号量机制用以解决进程互斥与同步的问题。用户进程可以通过OS提供的一对原语来操作信号量,从而方便的实现进程同步与互斥。

定义

信号量

信号量可以理解为一个变量(可以是一个整数或更复杂的记录型变量),我们可以用信号量来表示系统中资源的数量。比如:系统中只有一台打印机,就可以设置一个初值为1的信号量。

原语

原语是一种特殊的程序段,其执行无法被中断。原语由关中断/开中断指令实现。前面一节中的软件解决方法的问题主要在于进入区的各种操作不具备原子性,因此只要把进入区、退出区的操作都用原语实现,则可以解决问题。

P、V操作

即上文中的“一对原语”,指的是wait(S)和signal(S)原语,人们常称为PV操作(首字母来自于荷兰单词)。P(S)等价于wait(S),V同理。

整型信号量与记录型信号量

整型

整型PV操作可以描述为:

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

signal(S){
    S=S+1;
}

注意:

1、S表示空闲资源数量(锁方法中仅有一个),P操作申请一个资源,V操作释放一个资源。

2、PV操作必须为原子操作,对单处理机可以用禁止中断实现,而多处理机需要软件协同解决。

3、PV操作必须成对出现。

缺点:“忙等“,所以把这种PV操作称为“忙等”PV操作。

关于wait原语的忙等问题

整型信号量在wait这个原语当中的这两个操作其实逻辑上来看和双标志先检查法当中先检查后上锁其实做的是一样的事情,大家可以对比着来串联一下这两个知识点。那么因为它是用一个原语来实现的检查和上锁,所以它这两个操作一气呵成,就避免了双标志先检查法那种就是两个进程同时进入临界区的问题。

如果一个进程暂时进不了临界区,也就意味着它被卡在wait这个原语的这个while循环里,既然wait原语它是不可被中断的,也就意味着当前正在执行while循环的这个进程是不是一直不会被切换呢?这个地方确实是一个让人感觉不太严谨的地方。但是我们在很多经典的教材当中,其实它们都是这么写的,所以这个地方我们姑且认为它没有问题,不会导致一个进程一直占用处理机的情况吧。那在整型信号量当中其实比较容易考察的是它存在的问题这一点,经常会把这个整型信号量和记录型的信号量做对比。那么它俩的区别就在于整型信号量不满足“让权等待”,会发生忙等。

记录型

/*记录型信号量结构体定义*/
typedef struct {
    int value;    //剩余资源数量
    struct process *L;    //等待队列
}semaphore;

由于整型信号量忙等的缺陷,又提出了记录型信号量如上定义。

//某进程需要使用资源时,通过wait原语申请
void wait(semaphore S){
    S.value--;
    if(S.value<0){
    //如果剩余资源数量不够,
    //使用block原语将进程从运行态转入阻塞态,
    //并将其挂到S的等待队列中(即阻塞队列)
        block(S.L);
    }
}

//进程使用完资源后,通过signal原语释放
void signal(semaphore S){
    S.value++;
    if(S.value<=0){    
    //若释放进程后,还有别的资源在等待资源,
    //则唤醒等待队列中的一个进程使其从阻塞状态转为就绪
        wakeup(S.L);
    }
}

S.value初值为1时,表示只允许一个进程访问临界资源,此时的信号量转化为互斥信号量。

信号量机制实现进程互斥

1、分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应该放在临界区)

2、设置互斥信号量mutex,初值为1

3、在临界区前执行P(mutex)

4、在临界区后执行V(mutex)

semaphore mutex=1;

P1(){
    ...
    P(mutex);    //使用前加锁
    临界区代码
    V(mutex);    //使用后解锁
    ...
}

P2(){
    ...
    P(mutex);
    临界区代码
    V(mutex);
...
}

信号量机制实现进程同步

最后更新于