进程管理

1. 进程和线程

1.1 进程的概念和特征

进程是资源分配的基本单位。
为了使参与并发执行的程序(含数据)能独立地运行,必须为之配置一个专门的数据结构,称为进程控制块(Process Control Block,PCB)。系统利用PCB来描述进程的基本情况和运行态,进而控制和管理进程。由程序段、相关数据段和PCB三部分构成了进程映像(进程实体)。所谓创建进程,实质上是创建进程映像中的PCB;而撤销进程,实质上是撤销进程的PCB。进程映像是静态的,进程则是动态的。

1)动态性。进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的。动态性是进程最基本的特征。
2)并发性。指多个进程实体同时存在于内存中,能在一段时间内同时运行。并发性是进程的重要特征,同时也是操作系统的重要特征。引入进程的目的就是为了使程序能与其他进程的程序并发执行,以提高资源利用率。
3)独立性。指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。凡未建立PCB的程序,都不能作为一个独立的单位参与运行。
4)异步性。由于进程的相互制约,使得进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此在操作系统中必须配置相应的进程同步机制
5)结构性。每个进程都配置一个PCB对其进行描述。从结构上看,进程实体是由程序段、数据段和进程控制段三部分组成的。

1.2 进程的状态和转换

1)运行态。进程正在处理机上运行。在单处理机环境下,每个时刻最多只有一个进程处于运行态。
2)就绪态。进程已处于准备运行的状态,即进程获得了除处理机外的一切所需资源,一旦得到处理机即可运行。分时系统的时间片轮转机制中,每个进程分到的时间片很短,意味着得到处理机的时间很短很频繁,运行过程中会频繁切换到就绪态。
3)阻塞态,又称等待态。进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。
4)创建态。进程正在被创建,尚未转到就绪态。创建进程通常需要多个步骤:首先申请一个空白的PCB,并向PCB中填写一些控制和管理进程的信息;然后由系统为该进程分配运行时所必需的资源;最后把该进程转入就绪态。
5)结来态。进程正从系统中消失,可能是进程正常结束或其他原因中断退出运行。进程需要结束运行时,系统首先必须置该进程为结束态,然后再进一步处理资源释放和回收等工作。
在这里插入图片描述
就绪态→运行态:处于就绪态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪态转换为运行态。
运行态→就绪态:处于运行态的进程在时间片用完后,不得不让出处理机,从而进程由运行态转换为就绪态。此外,在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程度将正执行的进程转换为就绪态,让更高优先级的进程执行。
运行态→阻塞态:进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如IO操作的完成)时,它就从运行态转换为阻塞态。进程以系统调用的形式请求操作系统提供服务,这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式。
阻塞态→就绪态:进程等待的事件到来时,如L/O操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞态转换为就绪态。

