操作系统之进程与线程5——进程同步与信号量

来源:互联网 发布:c语言if语句格式 编辑:程序博客网 时间:2024/05/20 12:49

一、进程同步与信号量


多进程除了切换和调度之外,还有相互之间的合作问题(即进程同步:多个进程共同完成一个任务),而实现进程同步的合理有序,需要靠信号量。

从进程同步提出信号

多个进程之间按照一定顺序进行,一个进程的执行可能依赖于另外的进程执行时发出的特定信号(状态:有/无)。因此,控制多个进程按条件执行,依赖于特定信号的传递。

例如,实例1——司机进程启动车辆需要收到 售票员进程发出的关门信号;售票员进程开门需要 司机进程的停车信号。


从信号到信号量

生产者-消费者实例

  • —— 这些程序都是用户态程序。
  • —— 多进程同步在于让进程走走停停,并且关键是让进程停下来。来保证进程的合理有序进行。

生产者-消费者实例的走停,大致过程如下

  • 停:缓冲区满;
  • 走:另一个进程发出有资源/缓冲区有空闲;



只发信号还不能解决全部问题

上述过程的大致代码实现如下


可以看出:在一个生产者调度sleep时,并不能约束是否还能调度,因此另外的生产者调度再调度即再次sleep。

——> 可以看出信号counter只有0/1两个状态,只能表征缓冲区资源数量,并不能表征有多少个进程sleep,即不能完全表征多进程合作时的变化。从程序运行可以看出sleep的P2进程一直不能被WakeUp

——>因此,在生产者进程中引入信号量sem 记录“和睡眠进程相关的量”记录有几个进程在sleep状态/资源个数,判断信号量决定是否需要发信号,要发几个信号——> 此时即可以合理完成多个生产者-多个消费者的进程同步问题。

注:多个sleep进程进入阻塞队列,从队列头WakeUp

引入信号量

此处sem——>空闲资源个数:负数表示有多个Producer进程sleep,同时缓冲区满(需要Consumer进程不断WakeUp);正数表示缓冲区空闲个数(Producer进程可以执行);0表示缓冲区满且没有Producer进程阻塞。


——> 停:sem <= 0,进程睡眠,需要发信号唤醒; 走:sem > 0。

例题:资源信号量为2,即该信号量表征资源数量,因此若为-2,即资源不够,有两个进程等待该资源。


信号量的实现

阻塞进程需要sleep在该信号量的队列下(类似于车停在相应的红绿灯下),因此信号量含有两个成员—— 此处value记录Producer进程的参数,即资源个数,即缓冲区空闲资源个数(负值即阻塞个数,正数即空闲个数)。

生产者进程和消费者进程提供一个接口函数:P、V操作用于在各个进程中判断信号量多少,并控制走停。(0为其重要结点)


此处P、V操作的含义:

  • P操作(P停):消耗空闲资源——>应该提供给可能产生/判断是否产生 sleep的程序—— 判断是否会阻塞
  • V操作(P走):产生空闲资源——>从阻塞队列唤醒

注释:P、V操作必须操作进程,并且阻塞队列为PCB队列,都在内核态进行,因此P、V操作中都必须做成系统调用,上层应用程序调用系统调用使用信号量。

利用信号量解决生产者-消费者问题


设置三个信号量:

  • empty表示空闲资源个数,初始化为BUFFER_SIZE,即初始缓冲区为空;——>用于在Producer进程中做为P操作参数控制“停”,Consumer进程中V操作控制“走”;
  • full表示缓冲区资源个数,初始化为0,即初始缓冲区为空;——>用于在Consumer进程中做为P操作参数控制“停”,Producer进程中V操作控制“走”;
  • mutex表示“互斥信号量”,初始化为1;——>控制某一时刻只能有一个进程对文件执行读写操作:若可读/写,则P操作减一为0,即锁定文件,执行完再V加一为1;

二、信号量的临界区保护


本节的两个问题:

  • 为什么要保护信号量?为什么引出临界区?
  • 如何保护信号量?怎么实现临界区?
——>仍需要依靠临界区保护信号量,再以信号量实现进程同步。

共同修改信号量引发的问题?


两个Producer进程P1、P2调度时,需要同时修改信号量empty,正确结果为-3。但若由于时间片关系发生如图所示调度,则会产生错误empty。

——>因此,信号量表达的含义必须正确,不能因为进程调度算法产生的调度次序不同产生错误。

注释:和调度相关的共享数据都会引起类似的这种错误。——>体会“调度算法的动态可变性”

竞争条件:没有对共享对象进行保护,产生了竞争错误(多个进程竞争时,某个进程获得了错误的修改共享数据的权利)

——>加几个空循环的做法不可取

——>必须加入信号量保护机制

解决竞争条件的直观想法

对信号量的一次操作上锁,只能对操作量完成  要么不操作/要么全部操作(原子操作,不可分割),一个进程对共享变量操作时,不能在操作中途被其他进程竞争执行。

——>共享修改一个全局变量,必须用临界区保护,否则调度策略即会产生竞争错误。

——>修改信号量的代码必须语义正确,因此 修改信号量的代码 必须是进程中的临界区的代码,这段代码必须进行保护。


——>因此,必须在临界区之前和之后的进入区和退出区写一段代码。


临界区代码的保护原则


进入临界区做法

软件实现方法

1.轮换法(单标志法)

P0进程非值日日(turn !=0)不值日,一般处理为:运行完一个时间片时间。

