更多优质内容
请关注公众号

操作系统入门(七)进程互斥和同步的完美解决方案——信号量机制-张柏沛IT博客

正文内容

操作系统入门(七)进程互斥和同步的完美解决方案——信号量机制

栏目:其他内容 系列:操作系统入门 发布时间:2022-02-14 10:36 浏览量:1482
本系列文章目录
展开/收起

信号量机制

上面说的4种软件互斥方法和3种硬件互斥方法的共同缺点是不满足 让权等待 原则。而信号量机制可以解决这个问题。

信号量S是一个用来表示系统中某种资源的可用数量的变量。

信号量机制是操作系统提供的一对用来操作信号量的 wait(S) 和 signal(S) 原语,通过 wait/signal 原语可以很方便的实现进程互斥和同步。


对信号量的操作只有3种:初始化,wait操作 和 signal操作。

从前面的知识我们知道,原语是不可中断的,原语是通过“关中断/开中断”指令实现的,关中断/开中断之间的指令就是原语指令集。因此 wait 和 signal 内的代码也是被关中断/开中断包裹着,是不可中断的,执行时不会发生进程切换的。

wait 和 signal 原语简称为P、V操作。

信号量可以分为 整型信号量(信号量是一个整型) 和 记录型信号量(信号量是一个对象,一个结构体)。因此,信号量机制也分为两种。




整型信号量

用一个整数型的变量作为信号量,用来表示系统中某种资源的可用数量。

下面是整型信号量机制的逻辑实现:



从逻辑上看,整型信号量机制类似于 双标志先检查 的互斥机制,即先检查后上锁(S--可以理解为上锁这一过程),只不过在这里,检查和上锁是原子操作,不可分割和中断。

该机制的问题依旧是不满足 “让权等待”,进程等待资源释放过程中会占用CPU。

稍微提一个问题:我们知道wait是一个原子操作,wait内的代码都不会接收中断,不会发生进程切换。如果wait陷入while循环,即使该进程时间片用完了也不会切换进程,这个CPU会一直卡在这个进程上,无法执行其他任务。除非其他进程在其他CPU执行了signal释放临界区,原来那个CPU执行的wait才能结束。但这样一直占用CPU其实不合理。这个问题留给读者,本文不过多解读,我们认为进程陷入wait的while循环后,系统依然能够在该进程时间片结束时让该进程被迫让出CPU即可。




记录型信号量

记录型信号量除了记录资源的可用数量,还包含了一个等待队列(阻塞队列)。

wait 操作申请资源时,如果可用资源数量不够,会使用 block原语 使进程进入阻塞态,主动让出CPU,并将该进程PCB挂到信号量S的等待队列中。

signal 操作给资源释放锁时,若还有别的进程 在等待这种资源,则使用 wakeup 原语 唤醒等待队列中的 一个进程,该进程从阻塞态变 为就绪态,让该进程进入临界区。(如果调用signal时没有其他进程阻塞,则不做唤醒操作)。

下面是整型信号量机制的逻辑实现,类似于:


使用记录型信号量的信号量机制实现互斥,满足了 ”让权等待“ 的要求,真正意义上完美实现了互斥。

信号量小结:无论是哪种类型的信号量,对信号量的两种操作分别表示:

wait:申请一单位的资源并对这一单位资源上锁;

signal:释放一单位的资源;


问题:能否按如下所示代码实现wait 和 signal?

func wait(S){
    if(S.value <= 0){ // 检查资源个数小于等于0就阻塞
        block(S.L)
    }
    S.value--
}

func signal(S){
    S.value++
    if(S.value <= 1){   // 如果资源个数等于1,说明本进程是最后一个进入临界区的进程
        wakeup(S.L)
    }
}

答:

两种方案的区别在于wait是先上锁后检查还是先检查后上锁(是在进程表达使用意愿时上锁,还是真正可以使用资源时上锁)。

后者方案没有前者方案的wait和signal的实现完美,因为后者方案释放资源时,S.value <= 1 说明本进程是最后一个进入临界区的进程,此时可能有其他进程被阻塞,也可能没有,如果是后者,那么调用wakeup()就会做无用功。而前者方案中signal则可以明确知道 本进程在归还资源后仍然有 S.value <= 0,说明有其他进程在等待资源。因此进程只要表达自己想要使用资源的意愿即可对 S.value-- ,而非真正使用时才对 S.value--。




用信号量实现进程互斥和同步问题

信号量实现进程互斥的基本逻辑如下:

/* 信号量机制实现互斥 */
semaphore mutex = 1;	// 初始化信号量为资源数量

P1(){
    ...
    P(mutex);
    临界区...
    V(mutex);
    ...
}

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

注意:对不同的临界资源需要设置 不同的互斥信号量。 P、V操作必须成对出现。缺少 P(mutex) 就不能保证临界资源的互 斥访问。缺少 V(mutex) 会导致资源 永不被释放,等待进程永不被唤醒。


信号量实现进程同步的基本逻辑如下:

1. 分析什么地方需要实现“同步关系”,即必须保证不同进程的“一前一后”执行的两个操作

2. 设置同步信号量 S, 初始为 0

3. 在进程1的“前操作”之后执行 V(S),在进程2的“后操作”之前执行 P(S),这样就可以保证如果CPU先执行后操作时进程2会被阻塞

我们可以将进程同步中的信号量S看做“某种虚拟的资 源”,刚开始是没有这种资 源的。进程2的后操作需要使用这种资源, 而又只能由进程1的前操作产生这种资源。

假如存在两个进程P1和P2,P1要执行代码1~3,P2要执行代码4~6。要求执行顺序为 1~2->4~6->3。

/* 信号量机制实现同步 */
semaphore S = 0;	// 初始化信号量为资源数量

P1(){
    代码1;
    代码2;
    V(S)
    代码3;
}

P2(){
    P(S)
    代码4;
    代码5;
    代码6;
}

若先执行到 V(S) 操作,则 S++ 后 S=1。之后当执行到 P(S) 操作 时,由于 S=1,表示有可用资源,会执行 S--,S 的值变回 0, P2 进程不会执行 block 原语,而是继续往下执行代码4。

若先执行到 P(S) 操作,由于 S=0,S-- 后 S=-1,表示此时没有 可用资源,因此P操作中会执行 block 原语,主动请求阻塞。 之后当执行完代码2,继而执行 V(S) 操作, S++,使 S 变回 0, 由于此时有进程在该信号量对应的阻塞队列中,因此会在 V 操作中执行 wakeup 原语,唤醒 P2 进程。这样 P2 就可以继续 执行 代码4 了。



再看一个简单的信号量实现同步的问题。

进程 P1 中有句代码 S1,P2 中有句代码 S2 ,P3中有句代码S3 …… P6 中有句代码 S6。这些代码要求 按如下前驱图所示的顺序来执行:

1. 要为每一对前驱关系各设置一个同步信号量

2. 在“前操作”之后对相应的同步信号量执行 V 操作

3. 在“后操作”之前对相应的同步信号量执行 P 操作


实现逻辑如下:



小结:无论是用信号量实现互斥还是同步,关键都在于“阻塞”。

互斥是P和V操作在同一进程中;

同步则是P和V操作分别发生在两个不同进程中。




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > 操作系统入门(七)进程互斥和同步的完美解决方案——信号量机制

热门推荐
推荐新闻