1.3 进程的控制

  1. 进程的创建
    允许一个进程创建另一个进程。此时创建者称为父进程,被创建的进程称为子进程。子进程可以继承父进程所拥有的资源。当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程。在撤销父进程时,必须同时撤销其所有的子进程。在操作系统中,终端用户登录系统、作业调度、系统提供服务、用户程序的应用请求等都会引起进程的创建。
    操作系统创建一个新进程的过程如下:
    1)为新进程分配一个唯一的进程标识号,并申请一个空白的PCB (PCB是有限的)。若PCB申请失败,则创建失败。
    2)为进程分配资源,为新进程的程序和数据及用户栈分配必要的内存空间(在PCB中体现)。注意,若资源不足(如内存空间),则并不是创建失败,而是处于“阻塞态”,等待的是内存这个资源。
    3)初始化PCB,主要包括初始化标志信息、初始化处理机状态信息和初始化处理机控制信息,以及设置进程的优先级等。
    4)若进程就绪队列能够接纳新进程,则将新进程插入就绪队列,等待被调度运行。

  2. 进程的终止
    引起进程终止的事件主要有:正常结束,表示进程的任务已经完成并准备退出运行。异常结
    束,表示进程在运行时,发生了某种异常事件,使程序无法继续运行。外界干预是指进程应外界的请求而终止运行
    操作系统终止进程的过程如下:
    1)根据被终止进程的标识符,检索PCB,从中读出该进程的状态。
    2)若被终止进程处于执行状态,立即终止该进程的执行,将处理机资源分配给其他进程。
    3)若该进程还有子进程,则应将其所有子进程终止。
    4)将该进程所拥有的全部资源归还给其父进程,或归还给操作系统。
    5)将该PCB从所在队列(链表)中删除。

  3. 进程的阻塞和唤醒
    正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败等,由系统自动执行阻塞(Block), 使自己由运行态变为阻塞态。进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞态。阻塞的执行过程如下:
    1)找到将要被阻塞进程的标识号对应的PCB.
    2)若该进程为运行态,则保护其现场,将其状态转为阻塞态,停止运行。
    3)把该PCB插入相应事件的等待队列。
    当被阻塞进程所期待的事件出现时,如它所启动的IO操作已完成或其所期待的数据已到达,由有关进程(如提供数据的进程)调用唤醒原语(Wakeup), 将等待该事件的进程唤醒。唤醒原语的执行过程如下:
    1)在该事件的等待队列中找到相应进程的PCB.
    2)将其从等待队列中移出,并置其状态为就绪态。
    3)把该PCB插入就绪队列,等待调度程序调度。

  4. 进程切换
    对于通常的进程而言,其创建、撤销及要求由系统设备完成的I/0操、进程切换,都是利用系统调用
    而进入内核,再由内核中的相应处理程序予以完成的。
    进程切换是指处理机从一个进程的运行转到另一个进程上运行,在这个过程中,进程的运行
    环境产生了实质性的变化。进程切换的过程如下:
    1)保存处理机上下文,包括程序计数器和其他寄存器。
    2)更新PCB信息。
    3)把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
    4)选择另一个进程执行,并更新其PCB.
    5)更新内存管理的数据结构。
    6)恢复处理机上下文。

进程切换与处理机模式切换是不同的,模式切换时,处理机逻辑上可能还在同一进程中运行。若进程因中断或异常进入核心态运行,执行完后又回到用户态刚被中断的程序运行,则操作系统只需恢复进程进入内核时所保存的CPU现场,而无须改变当前进程的环境信息。但若要切换进程,当前运行进程改变了,则当前进程的环境信息也需要改变。

1.4 进程的组织

1.进程控制块
进程创建时,操作系统新建一一个PCB结构,该结构之后常驻内存,任意时刻都可以存取,
并在进程结束时删除。PCB是进程实体的一部分,是进程存在的唯一标志。
创建一个进程时,系统为该进程建立-一个PCB;进程执行时,系统通过其PCB了解进程的
现行状态信息,以便对其进行控制和管理:进程结束时,系统收回其PCB,该进程随之消亡。操
作系统通过PCB表来管理和控制进程。
PCB主要包括进程描述信息、进程控制和管理信息、资源分配清单和处理机相关信息等。进程描述信息包括进程标识符(PID)和用户标识符(UID)。进程标识符:标志各个进程,每个进程都有一个唯一的标识号。用户标识符:进程归属的用户,用户标识符主要为共享和保护服务。

在一个系统中,通常存在着许多进程,有的处于就绪态,有的处于阻塞态,而且阻塞的原因各不相同。为了方便进程的调度和管理,需要将各进程的PCB用适当的方法组织起来。目前,常用的组织方式有链接方式和索引方式两种。链接方式将同一状态的PCB链接成一个队列,不同状态对应不同的队列,也可把处于阻塞态的进程的PCB,根据其阻塞原因的不同,排成多个阻塞队列。索引方式将同一状态的进程组织在一个索引表中,索引表的表项指向相应的PCB,不同状态对应不同的索引表,如就绪索引表和阻塞索引表等。

2.程序段
程序段就是能被进程调度程序调度到CPU执行的程序代码段,程序可被多个进程共享,多个进程运行同一个程序。
3.数据短
一个进程的数据段,可以是进程对应的程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果。

1.5 进程的通信

  1. 管道
    “管道”,是指用于连接一个读进程和一个写进程以实现它们之间的通信的一个共享文件,又名pipe 文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入(写)管道;而接收管道输出的接收进程(即读进程)则从管道中接收(读)数据。为了协调双方的通信,管道机制必须提供以下三方面的协调能力:互斥、同步和确定对方的存在。

