进程管理(4):信号量与管程

2023-12-05 10:38
文章标签 管理 信号量 进程 管程

本文主要是介绍进程管理(4):信号量与管程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在进程管理(3):同步互斥中,我们提到,为了解决同步互斥问题,操作系统会提供一些高级抽象方法供应用进程调用,这样应用进程就不需要自己利用繁琐的软件方法来解决同步互斥了。存在三种高级抽象方法,分别是锁,信号量与条件变量,其中锁也在上面那篇中讨论过了,这里主要是讨论信号量与条件变量。同步互斥的层次结构如下图所示:

synchronization_methods

信号量

为什么要引入信号量?

前面我们已经用锁机制方便的解决了临界区的互斥访问问题了,但是锁机制似乎并不能适用于更一般的情况。例如操作系统某些资源是有多个的,可以让多个进程共同访问,只有当访问的进程多于资源数量的时候才对进程访问加以限制。很明显,对于这种情况,使用锁机制的结果是一次只能有一个进程访问资源,这无疑是对资源的一种浪费。

此外,锁机制只能解决进程间的互斥访问问题,并不能推广到进程间的同步问题。

因此,有必要引入一个新的机制,既可以合理地对多个资源进行分配,又可以同时兼顾同步与互斥问题。这就是引入信号量机制的原因。

什么是信号量?

信号量是操作系统提供的一种协调共享资源访问的方法。它采用一种简明的方法来协调多个资源的访问,即采用信号量来表示当前剩余的系统资源数量。如果有一个进程请求某个资源,则将该资源的信号量递减;反之,如果某个进程释放了资源,就将该资源的信号量递增。这里对信号量的操作都是被操作系统封装起来的,因此可以看到,信号量更多的是一种抽象数据类型,由一个整型变量和两个基本操作组成,通常用P操作和V操作来表示递减和递增,分别对应了荷兰语里面的尝试减少增加。由于PV操作的对象,即sem变量实质上也是一个共享变量,对它的访问是需要互斥进行的,因此这里的PV操作都需要是原子操作。

class Semaphore{
private:int sem;public:void up();void down();
}

信号量的实现

为了实现信号量,主要就是需要实现它的两个成员函数downup

当一个进程请求资源时,首先应该检查sem当前的值,如果sem > 0,表示系统还有剩余的资源,则执行--sem并且将资源分配给请求进程;如果sem <= 0,则表示当前系统已经没有这种资源了,仍然执行--sem,表示新增了一个进程在等待这个资源。由于当前进程得不到它请求的资源,此时应该释放CPU的控制权,调度其他进程进入运行状态。为了在资源空闲时可以唤醒当前进程,可以对每个信号量设置一个等待队列,并且将等待该信号量的进程加入等待队列中。

当一个进程释放了它占用的资源时,首先应该执行++sem表示将一个资源归还给了操作系统。此时如果sem <= 0,则表示还有其他进程在等待这个资源,所以应该从等待队列中挑选一个进程,将其唤醒;否则,如果sem > 0,表示当前已经有空闲的资源了,所以没有进程在等待队列中,直接退出就可以了。

根据这里的分析,给出上面Semaphore类的伪代码实现:

class Semaphore{
private:int sem;WaitQueue q;public:void up();void down();
}void Semaphore::down(){--sem;if(sem < 0){add current process to q;schedule();}
}void Semaphore::up(){++sem;if(sem <= 0){pick a process in q;wakeup_proc();}
}

需要指出的是,上面的代码只是示意而已,实际的实现中还有一些细节的部分在这里没有体现。在lab7 report里面会有具体的PV操作的代码。

信号量的应用

利用信号量实现临界区的互斥访问

参照前面提过的锁机制,为了利用信号量实现临界区的互斥访问,只需要利用信号量来实现这样一个锁。具体说来,为每个临界区构造一个信号量,其初值为1,表示一次只能有一个进程访问临界区资源。

进程在进入临界区之间,首先需要获得锁,即对应了信号量的P操作;进程退出临界区之后,需要释放锁,让其他进程也可以进入,这对应了信号量的V操作。因此,只需要成对地使用P操作和V操作,就实现了利用信号量对临界区的互斥访问。其代码如下:

mutex = new Semaphore(1);mutex->down();
critical_section();
mutex->up();

和锁机制相比较,由于信号量引入了等待队列,等待进入临界区的进程可以不用占用CPU,因此可以提高CPU的使用率。另一方面,引入了等待队列后,可以实现等待信号量的进程先进先出,在一定程度上保证了调度的公平性,而锁机制则不能做到这点,进程获得临界区资源只能是随机的。

利用信号量实现进程间同步

进程间同步,即两个或多个进程之间的某些操作,需要具有一定的先后次序。例如进程1的prev()函数必须先于进程2的next()函数执行,如果进程2运行到了next()函数处时,prev()还没有执行,则进程2只能等待。下面就叙述如何利用信号量实现这种关系。

从信号量的观点来看,进程2等待的prev函数执行,实际上也是等待某种资源,而这种资源只有通过进程1才可以释放,在此之前这种资源的数量都是零。于是可以形成下面的代码:

condition = new semaphore(0);//for process 1
process1(){prev();condition->up();
}//for process 2
process2(){condition->down();next();
}

可以看到,为了实现两进程之间的同步,信号量必须成对地出现在两个不同的进程中,并且其位置也要相互匹配。

利用信号量实现生产者-消费者问题

生产者-消费者问题是指,存在某个有限大小的缓冲区,以及若干个生产者和一个消费者。每个生产者一次可以将一个单位的数据存放在缓冲区中,而消费者一次可以从缓冲区中读出一个单位的数据。每个生产者之间以及生产者与消费者之间,每次都只能有一个进程访问缓冲区。当缓冲区满时,生产者将不能生产数据;相应的,缓冲区为空时,消费者将不能读出数据。下面讨论如何利用信号量机制来实现该问题。

通过上面的问题描述,可以抽象出该问题中存在的若干同步互斥关系。

  • 任意时刻只能有一个进程进入缓冲区进行访问,即互斥访问缓冲区。
  • 缓冲区满时,生产者必须等待消费者。
  • 缓冲区空时,消费者必须等待生产者。

可见,问题的关键,就在于利用信号量实现上面三组同步互斥关系。首先,为了实现对缓冲区的互斥访问,需要设置一个二进制信号量mutex;为了指示生产者等待消费者的关系,设置二进制信号量emptybuffer,表示当前空闲缓冲区的大小;相应的,也设置信号量fullbuffer表示当前已被占用的缓冲区的大小。这样,根据上面叙述的利用信号量实现同步互斥的模式,可以形成下面的代码:

mutex       = new semaphore(1); 
emptybuffer = new semaphore(n);      //buffers are all empty in the begining
fullbuffer  = new semaphore(0);
int count   = 0;//for producer
produce(){emptybuffer->down();mutex->down();//producemutex->up();fullbuffer->up();
}//for consumer
consume(){fullbuffer->down();mutex->down();//consumemutex->up();emptybuffer->up();
}

需要注意的是,无论是生产者还是消费者,一开始的两个P操作的顺序是不能颠倒的,否则可能出现这样一种情况,比如生产者成功进入了临界区,却发现没有空闲的缓冲区可供写入了,于是就等待emptybuffer信号。然而,此时消费者也无法进入被占用的缓冲区,在等待mutex信号。系统就进入了死锁状态。

条件变量与管程

管程的基本概念

信号量机制已经可以很优雅地实现多个进程之间的同步互斥问题了,但是信号量的使用还是存在一些其他问题,例如前面利用信号量实现进程之间的同步,需要不同进程的P V操作互相匹配,这使得进程的独立性下降而进程之间的耦合性增强。一旦程序员在编程过程中,忽略或者弄错了这种匹配关系,由于进程调度的不确定性,这种错误要检查起来是非常困难的。

管程就是为了解决这个问题的。所谓管程,实际上就是把多个进程要同步互斥访问的这些资源全部封装起来,由操作系统统一进行管理,它是一种用于多进程互斥访问共享资源的程序结构,因为采用了面向对象方法,所以简化了进程之间的同步控制。