turn是公用变量,


  • 优点:可证明满足互斥进入原则,可确保每次只有一个进程进入临界区;
  • 缺点:两个进程必须交替进入——> 若一个进程因为别的事件阻塞,则另一个进程执行完之后,不会再进入空闲的临界区,即不满足“空闲让进”,容易造成资源利用的不充分。

2.双标记法先检查

每一个进程在访问临界区资源之前,先查看一下临界资源是否正被访问,若正被访问,该进程需等待;否则,进程才进入自己的临界区。为此,设置了一个数据flag[i],如第i个元素值为FALSE,表示Pi进程未进入临界区,值为TRUE,表示Pi进程进入临界区。


  • 优点:不用交替进入,可连续使用;
  • 缺点:Pi和Pj可能同时进入临界区。按序列①②③④ 执行时,会同时进入临界区(违背“忙则等待”)。即在检查对方flag之后和切换自己flag 之前有一段时间,结果都检查通过。这里的问题出在检查和修改操作不能一次进行。

3.双标记法后检查

算法2是先检测对方进程状态标志后,再置自己标志,由于在检测和放置中可插入另一个进程到达时的检测操作,会造成两个进程在分别检测后,同时进入临界区。为此,算法3釆用先设置自己标志为TRUE后,再检测对方状态标志,若对方标志为TURE,则进程等待;否则进入临界区。

  • 缺点:按序列①②③④执行时,它们分别将自己的标志值flag设置为TRUE,之后检测对方的状态(执行while语句),发现对方也要进入临界区,于是双方互相谦让,结果谁也进不了临界区,从而导致“饥饿”现象。

4.非对称标记法之Peterson’s Algorithm

进入区包含两段代码:一段设置标志代码,一段循环代码;若Pi进程要求进入,即会令flag[i]=TRUE;turn=j;

Pi进程在Pj进程设置不进入或者值日日为i,则进入临界区。

为了防止两个进程为进入临界区而无限期等待,又设置变量turn,指示不允许进入临界区的进程编号,每个进程在先设置自己标志后再设置turn 标志,不允许另一个进程进入。这时,再同时检测另一个进程状态标志和不允许进入标志,这样可以保证当两个进程同时要求进入临界区,只允许一个进程进入临界区。


  • 1.互斥性证明:假设都进入临界区,参数矛盾
  • 2.假设先执行P1两句,再P2执行等等

本算法的基本思想是算法1和算法3的结合。利用flag解决临界资源的互斥访问,而利用turn解决“饥饿”现象。

——>此时,临界区为信号量等代码


硬件实现方法

1.中断屏蔽方法

当一个进程正在使用处理机执行它的临界区代码时,防止其他进程进入其临界区访问的最简单方法是禁止一切中断发生,或称之为屏蔽中断、关中断。

因为只有CPU调度产生进程切换才可能导致信号量错误,而CPU只在发生中断时引起进程切换,因此屏蔽中断就能保证当前运行进程将临界区代码顺利地执行完,从而保证了互斥的正确实现,然后再执行开中断。其典型模式为:

关中断;cli()
临界区;
开中断;sti()

这种方法限制了处理机交替执行程序的能力,因此执行的效率将会明显降低。对内核来说,当它执行更新变量或列表的几条指令期间关中断是很方便的,但将关中断的权力交给用户则很不明智,若一个进程关中断之后不再开中断,则系统可能会因此终止。

同时,该方法只适用于单CPU,因为关中断通过INTR实现。

2.硬件指令方法

可以分别通过TestAndSet指令或Swap指令完成临界区上锁

TestAndSet指令:这条指令是原子操作,即执行该代码时不允许被中断。其功能是 返回指定标志并且把该标志设置为真。指令的功能描述如下:

boolean TestAndSet(boolean *lock){    boolean old;    old = *lock;    *lock=true;    return old;}
可以为每个临界资源设置一个共享布尔变量lock,表示资源的两种状态:true表示正被占用,初值为false。在进程访问临界资源之前,利用TestAndSet检查和修改标志lock;若有进程在临界区,则重复检查,直到进程退出。利用该指令实现进程互斥的算法描述如下:
while TestAndSet (& lock);// 进程的临界区代码段;lock=false;// 进程的其他代码

Swap指令:该指令的功能是交换两个字节的内容。其功能描述如下。
Swap(boolean *a, boolean *b){      boolean temp;    Temp=*a;    *a = *b;    *b = temp;}
应为每个临界资源设置了一个共享布尔变量lock,初值为false;在每个进程中再设置一个局部布尔变量key,用于与lock交换信息。在进入临界区之前先利用Swap指令交换lock 与key的内容,然后检查key的状态;有进程在临界区时,重复交换和检查过程,直到进程退出。利用Swap指令实现进程互斥的算法描述如下:
key=true;while(key!=false)Swap(&lock, &key); // 进程的临界区代码段;lock=false;// 进程的其他代码;

注意:以上对TestAndSet和Swap指令的描述仅仅是功能实现,并非软件实现定义,事实上它们是由硬件逻辑直接实现的,不会被中断。

  • 优点:适用于任意数目的进程,不管是单处理机还是多处理机;简单、容易验证其正确性。可以支持进程内有多个临界区,只需为每个临界区设立一个布尔变量。
  • 缺点:进程等待进入临界区时要耗费处理机时间,不能实现让权等待。从等待进程中随机选择一个进入临界区,有的进程可能一直选不上,从而导致“饥饿”现象。

三、信号量的代码实现


信号量的应用不仅在生产者-消费者等应用中使用,在内核中也有很多类似的同步问题中使用,例如磁盘读写等。


????????????????












原创粉丝点击