管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。它具有以下限制:只支持半双工通信(单向交替传输);只能在父子进程或者兄弟进程中使用。从管道读数据是一次性操作,数据一旦被读取,就从管道中北抛弃,释放空间以便写更多的数据。
在这里插入图片描述
2. FIFO
也称为命名管道,去除了管道只能在父子进程中使用的限制。FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。
在这里插入图片描述
3. 消息队列
在这里插入图片描述
相比于 FIFO,消息队列具有以下优点:
消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。

  1. 信号量
    它是一个计数器,用于为多个进程提供对共享数据对象的访问,信号量的意图在于进程间同步。通信方式主要用于解决与同步相关的问题并避免竞争条件。

当有进程要求使用共享资源时,需要执行以下操作:
1.系统首先要检测该资源的信号量;
2.若该资源的信号量值大于0,则进程可以使用该资源,此时,进程将该资源的信号量值减1;
3.若该资源的信号量值为0,则进程进入休眠状态,直到信号量值大于0时进程被唤醒,访问该资源;
当进程不再使用由一个信号量控制的共享资源时,该信号量值增加1,如果此时有进程处于休眠状态等待此信号量,则该进程会被唤醒。

  1. 共享存储
    在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行写/读操作实现进程之间的信息交换,在对共享空间进行写/读操作时,需要使用同步互斥工具(如P操作、V操作),对共享空间的写/读进行控制。
    共享存储又分为两种:低级方式的共享是基于数据结构的共享:高级方式的共享则是基于存储区的共享。操作系统只负责为通信进程提供可共享使用的存储空间和同步互斥工具,而数据交换则由用户自己安排读/写指令完成。
    允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。需要使用信号量用来同步对共享存储的访问。

  2. 套接字
    主要用于在客户端和服务器之间通过网络进行通信。套接字是支持TCP/IP的网络通信的基本操作单元。与其它通信机制不同的是,它可用于不同机器间的进程通信。

1.6 线程的概念和多线程模型

引入进程的目的是为了更好地使多道程序并发执行,提高资源利用率和系统吞吐量,增加并发程度;引入线程的目的则是为了减小程序在并发执行时所付出的时空开销,提高操作系统的
并发性能。
线程是一个基本的CPU执行单元,也是程序执行流的最小单元,由线程ID、程序计数器、寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。线程也有就绪、阻塞和运行三种基本状态。

1.6.1 线程属性

1)线程是一一个轻型实体,它不拥有系统资源,但每个线程都应有一个唯一的标识符和一个线程控制块,线程控制块记录了线程执行的寄存器和栈等现场状态。
2)不同的线程可以执行相同的程序,即同一个服务程序被不同的用户调用时,操作系统把它们创建成不同的线程。
3)同一进程中的各个线程共享该进程所拥有的资源。
4)线程是处理机的独立调度单位,多个线程是可以并发执行的。在单CPU的计算机系统中,各线程可交替地占用CPU;在多CPU的计算机系统中,各线程可同时占用不同的CPU,若各个CPU同时为一个进程内的各线程服务,则可缩短进程的处理时间。
5)一个线程被创建后,便开始了它的生命周期,直至终止。线程在生命周期内会经历阻塞态、就绪态和运行态等各种状态变化。

1.6.2 进程线程比较

Ⅰ 拥有资源
进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
Ⅱ 调度
线程是独立调度的基本单位,进程是拥有资源的基本单位。在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
Ⅲ 系统开销
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
Ⅳ 通信方面
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。

1.7 线程池

Java线程池

2. 进程调度

一个作业从提交开始直到完成,往往要经历三级调度
1)作业调度。又称高级调度,其主要任务是按一定的原则从外存上处于后备状态的作业中挑选一个(或多个)作业,给它(们)分配内存、输入/输出设备等必要的资源,并建立相应的进程,以使它(们)获得竞争处理机的权利。作业调度就是内存与辅存之间的调度。对于每个作业只调入一次、调出一次。
多道批处理系统中大多配有作业调度,而其他系统中通常不需要配置作业调度。作业调度的执行频率较低,通常为几分钟一次。
2)中级调度。又称内存调度,其作用是提高内存利用率和系统吞吐量。为此,应将那些暂时不能运行的进程调至外存等待,把此时的进程状态称为挂起态。当它们已具备运行条件且内存又稍有空闲时,由中级调度来决定把外存上的那些已具备运行条件的就绪进程,再重新调入内存,并修改其状态为就绪态,挂在就绪队列上等待。
3)进程调度。又称低级调度,其主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它。进程调度是操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度。进程调度的频率很高,一般几十毫秒一次。