在管程的内部,实际上是使用条件变量来代表某一类资源,一个管程内部往往有多个条件变量。这里的条件变量,其实就类似于信号量机制中的信号量。每个条件变量都有两个操作,即wait操作和signal操作——一个进程进入管程后,需要请求某一类的资源,如果不能成功获得资源,则需要调用wait(cond_var)表示该进程在等待这个条件变量;当进程使用完资源以后,调用signal(cond_var)来释放条件变量。可见,waitsignal操作分别对应了信号量的P操作和V操作,当然它们也有一定的区别,比如管程的wait操作一定会使进程进入等待状态,而P操作则未必。

和信号量一样,为了使得进程可以等待某一个条件变量,每个条件变量也都设置了一个等待队列。但是和信号量不一样的是,条件变量并不管理空闲资源的数量,而只是维护了等待当前条件变量的进程个数,这样,可以给出条件变量的类定义:

class condvar{
private:int num_waiting;WaitingQueue q;
public:void wait();void signal();
}

这里的条件变量,本质上也是属于进程之间的共享资源,对它们的访问也需要互斥地进行。因此,任一时刻至多只能有一个进程可以进入管程执行管程代码,为此,管程还需要一个互斥访问锁。这样,就可以给出管程的类定义了:

class monitor{
private:lock mutex;condvar *cv;
public://monitor routines
}

管程的组成如下图所示:

monitor

这样,当一个进程想要访问管程代码时,首先需要请求管程的互斥访问锁,获得了这个锁后方可进入管程,并且阻止其他进程的进入。一旦进程请求某个资源失败,它将进入等待状态并且放弃CPU的使用权,但是除此以外,它还需要释放管程锁以使其他进程也可以进入管程;当该进程终于获得这个资源时,它将从等待状态进入运行态,并且重新请求管程锁以继续执行。下面给出waitsignal的伪代码实现:

void condvar::wait(){num_waiting++;q.enqueue(curr_proc);curr_proc.state = SLEEPING;mutex.release();            // release lock so that other processes can enter the monitorschedule();mutex.acquire();            // re-acquire lock
}void condvar::signal(){if(num_waiting > 0){--num_waiting;pick up one process from the waiting queuewakeup(proc);}
}

需要注意的是,上面给出的waitsignal操作只是示意而已,在具体的应用中会根据策略的不同而具有不同的实现方式。但不管怎么样,它们的内涵都是一样的。常见的两种策略有Hansen管程和Hoare管程,它们之间的区别将在下面涉及。

利用管程实现生产者消费者问题

根据前面对生产者-消费者问题的分析,为了实现缓冲区的互斥访问,并不需要额外的变量,使用管程的互斥访问锁就可以了。对于生产者而言,如果缓冲区满了,则需要等待缓冲区有空条件,为此设置notFull条件变量;对于消费者而言,如果缓冲区为空,则需要等待缓冲区非空条件,为此设置notEmpty条件变量。此外,还需要一个额外的变量count来表示当前缓冲区被占用的个数。具体的代码如下:

void monitor_init(){int count = 0;             // buffers are empty in the beginninglock mutex;cond_var notFull;cond_var notEmpty;notFull.num_waiting = 0;notEmpty.num_waiting = 0;
}// for producer
void producer(){mutex.acquire();while(count == n)notFull.wait();produce();++count;notEmpty.signal();mutex.release();
}// for consumer
void consumer(){mutex.acquire();while(count == 0)notEmpty.wait();consume();count--;notFull.signal();mutex.release();
}

当时看到这里,我不禁产生一些疑问——这里的管程访问代码还不是用户自己写的吗,为什么说管程代码是由操作系统管理的呢?实际上,用户编写用户程序的时候,只是写了实质性的操作代码,类似于上面的produce()consume()处的代码,前后的请求锁、对资源的请求与释放,释放锁都是由编译器添加的,是毋须由程序员关注的。

Hansen管程和Hoare管程

管程的实现策略有两种,分别是Hansen管程和Hoare管程,它们的主要区别在于进程调度策略的不同。