作业调度从外存的后备队列中选择一批作业进入内存,为它们建立进程,这些进程被送入就
绪队列,进程调度从就绪队列中选出一个进程, 并把其状态改为运行态,把CPU分配给它。中级
调度是为了提高内存的利用率,系统将那些暂时不能运行的进程挂起来。当内存空间宽松时,通
过中级调度选择具备运行条件的进程,将其唤醒.
1)作业调度为进程活动做准备,进程调度使进程正常活动起来,中级调度将暂时不能运行的进程挂起,中级调度处于作业调度和进程调度之间。
2)作业调度次数少,中级调度次数略多,进程调度频率最高。
3)进程调度是最基本的,不可或缺。

2.1 调度方式

1)非剥夺调度方式,又称非抢占方式。是指当一个进程正在处理机上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程完成或发生某种事件而进入阻塞态时,才把处理机分配给更为紧迫的进程。
在非剥夺调度方式下,一旦把CPU分配给一个进程,该进程就会保持CPU直到终止或转换到等待态。这种方式的优点是实现简单、系统开销小,适用于大多数的批处理系统,但它不能用于分时系统和大多数的实时系统。
2)剥夺调度方式,又称抢占方式。剥夺调度方式是指当一个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给这个更为重要或紧迫的进程。
采用剥夺式的调度,对提高系统吞吐率和响应效率都有明显的好处。但“剥夺”不是一种任意性行为,必须遵循一定的原则,主要有优先权、短进程优先和时间片原则等。

2.2 调度算法

不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。

  1. 批处理系统
    批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。

    1.1 先来先服务 first-come first-serverd(FCFS)
    非抢占式的调度算法,按照请求的顺序进行调度。
    算法简单,效率低。有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。有利于CPU繁忙型作业,而不利于IO繁忙型作业

    1.2 短作业优先 shortest job first(SJF)
    非抢占式的调度算法,按估计运行时间最短的顺序进行调度。
    长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。

    1.3 最短剩余时间优先 shortest remaining time next(SRTN)
    最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。

  2. 交互式系统
    交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。

    2.1 时间片轮转
    将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
    时间片轮转算法的效率和时间片的大小有很大关系:
    因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。而如果时间片过长,那么实时性就不能得到保证。

    2.2 优先级调度
    为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级

    根据新的更高优先级进程能否抢占正在执行的进程,可将该调度算法分为如下两种:
    1)非剥夺式优先级调度算法。
    2)剥夺式优先级调度算法。

    根据进程创建后其优先级是否可以改变,可以将进程优先级分为以下两种:
    1)静态优先级。优先级是在创建进程时确定的,且在进程的整个运行期间保持不变。确定静态优先级的主要依据有进程类型、进程对资源的要求、用户要求。
    2)动态优先级。在进程运行过程中,根据进程情况的变化动态调整优先级。动态调整优先级的主要依据有进程占有CPU时间的长短、就绪进程等待CPU时间的长短。

    进程优先级的设置可以参照以下原则:
    1)系统进程>用户进程。系统进程作为系统的管理者,理应拥有更高的优先级。
    2)交互型进程>非交互型进程(或前台进程>后台进程)。
    3)I/O 型进程>计算型进程。所谓I/O型进程,是指那些会频繁使用I/O设备的进程,而计算型进程是那些频繁使用CPU的进程(很少使用I/O设备)。我们知道,I/0 设备(如打印机)的处理速度要比CPU慢得多,因此若将I/O型进程的优先级设置得更高,就更有可能让I/0设备尽早开始工作,进而提升系统的整体效率。

    2.3 多级反馈队列
    一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,…。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。
    每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

  3. 实时系统
    实时系统要求一个请求在一个确定时间内得到响应。分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。

3. 进程同步

3.1 基本概念

1.临界资源
对临界资源进行访问的那段代码称为临界区。为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。为了保证临界资源的正确使用,可把临界资源的访问过程分成4个部分:

  1. 进入区。为了进入临界区使用临界资源,在进入区要检查可否进入临界区,若能进入临界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区
  2. 临界区。进程中访问临界资源的那段代码,又称临界段。
  3. 退出区。将正在访问临界区的标志清除
  4. 剩余区。代码中的其余部分。
do {
	entry section;//进入区
	critical section;//临界区
	exit section;//退出区
	remainder section;//剩余区
}while (true);

2.同步
同步称直接制约关系,是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系源于它们之间的相互合作。
例如,输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B就被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才唤醒进程A

3.互斥
互斥也称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。

为禁止两个进程同时进入临界区,同步机制应遵循以下准则:
1)空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
2)忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
3)有限等待。对请求访问的进程,应保证能在有限时间内进入临界区。
4)让权等待。当进程不能进入临界区时,应立即释放处理器,防止进程忙等待。

3.2 信号量

信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。
down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。
down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。

  1. 利用信号量实现同步
    信号量机制能用于解决进程间的各种同步问题。设S为实现进程P1、P2 同步的公共信号量,初值为0.进程P2中的语句y要使用进程P1中语句x的运行结果,所以只有当语句x执行完成之后语句y才可以执行。若P2先执行到P(S)时,S为0,执行P操作会把进程P2阻塞,并放入阻塞队列:当进程P中的x执行完后,执行V操作,把P2从阻塞队列中放回就绪队列,当P2得到处理机时,就得以继续执行。其实现进程同步的算法如下:
semaphore S=0;//初始化信号量
P1(){
	x;//语句x
	V(S) ;}//告诉进程P2,语句x已经完成
P2() {
	...
	P(S) ;//检查语句x是否运行完成
	y;}//检查无误,运行y语句
  1. 利用信号量实现互斥
    设S为p1,p2互斥的信号量,由于每次只允许一个进程进入临界区,所以S的初始值设置为1(可用资源数为1),只要把临界区至于P(S)和V(S)之间,就可以实现两个进程对临界资源的互斥访问
semaphore S=1;//初始化信号量
P1() {
	P(S);//准备开始访问临界资源,加锁
	进程P1的临界区;
	V(S);//访间结束,解锁
P2() {
	P(S);//准备开始访问临界资源,加锁
	进程P2的临界区: 
	V(S) ;//访问结束,解锁
}
  1. 利用信号量实现前驱关系
    一个进程V操作的资源如果是另一个进程P操作的资源,那么前者是后者的前驱

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

问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。

因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。

为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。

注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。

#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;

void producer() {
    while(TRUE) {
        int item = produce_item();
        down(&empty);
        down(&mutex);
        insert_item(item);
        up(&mutex);
        up(&full);
    }
}

void consumer() {
    while(TRUE) {
        down(&full);
        down(&mutex);
        int item = remove_item();
        consume_item(item);
        up(&mutex);
        up(&empty);
    }
}

3.3 管程

使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易

示例代码的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。

monitor ProducerConsumer
    integer i;
    condition c;

    procedure insert();
    begin
        // ...
    end;

    procedure remove();
    begin
        // ...
    end;
end monitor;

管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。
管程引入了 条件变量 以及相关的操作:wait() 和 signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。

// 管程
monitor ProducerConsumer
    condition full, empty;
    integer count := 0;
    condition c;

    procedure insert(item: integer);
    begin
        if count = N then wait(full);
        insert_item(item);
        count := count + 1;
        if count = 1 then signal(empty);
    end;

    function remove: integer;
    begin
        if count = 0 then wait(empty);
        remove = remove_item;
        count := count - 1;
        if count = N -1 then signal(full);
    end;
end monitor;

// 生产者客户端
procedure producer
begin
    while true do
    begin
        item = produce_item;
        ProducerConsumer.insert(item);
    end
end;

// 消费者客户端
procedure consumer
begin
    while true do
    begin
        item = ProducerConsumer.remove;
        consume_item(item);
    end
end;

经典同步问题:
生产者-消费者问题;哲学家进餐问题;读者-写者问题
允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值