假定首先有一个进程进入管程执行管程的例程,它在执行过程中需要等待某个资源,因此该进程进入阻塞状态,并且放弃了CPU的使用权。此后,第二个进程得以进入管程,该进程首先是释放了第一个进程请求的资源,第一个进程因而得以被唤醒。在Hansen管程的语意下,第二个进程将继续执行,直到它因为等待某个资源或者执行完毕,而让出管程的访问权限,此后第一个进程才有可能被调度。Hansen管程执行的流程如下图所示:

hansen

以上面的生产者-消费者问题为例,此时生产者的代码就和上面给出的一样,需要注意的是这行代码:

......
while(count == n)notFull.wait();
......

这是因为,在Hansen管程的语意下,被阻塞的进程需要和其他尚未进入管程的进程,同时竞争管程访问的权限mutex。因此,当被阻塞进程被唤醒后,也许有一个另外的生产者进程已经得到执行,使得当前缓冲区再次满了,被唤醒的进程需要再次判断它请求的资源是否为空闲。

而在Hoare管程的语意下,第二个进程释放第一个进程的资源后,将第一个进程唤醒,然后自己立刻进入阻塞状态,此时第一个进程将得到调度执行。该语意的实质是保证已经进入管程的阻塞进程优先于尚未进入管程的进程执行。因此,当第一个进程执行完毕后,它不会释放锁,而是直接将CPU的控制权转交给第二个进程。Hoare管程的执行流程如下图:

hoare

Hoare管程的语意下,wait操作和signal操作都需要做一定的修改。具体说来,在signal操作中,唤醒一个等待进程后,不释放管程锁,将自身加入signal queue中进入等待状态,此时只有被唤醒的进程可以得到管程的使用权。一个进程执行管程例程结束后,首先检查是否有处于signal queue状态的进程,如果有,也不释放管程锁,直接选择其中一个唤醒即可。hoare管程的具体代码在lab7 report中有所体现。

此时,生产者-消费者问题中的生产者代码将作出如下改动:

void producer(){
......
if(count == n)notFull.wait();
......
}

这是因为进程被唤醒后,一定可以优先得到管程的使用权,因此不需要像上面那样对条件做循环判断。

这篇关于进程管理(4):信号量与管程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/457230

相关文章

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

软考系统规划与管理师考试证书含金量高吗?

2024年软考系统规划与管理师考试报名时间节点: 报名时间:2024年上半年软考将于3月中旬陆续开始报名 考试时间:上半年5月25日到28日,下半年11月9日到12日 分数线:所有科目成绩均须达到45分以上(包括45分)方可通过考试 成绩查询:可在“中国计算机技术职业资格网”上查询软考成绩 出成绩时间:预计在11月左右 证书领取时间:一般在考试成绩公布后3~4个月,各地领取时间有所不同

安全管理体系化的智慧油站开源了。

AI视频监控平台简介 AI视频监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。用户只需在界面上进行简单的操作,就可以实现全视频的接入及布控。摄像头管理模块用于多种终端设备、智能设备的接入及管理。平台支持包括摄像头等终端感知设备接入,为整个平台提

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Sentinel 高可用流量管理框架

Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。 Sentinel 具有以下特性: 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应

java 进程 返回值

实现 Callable 接口 与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。 public class MyCallable implements Callable<Integer> {public Integer call() {return 123;}} public static void main(String[] args

C#关闭指定时间段的Excel进程的方法

private DateTime beforeTime;            //Excel启动之前时间          private DateTime afterTime;               //Excel启动之后时间          //举例          beforeTime = DateTime.Now;          Excel.Applicat

linux中使用rust语言在不同进程之间通信

第一种:使用mmap映射相同文件 fn main() {let pid = std::process::id();println!(

NGINX轻松管理10万长连接 --- 基于2GB内存的CentOS 6.5 x86-64

转自:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=190176&id=4234854 一 前言 当管理大量连接时,特别是只有少量活跃连接,NGINX有比较好的CPU和RAM利用率,如今是多终端保持在线的时代,更能让NGINX发挥这个优点。本文做一个简单测试,NGINX在一个普通PC虚拟机上维护100k的HTTP