操作系统---进程管理

进程的基本概念

  • 在未配置 OS 的系统中,程序的执行方式是顺序执行,即必须在一个程序执行完后,才允许另一个程序执行,在多道程序环境下,则允许多个程序并发执行。

程序的顺序执行及其特征

  • 程序的顺序执行
    把一个应用程序分成若干个程序段,在各程序段之间,必须按照某种先后次序顺序执行。仅当前一个操作(程序段)执行完后,才能执行后继操作。对一个程序段中的多条语句来说,也有一个执行顺序问题。

  • 程序顺序执行时的特征
    顺序性:处理机的操作严格按照程序所规定的顺序执行,即每一操作必须在上一个操作结束之后开始。

    封闭性:程序是在封闭的环境下执行的,即程序运行时独占全机资源,资源的状态 (除初始状态外)只有本程序才能改变,程序一旦开始执行,其执行结果不受外界因素影响。

    可再现性:只要程序执行时的环境和初始条件相同,当程序重复执行时,不论它是从头到尾不停顿地执行,还是“停停走走”地执行,都将获得相同的结果。

前趋图

  • 是指一个有向无循环图,可记为DAG(Directed Acyclic Graph),它用于描述进程之间执行的先后顺序。结点可用来表示一个进程或程序段,乃至一条语句,结点间的有向边则表示两个结点之间存在的偏序(Partial Order)或前趋关系(Precedence Relation)。

程序的并发执行的特征

  • 间断性
    程序在并发执行时,由于它们共享系统资源,以及为完成同一项任务而相互合作,致使在这些并发执行的程序之间,形成了相互制约的关系,导致并发程序具有“执行—暂停—执行”这种间断性的活动规律。

  • 失去封闭性
    程序在并发执行时,是多个程序共享系统中的各种资源,因而这些资源的状态将由多个程序来改变。

  • 不可再现性
    程序在并发执行时,由于失去了封闭性,也将导致其再失去可再现性。

进程的特征与状态

  • 进程的结构特征:
    通常的程序是不能并发执行的,为使程序(含数据)能独立运行,应为之配置一个进程控制块,即 PCB(Process Control Block)。

    程序段、相关的数据段和 PCB 三部分构成了进程实体,在早期的UNIX 版本中,把这三部分总称为“进程映像”。

  • 进程的特征

    动态性:进程的实质是进程实体的一次执行过程,是进程的最基本的特征。进程由创建而产生,由调度而执行,由撤消而消亡。进程实体有一定的生命期。程序则只是一组有序指令的集合,并存放于某种介质上,其本身并不具有运动的含义,是静态的。

    并发性:这是指多个进程实体同存于内存中,且能在一段时间内同时运行,引入进程的目的是为了使其进程实体能和其它进程实体并发执行,程序(没有建立PCB)是不能并发执行的。

    独立性:进程实体是一个能独立运行、独立分配资源和独立接受调度的基本单位,凡未建立 PCB 的程序都不能作为一个独立的单位参与运行。

    异步性:进程按各自独立的、不可预知的速度向前推进,或说进程实体按异步方式运行。

  • 进程是具有独立功能的程序在一个数据集合上运行的过程,是一个程序及其数据在处理机上顺序执行时所发生的活动,它是系统进行资源分配和调度的一个独立单位。

  • 进程的三种基本状态:进程执行时的间断性决定了进程可能具有多种状态。

    就绪(Ready)状态:当进程已分配到除CPU 以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个就绪队列。

    执行(Running)状态进程已获得 CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态,在多处理机系统中,则有多个进程处于执行状态。

    阻塞(Block)状态:正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机处于暂停状态,即进程的执行受到阻塞,通常将这种处于阻塞状态的进程也排成一个队列,有的系统则根据阻塞原因的不同而把处于阻塞状态的进程排成多个队列。

在这里插入图片描述

  • 挂起状态的原因
    终端用户的请求:当终端用户在自己的程序运行期间发现有可疑问题时,希望暂时使自己的程序静止下来,使正在执行的进程暂停执行。若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。

    父进程请求:父进程希望挂起自己的某个子进程,以便考查和修改该子进程,或者协调各子进程间的活动。

    负荷调节的需要:当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。

    操作系统的需要:操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或进行记账。

  • 创建状态:当一个新进程被创建时,系统为其分配了PCB,填写进程标识等信息,但由于该进程所必需的资源或其它信息尚未分配等,进程自身还未进入主存,即创建工作尚未完成,进程还不能被调度运行。对于处于创建状态的进程,获得了其所必需的资源,以及对其PCB初始化工作完成后,状态便可由创建状态转入就绪状态。

    引入创建状态,是为了保证进程的调度必须在创建工作完成后进行,以确保对进程控制块操作的完整性。创建状态的引入增加了管理的灵活性,操作系统可以根据系统性能或主存容量的限制,推迟创建状态进程的提交。

  • 终止状态:等待操作系统进行善后处理,然后将其PCB清零,并将PCB 空间返还系统。进入终止态的进程以后不能再执行,但在操作系统中依然保留一个记录,保存状态码和一些计时统计数据,供其它进程收集,一旦其它进程完成了对终止状态进程的信息提取之后,操作系统将删除该进程

  • 进程状态的转换

    就绪态→执行态:调度程序为之分配了处理机之后

    执行态→就绪态:分配给它的时间片已完而被剥夺处理机暂停执行

    执行态→阻塞态:因发生某事件,致使当前进程的执行受阻

    活动就绪→静止就绪:当进程处于未被挂起的就绪状态时,称此为活动就绪状态,当用挂起原语 Suspend 将该进程挂起后,该进程便转变为静止就绪,进程不再被调度执行。

    活动阻塞→静止阻塞:当进程处于未被挂起的阻塞状态时,称它是活动阻塞,当用 Suspend 原语将它挂起后,进程便转变为静止阻塞,处于该状态的进程在其所期待的事件出现后,将从静止阻塞变为静止就绪。

    静止就绪→活动就绪:处于静止就绪状态的进程,若用激活原语 Active 激活后,该进程将转变为活动就绪状态。

    静止阻塞→活动阻塞:处于静止阻塞状态的进程,若用激活原语 Active 激活后,该进程将转变为活动阻塞状态。

    NULL→创建:一个新进程产生时,该进程处于创建状态

    创建→活动就绪:在当前系统的性能和内存的容量均允许的情况下,完成对进程创建的必要操作后;相应的系统进程将进程的状态转换为活动就绪状态。

    创建→静止就绪:考虑到系统当前资源状况和性能要求,并不分配给新建进程所需资源,主要是主存资源,相应的系统进程将进程状态转为静止就绪状态,对换到外存,不再参与调度,此时创建工作尚未完成

    执行→终止:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结,进程即进终止状态。
    在这里插入图片描述

进程控制块PCB(Process Control Block)

  • 作为独立运行基本单位的标志、是进程存在的惟一标志。能实现间断性运行方式,提供进程管理、进程调度所需要的信息,实现与其它进程的同步与通信。

  • 进程控制块的作用:
    PCB 中记录了操作系统所需的、用于描述进程的当前情况以及控制进程运行的全部信息。使一个在多道程序环境下不能独立运行的程序,成为一个能独立运行的基本单位,一个能与其它进程并发执行的进程。系统是根据PCB感知到该进程的存在的,当系统创建一个新进程时,就为它建立了一个PCB,进程结束时又回收其PCB,进程于是也随之消亡。PCB 可以被操作系统中的多个模块读取或修改。

    因为 PCB 经常被系统访问,尤其是被运行频率很高的进程及分派程序访问,故PCB 应常驻内存,系统将所有的 PCB 组织成若干个链表(或队列),存放在操作系统中专门开辟的PCB 区内。

  • 进程管理中的数据结构
    在计算机系统中,对于每个资源和每个进程都设置了一个数据结构,用于表征其实体,我们称之为资源信息表或进程信息表。其中包含了资源或进程的标识、描述、状态等信息以及一批指针。通过指针,可以将同类资源或进程的信息表,或同一进程所占用的资源信息表分类链接成不同的队列,便于操作系统进行查找。一般分为以下四类:内存表、设备表、文件表和用于进程管理的进程表(进程控制块PCB)。

  • PCB中的信息
    进程标识符:用于唯一地标识一个进程。
    内部标识符:在所有的操作系统中,都为每一个进程赋予了一个惟一的数字标识符,它通常是一个进程的序号,方便系统使用。
    外部标识符:它由创建者提供,通常是由字母、数字组成,往往是由用户(进程)在访问该进程时使用。
    为了描述进程的家族关系,还应设置父进程标识及子进程标识,还有用户标识,以指示拥有该进程的用户。

    处理机状态(处理机的上下文):
    主要是由处理机的各种寄存器中的内容组成的,当处理机被中断时,所有这些信息都必须保存在PCB 中,以便在该进程重新执行时,能从断点继续执行。

    通用寄存器
    又称为用户可视寄存器,它们是用户程序可以访问的,用于暂存信息。

    程序状态字PSW
    其中含有状态信息,如条件码、执行方式、中断屏蔽标志等。

    用户栈指针
    指每个用户进程都有一个或若干个与之相关的系统栈,用于存放过程和系统调用参数及调用地址。

    进程调度信息

    进程状态:指明进程的当前状态,它是作为进程调度和对换时的依据。

    进程优先级:优先级高的进程应优先获得处理机。

    进程调度所需的其它信息:与所采用的进程调度算法有关。

    事件:进程由执行状态转变为阻塞状态所等待发生的事件,即阻塞原因。

    进程控制信息:用于进程控制所必须的信息。

    程序和数据的地址:程序和数据的内存或外存地(首)址,以便再调度该进程时,能从PCB中找到程序和数据。

    进程同步和通信机制:消息队列指针、信号量等,它们可能全部或部分地放在PCB中。

    资源清单:在该清单中列出了进程在运行期间所需的全部资源,已分配到该进程的资源的清单。

    链接指针:它给出了本进程(PCB)所在队列中的下一个进程的PCB的首地址。

  • PCB的组织方式
    线性方式:将系统中所有的PCB都组织在一张线性表中,将该表的首址存放在内存的一个专用区域中。实现简单、开销小。每次查找时都需要扫描整张表,因此适合进程数目不多的系统。

    链接方式:把具有相同状态进程的PCB分别通过PCB中的链接字链接成一个队列,可以形成就绪队列、若干个阻塞队列和空白队列等。对就绪队列而言,按进程的优先级将PCB从高到低进行排列,将优先级高的进程PCB排在队列的前面。把处于阻塞状态进程的PCB根据其阻塞原因的不同,排成多个阻塞队列。

    索引方式:即系统根据所有进程状态的不同,建立几张索引表,并把各索引表在内存的首地址记录在内存的一些专用单元中,在每个索引表的表目中,记录具有相应状态的某个PCB在PCB表中的地址。

  • 进程的层次结构:
    允许一个进程创建另一个进程。把创建进程的进程称为父进程,把被创建的进程称为子进程,子进程可继续创建更多的孙进程,由此便形成了一个进程的层次结构。如在UNIX中,进程与其子孙进程共同组成一个进程家族(组)。

    子进程可以继承父进程所拥有的资源。当子进程被撤消时,应将其从父进程那里获得的资源归还给父进程,在撤消父进程时,必须同时撤消其所有的子进程,为了标识进程之间的家族关系,在 PCB 中都设置了家族关系表项,以标明自己的父进程及所有的子进程。

进程控制

  • 进程管理中最基本的功能,一般是由OS的内核中的原语来实现的。

  • 创建新进程,终止已完成的进程,将因发生异常情况而无法继续运行的进程置于阻塞状态,进程运行中的状态转换功能。

  • 原语(Primitive)由若干条指令组成的,用于完成一定功能的一个过程。实现进程的通信和控制。系统对进程的控制如不使用原语,就会造成其状态的不确定性,达不到进程控制的目的。

  • 原子操作(Action Operation):一个不可分割的基本单位,在执行过程中不允许被中断。原子操作在管态下执行,常驻内存。

  • 进程图(Process Graph):为了形象地描述一个进程的家族关系而引入的一棵有向树。

进程的创建

  • 引起创建进程的事件:
    用户登录:在分时系统中,在终端键入登录命令后,如果用户合法,系统将为该终端建立一个进程,并把它插入就绪队列中。

    作业调度:在批处理系统中,当作业调度程序按一定的算法调度到某作业时,便将该作业装入内存,为它分配必要的资源,并立即为它创建进程,再插入就绪队列中。

    提供服务: 当运行中的用户程序提出某种请求后,系统将专门创建一个进程来提供用户所需要的服务。可使进程与该用户进程并发执行,而且还便于计算出为完成任务所花费的时间。

    应用请求:基于应用进程的需求,由它自己创建一个新进程,以便使新进程以并发运行方式完成特定任务。

  • OS调用进程创建原语Create创建一个新进程
    申请空白PCB:为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB。

    分配其运行所需的资源:为新进程的程序和数据以及用户栈分配必要的内存空间各种物理和逻辑资源,如内存、文件、I/O设备和CPU时间等,此时操作系统必须知道新进程所需内存的大小,对于批处理作业,其大小可在用户提出创建进程要求时提供,若为应用进程创建子进程,也应在该进程提出创建进程的请求中给出所需内存的大小。对于交互型作业,用户可以不给出内存要求而由系统分配一定的空间,如果新进程要共享某个已在内存的地址空间(即已装入内存的共享段),则必须建立相应的链接。

    初始化初始化标识信息:将系统分配的标识符和父进程标识符填入新 PCB 中。初始化处理机状态信息:使程序计数器指向程序的入口地址,使栈指针指向栈顶。初始化处理机控制信息:将进程的状态设置为就绪状态或静止就绪状态。优先级:通常是将它设置为最低优先级,除非用户以显式方式提出高优先级要求。

    如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列

进程的终止

  • 引起进程终止的事件:
    正常结束:在任何计算机系统中,都应有一个用于表示进程已经运行完成的指示。

    异常结束:在进程运行期间,由于出现某些错误和故障而迫使进程终止。

    越界错误:程序所访问的存储区已越出该进程的区域。

    保护错:进程试图去访问一个不允许访问的资源或文件,或者以不适当的方式进行访问。

    非法指令:程序试图去执行一条不存在的指令。可能是程序错误地转移到数据区,把数据当成了指令。

    特权指令错:用户进程试图去执行一条只允许OS执行的指令。

    运行超时:进程的执行时间超过了指定的最大值。

    等待超时:进程等待某事件的时间超过了规定的最大值。

    算术运算错:进程试图去执行一个被禁止的运算,例如被 0 除。

    I/O 故障:在 I/O 过程中发生了错误。

    外界干预:应外界的请求而终止运行。操作员或操作系统干预:由于某种原因,例如,发生了死锁,由操作员或操作系统终止该进程。父进程请求:由于父进程具有终止自己的任何子孙进程的权力,因而当父进程提出请求时,系统将终止该进程。父进程终止:当父进程终止时,OS 也将它的所有子孙进程终止。

  • 进程的终止过程
    OS 调用进程终止原语,根据被终止进程的标识符,从PCB 集合中检索出该进程的PCB,从中读出该进程的状态。若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真,用于指示该进程被终止后应重新进行调度。若该进程还有子孙进程,还应将其所有子孙进程予以终止,以防它们成为不可控的进程,将被终止进程所拥有的全部资源或者归还给其父进程,或者归还给系统,将被终止进程(PCB)从所在队列(或链表)中移出,等待其它程序来搜集信息

进程的阻塞与唤醒

  • 引起进程阻塞和唤醒的事件
    请求系统服务:当正在执行的进程请求操作系统提供服务时,由于某种原因,操作系统并不立即满足该进程的要求。该进程只能转变为阻塞状态来等待。

    启动某种操作:当进程启动某种操作后,如果该进程必须在该操作完成之后才能继续执行,则必须先使该进程阻塞,以等待该操作完成。

    新数据尚未到达:对于相互合作的进程,如果其中一个进程需要先获得另一进程提供的数据后才能对数据进行处理,只要其所需数据尚未到达,该进程只有(等待)阻塞。

    无新工作可做:系统往往设置一些具有某特定功能的系统进程,每当这种进程完成任务后,便把自己阻塞起来以等待新任务到来。

  • 进程阻塞过程
    调用阻塞原语block将自己阻塞(阻塞是进程自身的一种主动行为),先立即停止执行,把进程控制块中的现行状态由执行改为阻塞将PCB插入阻塞队列,如果系统中设置了因不同事件而阻塞的多个阻塞队列,则应将本进程插入到具有相同事件的阻塞队列。转调度程序进行重新调度,将处理机分配给另一就绪进程,并进行切换,保留被阻塞进程的处理机状态,按新进程的PCB中的处理机状态设置CPU的环境。

  • 进程唤醒过程
    当被阻塞进程所期待的事件出现时,由有关进程调用唤醒原语 wakeup,将等待该事件的进程唤醒。把被阻塞的进程从等待该事件的阻塞队列中移出,将其 PCB 中的现行状态由阻塞改为就绪,然后再将该 PCB 插入到就绪队列中

  • block 原语和 wakeup 原语是一对作用刚好相反的原语。如果在某进程中调用了阻塞原语,则必须在与之相合作的另一进程中或其他相关的进程中安排唤醒原语,以能唤醒阻塞进程。否则,被阻塞进程将会因不能被唤醒而长久地处于阻塞状态,从而再无机会继续运行。

进程的挂起与激活

  • 进程的挂起
    原因:终端用户的需要,父进程请求,负荷调节的需要,操作系统的需要。
    处理机调度的时候直接忽略挂起状态的进程。

  • 进程挂起过程
    当出现了引起进程挂起的事件时,系统将利用挂起原语 suspend将指定进程挂起,为了方便用户或父进程考查该进程的运行情况而把该进程的 PCB 复制到某指定的内存区域。若被挂起的进程正在执行,则转向调度程序重新调度

  • 进程的激活过程
    当发生激活进程的事件时,则可将在外存上处于静止就绪状态的该进程换入内存。这时,系统将利用激活原语 active将指定进程激活,先将进程从外存调入内存,假如采用的是抢占调度策略,则每当有新进程进入就绪队列时,应检查是否要进行重新调度,由调度程序将被激活进程与当前进程进行优先级的比较,如果被激活进程的优先级更低,就不必重新调度,否则,立即剥夺当前进程的运行,把处理机分配给刚被激活的进程。

进程同步

进程同步的基本概念

  • 进程同步的主要任务是对多个相关进程在执行次序上进行协调,以使并发执行的进程之间能有效地共享资源和相互合作,从而使程序的执行具有可再现性

  • 两种形式的制约关系
    间接相互制约关系:(互斥)则是由于进程间共享临界资源而引起的。
    直接相互制约关系:(同步)是由于进程间的相互合作而引起的。

  • 临界资源(Critical Resources):进程间应采取互斥方式实现对这种资源的共享。

  • 每个进程中访问临界资源的那段代码称为临界区(critical section)。若能保证进程互斥地进入自己的临界区,便可实现进程对临界资源的互斥访问。每个进程在进入临界区之前,应先对欲访问的临界资源进行检查,看它是否正被访问,如果此刻该临界资源未被访问,进程便可进入临界区对该资源进行访问,并设置它正被访问的标志,如果此刻该临界资源正被某进程访问,则本进程不能进入临界区。因此,必须在临界区前面增加一段用于进行上述检查的代码,把这段代码称为进入区(entry section)。相应地,在临界区后面也要加上一段称为退出区(exit section)的代码,用于将临界区正被访问的标志恢复为未被访问的标志,进程中除上述进入区、临界区及退出区之外的其它部分的代码,称为剩余区

  • 同步机制应遵循的规则
    空闲让进:当无进程处于临界区时,应允许一个请求进入临界区的进程立即进入自己的临界区,以有效地利用临界资源。

    忙则等待:当已有进程进入临界区时,其它试图进入临界区的进程必须等待,以保证对临界资源的互斥访问。

    有限等待:对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,以免陷入“死等”状态。

    让权等待:当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”状态。

硬件同步机制

  • 关中断
    在进入锁测试之前关闭中断,直到完成锁测试并上锁之后才能打开中断,这样,进程在临界区执行期间,计算机系统不响应中断,从而不会引发调度,也就不会发生进程或线程切换,保证了对锁的测试和关锁操作的连续性和完整性,有效地保证了互斥。

    滥用关中断权力可能导致严重后果:关中断时间过长,会影响系统效率,限制了处理器交叉执行程序的能力,关中断方法不适用于多CPU 系统,因为在一个处理器上关中断并不能防止进程在其它处理器上执行相同的临界段代码。

  • 利用Test-and-Set指令实现互斥

  • 利用Swap指令实现进程互斥(在Intel 80x86中又称为XCHG指令)

信号量机制

  • 整型信号量
    一个用于表示资源数目的整型量S ,除初始化外,仅能通过两个标准的原子操作(Atomic Operation) wait(S)和signal(S) 来访问,即PV操作,它们在执行时是不可中断的,即当一个进程在修改某信号量时,没有其他进程可同时对该信号量进行修改。只要是信号量S≤0,就会不断地测试,未遵循“让权等待”的准则,而是使进程处于“忙等”的状态。
//
wait(S)while  S<=0 do no-op;
		S:=S-1//
signal(S):  
	S:=S+1
  • 记录型信号量
    不存在“忙等”现象的进程同步机制,会出现多个进程等待访问同一临界资源的情况,在信号量机制中,除了需要一个用于代表资源数目的整型变量 value 外,还应增加一个进程链表指L,用于链接所有等待进程。在记录型信号量机制中,S.value 的初值表示系统中某类资源的数目,因而又称为资源信号量

    对它的每次wait操作,意味着进程请求一个单位的该类资源,使系统中可供分配的该类资源数减少一个。因此描述为 S.value=S.value-1。当 S.value<0 时,表示该类资源已分配完毕,因此进程应调用 block 原语,进行自我阻塞,放弃处理机,并插入到信号量链表S.L 中,此时 S.value 的绝对值表示在该信号量链表中已阻塞进程的数目。

    对信号量的每次 signal 操作,表示执行进程释放一个单位资源,使系统中可供分配的该类资源数增加一个,故 S.value:=S.value+1 操作表示资源数目加 1。若加 1 后仍是 S.value≤0,则表示在该信号量链表中,仍有等待该资源的进程被阻塞,故还应调用 wakeup 原语,将 S.L 链表中的第一个等待进程唤醒,如果 S.value 的初值为 1,表示只允许一个进程访问临界资源,此时的信号量转化为互斥信号量,用于进程互斥。

//
type semaphore=record          
	value: integer;         
	L: list of process;          
end
//
procedure wait(S)      
	var S:semaphore;      
	begin       
		S.value:=S.value-1if S.value<0 then block(S.L);      
	end  
//
procedure signal(S)      
	var S: semaphore;      
	begin       
		S.value:=S.value+1if S.value<=0 then wakeup(S.L);      
	end

  • AND 型信号量
    在有些应用场合,是一个进程需要先获得两个或更多的共享资源后方能执行其任务,将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程待进程使用完后再一起释放,只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源也不分配给它,亦即,对若干个临界资源的分配,采取原子操作方式:要么把它所请求的资源全部分配到进程,要么一个也不分配。可避免死锁情况的发生。
//
Swait(S1,S2,…,Sn)     
	if Si>=1 and … and Sn>=1 
	then      
	for i:=1 to n do      
		S i :=Si-1;      
	endfor     
	else    
	place the process in the waiting queue associated with the first Si found with Si<1,and set the program count of this process to the beginning of Swait operation     
endif
//
Ssignal(S1,S2,…,Sn)   
	for i:=1 to n do     
		S i :=Si+1;     
		Remove all the process waiting in the queue associated with Si into the ready queue.   
	endfor;
  • 信号量集
    记录型信号量机制,仅能对信号量施以加1或减1操作而当一次需要N个某类临界资源时,便要进行N次操作。此外,在有些情况下,当资源数量低于某一下限值时,便不予以分配,因而,在每次分配之前,都必须测试该资源的数量,看其是否大于其下限值。

    Swait(S,d,d):此时在信号量集中只有一个信号量S,但允许它每次申请d个资源,当现有资源数少于d时,不予分配。

    Swait(S,1,1):此时的信号量集已蜕化为一般的记录型信号量(S>1 时)或互斥信号量。

    Swait(S,1,0):当 S≥1 时,允许多个进程进入某特定区;当 S 变为 0 后,将阻止任何进程进入特定区,相当于一个可控开关。

//
Swait(S1,t1,d1,…,Sn,tn,dn)     
	if Si>=t1 and … and Sn>=tn 
	then       
	for i:=1 to n do         
		S i :=Si-di;     
	endfor    
	else    
	Place the executing process in the waiting queue of the first Si with Si<ti and set its program counter to the beginning of the Swait Operation.    
	endif   
//
Ssignal(S1,d1,…,Sn,dn)    
	for i:=1 to n do      
		Si:=Si+di;    
		Remove all the process waiting in the queue associated with Si into the ready queue.
	endfor;

信号量的应用

  • 利用信号量实现进程互斥:为使多个进程能互斥地访问某临界资源,只须为该资源设置一个互斥信号量 mutex,并设其初始值为 1,然后将各进程访问该资源的临界区 CS 置于 wait(mutex)和 signal(mutex)操作之间即可。

    wait(mutex)和 signal(mutex)必须成对地出现。缺少 wait(mutex)将会导致系统混乱,不能保证对临界资源的互斥访问,缺少 signal(mutex) 将会使临界资源永远不被释放,从而使因等待该资源而阻塞的进程不能被唤醒。

  • 利用信号量实现前趋关系:设有两个并发执行的进程P1和P2 ,P1 中有语句 S1;P2 中有语句 S2,我们希望在 S1 执行后再执行 S2。为实现这种前趋关系,我们只须使进程 P1 和 P2 共享一个公用信号量 S,并赋予其初值为 0。将signal(S)操作放在语句 S1 后面;而在 S2 语句前面插入 wait(S)操作。

管程机制

  • 系统中的各种硬件资源和软件资源,均可用数据结构抽象地描述其资源特性,即用少量信息和对该资源所执行的操作来表征该资源,而忽略了它们的内部结构和实现细节。利用共享数据结构抽象地表示系统中的共享资源,而把对该共享数据结构实施的操作定义为一组过程。进程对共享资源的申请、释放和其它操作,都是通过这组过程对共享数据结构的操作来实现的。这组过程还可以根据资源的情况,或接受或阻塞进程的访问,确保每次仅有一个进程使用共享资源。这样就可以统一管理对共享资源的所有访问,实现进程互斥。

  • 管程 (Monitors,也称为监视器) :一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据,是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源,管程被请求和释放资源的进程所调用,每次只准许一个进程进入管程,从而实现了进程互斥
    在这里插入图片描述

  • 组成:
    管程的名称。
    局部于管程的共享数据结构说明
    对该数据结构进行操作的一组过程
    对局部于管程的共享数据设置初始值的语句

//
type  monitor_name = MONITOR;
 <共享变量说明>; 
define <(能被其他模块引用的)过程名列表>; 
use  <(要调用的本模块外定义的)过程名列表>//
procedure <过程名>(<形式参数表>); 
	begin M 
	end;       
	function <函数名>(<形式参数表>):值类型; 
	begin M
	end;       
	begin <管程的局部数据初始化语句序列>; 
	end
  • 信息掩蔽:管程中的数据结构只能被管程中的过程访问,这些过程也是在管程内部定义的,供管程外的进程调用。局部于管程内部的过程也仅能访问管程内的数据结构,而管程中的数据结构以及过程(函数)的具体实现外部不可见。

  • 管程和进程
    进程定义的是私有数据结构 PCB,管程定义的是公共数据结构。二者都存在对各自数据结构上的操作,进程是由顺序程序执行有关的操作,管程主要是进行同步操作和初始化操作。设置进程的目的在于实现系统的并发性,管程的设置则是解决共享资源的互斥使用问题。进程通过调用管程中的过程对共享数据结构实行操作,该过程就如通常的子程序一样被调用,因而管程为被动工作方式,进程则为主动工作方式。进程之间能并发执行,而管程则不能与其调用者并发。进程具有动态性,由“创建”而诞生,由“撤销”而消亡,而管程则是操作系统中的一个资源管理模块,供进程调用。

  • 条件变量
    在利用管程实现进程同步时,必须设置同步工具,如两个同步操作原语wait和signal。当某进程通过管程请求获得临界资源而未能满足时,管程便调用wait原语使该进程等待,并将其排在等待队列上,仅当另一进程访问完成并释放该资源之后,管程才又调用signal原语,唤醒等待队列中的队首进程。

    当一个进程调用了管程,在管程中时被阻塞或挂起,直到阻塞或挂起的原因解除,而在此期间,如果该进程不释放管程,则其它进程无法进入管程,被迫长时间地等待,为了解决这个问题,引入了条件变量 condition。一个进程被阻塞或挂起的条件(原因)可有多个,因此在管程中设置了多个条件变量,对这些条件变量的访问,只能在管程中进行,管程中对每个条件变量都须予以说明,其形式为:Var x,y:condition。对条件变量的操作仅仅是 wait 和 signal,因此条件变量也是一种抽象数据类型,每个条件变量保存了一个链表,用于记录因该条件变量而阻塞的所有进程,同时提供的两个操作。x.wait:正在调用管程的进程因 x 条件需要被阻塞或挂起,则调用 x.wait 将自己插入到 x 条件的等待队列上,并释放管程,直到 x 条件变化。此时其它进程可以使用该管程。x.signal:正在调用管程的进程发现 x 条件发生了变化,则调用 x.signal,重新启动一个因 x 条件而阻塞或挂起的进程,如果存在多个这样的进程,则选择其中的一个,如果没有,则继续执行原进程,而不产生任何结果,这与信号量机制中的 signal 操作不同,因为后者总是要执行 s:=s+1 操作,总会改变信号量的状态

  • 如果有进程Q因x条件处于阻塞状态,当正在调用管程的进程P执行了x.signal操作后, 进程Q被重新启动。此时两个进程 P 和 Q,如何确定哪个执行,哪个等待:
    P 等待,直至 Q 离开管程或等待另一条件。
    Q 等待,直至 P 离开管程或等待另一条件。
    规定管程中的过程所执行的 signal 操作是过程体的最后一个操作,进程 P 执行 signal 操作后立即退出管程,因而进程 Q 马上被恢复执行。

经典进程的同步问题

生产者—消费者问题(The proceducer-consumer problem)(进程同步问题)

  • 为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有 n 个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区中,消费者进程可从一个缓冲区中取走产品去消费,不允许消费者进程到一个空缓冲区去取产品,也不允许生产者进程向一个已装满产品且尚未被取走的缓冲区中投放产品。

  • 我们可利用一个数组来表示上述的具有 n 个(0,1,…,n-1)缓冲区的缓冲池。用输入指针 in 来指示下一个可投放产品的缓冲区,每当生产者进程生产并投放一个产品后,in= (in+1)mod n,用输出指针 out 来指示下一个可从中获取产品的缓冲区,每当消费者进程取走一个产品后,out= (out+1) mod n。当 (in+1) mod n=out 时表示缓冲池满;而 in=out 则表示缓冲池空。此外,还引入了一个整型变量counter,其初始值为 0,每当生产者进程向缓冲池中投放一个产品后,使 counter 加 1;反之,每当消费者进程从中取走一个产品时,使 counter 减 1。

  • 利用记录型信号量解决生产者—消费者问题
    假定在生产者和消费者之间的公用缓冲池中,具有 n 个缓冲区,这时可利用互斥信号量 mutex 实现诸进程对缓冲池的互斥使用,利用信号量 empty和 full 分别表示缓冲池中空缓冲区和满缓冲区的数量。首先,在每个程序中用于实现互斥的 wait(mutex)和 signal(mutex)必须成对地出现。其次,对资源信号量 empty和 full 的 wait 和 signal 操作,同样需要成对地出现,但它们分别处于不同的程序中。最后,在每个程序中的多个 wait 操作顺序不能颠倒,应先执行对资源信号量的 wait 操作,然后再执行对互斥信号量的 wait 操作,否则可能引起进程死锁。

//Var mutex,empty,full: semaphore:=1,n,0;     
buffer:array[0,…,n-1] of item;     
in,out:  integer:=00;     
begin      
parbegin       
   ···
parend   
end
//
proceducer:  begin             
repeat 
	producer an item nextp;
	wait(empty)wait(mutex)buffer(in):=nextp;
	in:=(in+1) mod n;             
	signal(mutex)signal(full);             
	until false;           
end       
//
consumer:  begin            
repeat             
	wait(full)wait(mutex);             
	nextc:=buffer(out);             
	out:=(out+1) mod n;             
	signal(mutex)signal(empty);             
	consumer the item in nextc;            
	until false;           
end      
  • 利用AND信号量解决生产者-消费者问题
//
Var mutex,empty,full: semaphore:=1,n,0;     
buffer:array[0,…,n-1] of item;     
in out:  integer:=00;    
begin     
parbegin           
   ···
parend     
end
//
producer:  begin             
repeat              
	produce an item in nextp;
	Swait(empty,mutex)buffer(in):=nextp;             
	in:=(in+1)mod n;             
	Ssignal(mutex,full);            
	until false;          
end  
//
consumer:begin             
repeat              
	Swait(full,mutex);              
	Nextc:=buffer(out);              
	Out:=(out+1) mod n;              
	Ssignal(mutex,empty);              
	consumer the item in nextc;             
	until false;          
end    
  • 利用管程
    首先便是为它们建立一个管程,并命名为procducerconsumer,简称为PC。

    put(item)过程:生产者利用该过程将自己生产的产品投放到缓冲池中,并用整型变量 count 来表示在缓冲池中已有的产品数目,当 count≥n 时,表示缓冲池已满,生产者须等待。

    get(item)过程:消费者利用该过程从缓冲池中取出一个产品,当 count≤0 时,表示缓冲池中已无可取用的产品,消费者等待。

//
type producer-consumer=monitor     
Var in,out,count:  integer;      
buffer:  array[0,, n-1] of item;      
notfull,notempty:condition;

begin in:=out:=0;  
count:=0  
end
//
procedure entry put(item)        
	begin         
	if count>=n then notfull.wait;         
	buffer(in):=nextp;          
	in:=(in+1) mod n;
	count:=count+1if notempty.queue then notempty.signal;         
end
//
procedure entry get(item)        
	begin         
	if count<=0 then notempty.wait;         
	nextc:=buffer(out);         
	out:=(out+1) mod n;         
	count:=count-1if notfull.quene then notfull.signal;        
	end
//
producer:  begin         
	repeat          
	produce an item in nextp;          
	PC.put(item);         
	until false;        
end
//
consumer:  begin        
	repeat          
	PC.get(item);          
	consume the item in nextc;         
	until false;        
end

哲学家进餐问题(The Dinning Philosophers Problem)(同步问题)

  • 有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐,进餐完毕,放下筷子继续思考。

  • 利用记录型信号量解决哲学家进餐问题
    放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。为了实现对筷子的互斥使用,用一个信号量表示一只筷子,由五个信号量构成信号量数组,所有信号量均被初始化为 1。第 i 位哲学家的活动可描述为:

repeat     
	wait(chopstick[i])wait(chopstick[(i+1)mod 5]);
	eat;
	signal(chopstick[i])signal(chopstick[(i+1)mod 5]);  
	think;   
	until false;
  • 在以上描述中,当哲学家饥饿时,总是先去拿他左边的筷子, 成功后,再去拿他右边的筷子,又成功后便可进餐。进餐完毕,又先放下他左边的筷子,然后再放右边的筷子。虽然,上述解法可保证不会有两个相邻的哲学家同时进餐,但有可能引起死锁。

    解决方法:至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子,而偶数号哲学家则相反。

  • 利用 AND 信号量机制解决哲学家进餐问题

Var chopsiick array  of semaphore:=(1,1,1,1,1);     
processi     
	repeat       
		think;       
		Sswait(chopstick[(i+1)mod 5],chopstick[i]);       
		eat;       
		Ssignat(chopstick[(i+1)mod 5],chopstick[i]);      
	until false;

读者—写者问题(Reader-Writer Problem)

  • 一个数据文件或记录,可被多个进程共享。我们把只要求读该文件的进程称为“Reader 进程”,其他进程则称为“Writer 进程”,允许多个进程同时读一个共享对象,不允许一个 Writer 进程和其他 Reader 进程或 Writer 进程同时访问共享对象,即保证一个 Writer 进程必须与其他进程互斥地访问共享对象。

  • 利用记录型信号量解决读者—写者问题
    为实现 Reader 与 Writer 进程间在读或写时的互斥而设置了一个互斥信号量 Wmutex。 再设置一个整型变量 Readcount 表示正在读的进程数目。由于只要有一个 Reader 进程在读,便不允许 Writer 进程去写。因此,仅当 Readcount=0,表示尚无 Reader 进程在读时,Reader 进程才需要执行 Wait(Wmutex)操作。若 Wait(Wmutex)操作成功,Reader 进程便可去读,相应地,做 Readcount+1 操作。同理,仅当 Reader 进程在执行了 Readcount 减 1 操作后其值为 0 时,才须执行 signal(Wmutex)操作,以便让 Writer 进程写,又因为 Readcount 是一个可被多个 Reader 进程访问的临界资源,因此,也应该为它设置一个互斥信号量 rmutex。

//
Var rmutex,wmutex:  semaphore:=1,1;     
Readcount:  integer:=0;     
begin     
parbegin      
  		···
parend   
end
//
Reader:  begin         
	repeat          
	wait(rmutex)if readcount=0 then wait(wmutex);           
	Readcount:=Readcount+1signal(rmutex);
	perform read operation;
	wait(rmutex);          
	readcount:=readcount-1if readcount=0 then signal(wmutex)signal(rmutex);         
	until false;        
end 
//
writer: begin 
	repeat          
	wait(wmutex);          
	perform write operation;          
	signal(wmutex);         
	until false;        
end   
  • 利用信号量集机制
    它增加了一个限制,最多只允许 RN 个读者同时读,为此,引入了一个信号量 L,并赋予其初值为RN,通过执行 wait(L,1,1) 操作,来控制读者的数目。
//
Var RN integer;     
L, mx: semaphore:=RN,1;    
begin     
parbegin      
		···  
parend
//
reader: begin         
repeat          
	Swait(L,1,1)Swait(mx,1,0);
	perform read operation; 
	Ssignal(L,1);         
	until false;        
end   
//
writer: begin          
repeat           
	Swait(mx,1,1;L,RN,0); 
	perform write operation;           
	Ssignal(mx,1);         
	until false;        
end  

进程通信

  • 进程之间的信息交换
  • 低级进程通信(交换的信息量少):进程的互斥与同步,效率低。通信对用户不透明:共享数据结构的设置、数据的传送、进程的互斥与同步等,都必须由程序员去实现,操作系统只能提供共享存储器。
  • 高级进程通信:用户可直接利用操作系统所提供的一组通信命令高效地传送大量数据的一种通信方式,使用方便:OS隐藏了实现进程通信的具体细节,向用户提供了一组用于实现高级通信的命令(原语),通信过程对用户是透明的。这样就大大减少了通信程序编制上的复杂性。

共享存储器系统(Shared-Memory System)

  • 在共享存储器系统中,相互通信的进程共享某些数据结构或共享存储区,进程之间能够通过这些空间进行通信。

  • 基于共享数据结构的通信方式:在这种通信方式中,要求诸进程公用某些数据结构,借以实现诸进程间的信息交换。公用数据结构的设置及对进程间同步的处理,都是程序员的职责,这无疑增加了程序员的负担,而操作系统却只须提供共享存储器。因此,这种通信方式是低效的,只适于传递相对少量的数据。

  • 基于共享存储区的通信方式:进程可通过对共享存储区中数据的读或写来实现通信。进程在通信前,先向系统申请获得共享存储区中的一个分区,并指定该分区的关键字,若系统已经给其他进程分配了这样的分区,则将该分区的描述符返回给申请者。由申请者把获得的共享存储分区连接到本进程上,此后,便可像读、写普通存储器一样地读、写该公用存储分区。

消息传递系统(Message passing system)

  • 当前应用最为广泛的一种进程间的通信机制。在该机制中,进程间的数据交换是以格式化的消息(message) 为单位的,程序员直接利用操作系统提供的一组通信命令(原语),不仅能实现大量数据的传递,而且还隐藏了通信的实现细节。微内核与服务器之间的通信,都采用了消息传递机制。又由于它能很好地支持多处理机系统、分布式系统和计算机网络,因此它也成为这些领域最主要的通信工具。

  • 通信链路 (communication link):为使在发送进程和接收进程之间能进行通信,必须在两者之间建立一条通信链路。

    第一种方式是由发送进程在通信之前用显式的“建立连接”命令(原语)请求系统为之建立一条通信链路,在链路使用完后,也用显式方式拆除链路。这种方式主要用于计算机网络中。

    第二种方式是发送进程无须明确提出建立链路的请求,只须利用系统提供的发送命令(原语),系统会自动地为之建立一条链路,这种方式主要用于单机系统中。

    根据通信链路的连接方法,又可把通信链路分为两类:点—点连接通信链路,这时的一条链路只连接两个结点(进程),多点连接链路,指用一条链路连接多个(n>2)结点(进程)。

    根据通信方式的不同,则又可把链路分成两种:单向通信链路,只允许发送进程向接收进程发送消息,或者相反,双向链路,既允许由进程 A 向进程 B 发送消息,也允许进程 B 同时向进程 A 发送消息。

    根据通信链路容量的不同而把链路分成两类:一是无容量通信链路,在这种通信链路上没有缓冲区,因而不能暂存任何消息。有容量通信链路,指在通信链路中设置了缓冲区,因而能暂存消息,缓冲区数目愈多,通信链路的容量愈大。

  • 消息的格式
    可把一个消息分成消息头和消息正文两部分,消息头包括消息在传输时所需的控制信息,如源进程名、目标进程名、消息长度、消息类型、消息编号及发送的日期和时间,消息正文是发送进程实际上所发送的数据。在某些 OS 中,消息采用比较短的定长消息格式,这便减少了对消息的处理和存储开销,这种方式可用于办公自动化系统中,为用户提供快速的便笺式通信,但这对要发送较长消息的用户是不方便的。在有的 OS 中,采用变长的消息格式,即进程所发送消息的长度是可变的,系统无论在处理还是在存储变长消息时,都可能会付出更多的开销,但这方便了用户。这两种消息格式各有其优缺点,故在很多系统(包括计算机网络)中,是同时都用的。

  • 进程同步方式
    阻塞发送,阻塞接收:这种情况主要用于进程之间紧密同步(tight synchronization),发送进程和接收进程之间无缓冲时。这两个进程平时都处于阻塞状态,这种同步方式称为汇合(rendezrous)。

    无阻塞发送,阻塞接收:这是一种应用最广的进程同步方式,发送进程不阻塞,它可以尽快地把一个或多个消息发送给多个目标,而接收进程平时则处于阻塞状态,直到发送进程发来消息时才被唤醒。

    无阻塞发送,无阻塞接受:发送进程和接收进程都在忙于自己的事情,仅当发生某事件使它无法继续运行时,才把自己阻塞起来等待,接收进程也可以连续地从消息队列中取得消息,也不必等待,只有当消息队列中的消息数已达到 n 个时,即消息队列已满,发送进程无法向消息队列中发送消息时才会阻塞,类似地,只有当消息队列中的消息数为 0,接收进程已无法从消息队列中取得消息时才会阻塞。

  • 直接消息传递系统(直接通信方式):即发送进程利用OS所提供的发送命令(原语),直接把消息发送给目标进程,要求发送进程和接收进程都以显式方式提供对方的标识符。

    通信命令 (原语):
    Send(Receiver,message):发送一个消息给接收进程。Receive(Sender,message):接收 Sender发来的消息。某些情况下,接收进程可与多个发送进程通信,因此,它不可能事先指定发送进程,对于这样的应用,在接收进程接收消息的原语中,表示源进程的参数,也是完成通信后的返回值,接收原语可表示为: Receive (id,message)。

    可以利用直接通信原语来解决生产者—消费者问题
    当生产者生产出一个产品 (消息)后,便用 Send 原语将消息发送给消费者进程,而消费者进程则利用 Receive 原语来得到一个消息,如果消息尚未生产出来,消费者必须等待,直至生产者进程将消息发送过来。

//
repeat 
	produce an item in nextp;
	send(consumer,nextp);    
until false;
//
repeat    
	receive(producer,nextc);
	consume the item in nextc;   
until false;
  • 间接通信
    通信需要通过作为共享数据结构的实体,该实体用来暂存发送进程发送给目标进程的消息。接收进程则从该实体中取出对方发送给自己的消息。通常把这种中间实体称为信箱。消息在信箱中可以安全地保存,只允许核准的目标用户随时读取。因此,利用信箱通信方式,既可实现实时通信,又可实现非实时通信

    信箱定义为一种数据结构,在逻辑上,可以将其分为信箱头和信箱体。进程可利用信箱创建原语来建立一个新信箱,创建者进程应给出信箱名字、信箱属性(公用、私用或共享),对于共享信箱,还应给出共享者的名字,当进程不再需要读信箱时,可用信箱撤消原语将之撤消。

    消息的发送和接收:当进程之间要利用信箱进行通信时,必须使用共享信箱,并利用系统提供的下述通信原语进行通信,Send(mailbox,message):将一个消息发送到指定信箱Receive(mailbox,message):从指定信箱中接收一个消息。

    私用信箱:用户进程可为自己建立一个新信箱,并作为该进程的一部分,信箱的拥有者有权从信箱中读取消息,其他用户则只能将自己构成的消息发送到该信箱中,这种私用信箱可采用单向通信链路的信箱来实现,当拥有该信箱的进程结束时,信箱也随之消失。

    公用信箱:它由操作系统创建,并提供给系统中的所有核准进程使用核准进程既可把消息发送到该信箱中,也可从信箱中读取发送给自己的消息,显然,公用信箱应采用双向通信链路的信箱来实现。通常,公用信箱在系统运行期间始终存在。

    共享信箱:它由某进程创建,在创建时或创建后指明它是可共享的,同时须指出共享进程(用户) 的名字,信箱的拥有者和共享者都有权从信箱中取走发送给自己的消息

    在利用信箱通信时,在发送进程和接收进程之间存在以下四种关系:
    一对一关系:这时可为发送进程和接收进程建立一条两者专用的通信链路,使两者之间的交互不受其他进程的干扰。

    多对一关系:允许提供服务的进程与多个用户进程之间进行交互,也称为客户/服务器交互(client/server interaction)。

    一对多关系:允许一个发送进程与多个接收进程进行交互,使发送进程可用广播方式向接收者发送消息。

    多对多关系:允许建立一个公用信箱,让多个进程都能向信箱中投递消息;也可从信箱中取走属于自己的消息。

消息缓冲队列通信机制

  • 消息缓冲队列通信机制中的数据结构:消息缓冲区
type message buffer=record               
	sender;发送者进程标识符              
	size;消息长度
	text;消息正文
	next;指向下一个消息缓冲区的指针               
end
  • PCB 中有关通信的数据项:
    除了需要为进程设置消息缓冲队列外,还应在进程的PCB 中增加消息队列队首指针,以及用于实现同步的互斥信号量 mutex 和资源信号量 sm。
type processcontrol block=record
	mq;消息队列队首指针                 
	mutex;消息队列互斥信号量                
	sm;消息队列资源信号量
end
  • 发送原语:发送进程在利用发送原语发送消息之前,应先在自己的内存空间设置发送区 a。把待发送的消息正文、发送进程标识符、消息长度等信息填入其中,然后调用发送原语,把消息发送给目标(接收)进程。发送原语首先根据发送区 a 中所设置的消息长度 a.size 来申请缓冲区 i,接着把发送区 a 中的信息复制到缓冲区 i 中,为了能将 i 挂在接收进程的消息队列 mq 上,应先获得接收进程的内部标识符 j,然后将 i 挂在 j.mq 上,由于该队列属于临界资源,故在执行 insert 操作的前后,都要执行 wait 和 signal 操作。
procedure send(receiver,a)    
begin     
	getbuf(a.size,i);  
	i.sender:= a.sender;          
	i.size:=a.size;     
	i.text:=a.text;     
	i.next:=0getid(PCB set,receiver.j)wait(j.mutex)insert(j.mq,i)signal(j.mutex)signal(j.sm);   
end
  • 接收原语:接收进程调用接收原语 receive(b),从自己的消息缓冲队列 mq 中摘下第一个消息缓冲区 i并将其中的数据复制到以 b 为首址的指定消息接收区内。
procedure receive(b)    
begin     
	j:= internal name;   
	wait(j.sm)wait(j.mutex)remove(j.mq,i)signal(j.mutex);     
	b.sender:=i.sender;        
	b.size:=i.size;     
	b.text:=i.text;   
end 

管道(pipe)通信系统

  • 所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件,向管道(共享文件)提供输入的发送进程(即写进程)以字符流形式将大量的数据送入管道,接受管道输出的接收进程(即读进程)则从管道中接收(读)数据。

  • 互斥:即当一个进程正在对pipe执行读/写操作时,其它(另一)进程必须等待。

    同步:指当写(输入)进程把一定数量的数据写入pipe,便去睡眠等待,直到读(输出)进程取走数据后再把它唤醒,当读进程读一空pipe时,也应睡眠等待,直至写进程将数据写入管道后才将之唤醒。

    确定对方是否存在:只有确定了对方已存在时才能进行通信。

客户机-服务器系统(Client-Server system)

  • 套接字(Socket): 一开始,套接字被设计用在同一台主机上多个应用程序之间的通信(即进程间的通信),主要是为了解决多对进程同时通信时端口和物理线路的多路复用问题,随着计算机网络技术的发展以及UNIX 操作系统的广泛使用,套接字已逐渐成为最流行的网络通信程序接口之一。

  • 远程过程调用和远程方法调用:远程过程(函数)调用RPC(Remote Procedure Call),是一个通信协议,用于通过网络连接的系统,该协议允许运行于一台主机系统上的进程调用另一台主机系统上的进程,对程序员表现为常规的过程调用,无需额外地为此编程,如果涉及的软件采用面向对象编程,那么远程过程调用亦可称做远程方法调用。

  • 过程:本地过程调用者以一般方式调用远程过程在本地关联的客户存根,传递相应的参数,然后将控制权转移给客户存根。客户存根执行,完成包括过程名和调用参数等信息的消息建立,将控制权转移给本地客户进程。本地客户进程完成与服务器的消息传递,将消息发送到远程服务器进程。远程服务器进程接收消息后转入执行,并根据其中的远程过程名找到对应的服务器存根,将消息转给该存根。该服务器存根接到消息后,由阻塞状态转入执行状态,拆开消息从中取出过程调用的参数,然后以一般方式调用服务器上关联的过程。 在服务器端的远程过程运行完毕后,将结果返回给与之关联的服务器存根。该服务器存根获得控制权运行,将结果打包为消息,并将控制权转移给远程服务器进程。远程服务器进程将消息发送回客户端。本地客户进程接收到消息后,根据其中的过程名将消息存入关联的客户存根,再将控制权转移给客户存根。客户存根从消息中取出结果,返回给本地调用者进程,并完成控制权的转移。

线程

线程的基本概念

  • 由于进程是一个资源的拥有者,因而在创建、撤消和切换中,系统必须为之付出较大的时空开销,在系统中所设置的进程,其数目不宜过多,进程切换的频率也不宜过高,这也就限制了并发程度的进一步提高。

  • 线程(Threads)(调度和分派的基本单位):为了减少程序在并发执行时所付出的时空开销,使OS具有更好的并发性。

  • 线程与进程的比较:
    调度:在传统的操作系统中,作为拥有资源的基本单位和独立调度、分派的基本单位都是进程,而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位。

    并发性:在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行。

    系统开销:在创建或撤消进程时,系统都要为之创建和回收PCB,分配或回收资源,开销明显大于线程创建或撤消时的开销。类似地, 在进程切换时,涉及到当前进程 CPU 环境的保存及新被调度运行进程的 CPU 环境的设置。线程的切换则仅需保存和设置少量寄存器内容不涉及存储器管理方面的操作,由于一个进程中的多个线程具有相同的地址空间,在同步和通信的实现方面线程也比进程容易,在一些操作系统中,线程的切换、同步和通信都无须操作系统内核的干预。

  • 线程的属性(轻型实体)
    线程中的实体基本上不拥有系统资源,只是有一点必不可少的、 能保证其独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块 TCB,用于指示被执行指令序列的程序计数器,保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

    独立调度和分派的基本单位:可并发执行。

    共享进程资源:在同一进程中的各个线程都可以共享该进程所拥有的资源,这首先表现在所有线程都具有相同的地址空间(进程的地址空间),这意味着线程可以访问该地址空间中的每一个虚地址,此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。

  • 线程的状态
    状态参数:在 OS 中的每一个线程都可以利用线程标识符和一组状态参数进行描述。
    寄存器状态,它包括程序计数器 PC 和堆栈指针中的内容。堆栈,在堆栈中通常保存有局部变量和返回地址。线程运行状态,优先级,线程专有存储器,用于保存线程自己的局部变量拷贝。信号屏蔽,即对某些信号加以屏蔽。

    线程运行状态:
    执行状态,表示线程正获得处理机而运行。
    就绪状态,指线程已具备了各种执行条件,一旦获得 CPU 便可执行的状态。
    阻塞状态,指线程在执行中因某事件而受阻,处于暂停执行时的状态。

  • 线程的创建
    在多线程 OS 环境下,应用程序在启动时,通常仅有一个线程在执行,该线程被称为“初始化线程”。它可根据需要再去创建若干个线程,在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等,在线程创建函数执行完后,将返回一个线程标识符供以后使用。

  • 线程的终止
    线程完成了自己的工作后自愿退出,或线程在运行中出现错误或由于某种原因而被其它线程强行终止。但有些线程(主要是系统线程),在它们一旦被建立起来之后,便一直运行下去而不再被终止。

    在大多数的 OS 中,线程被中止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了分离函数后,被终止的线程才与资源分离,此时的资源才能被其它线程利用,已被终止但尚未释放资源的线程,仍可以被需要它的线程所调用,以使被终止线程重新恢复运行。为此,调用者线程须调用一条被称为等待线程终止的连接命令,来与该线程进行连接,如果在一个调用者线程调用“等待线程终止”的连接命令试图与指定线程相连接时,若指定线程尚未被终止,则调用连接命令的线程将会阻塞,直至指定线程被终止后才能实现它与调用者线程的连接并继续执行,若指定线程已被终止,则调用者线程不会被阻塞而是继续执行。

  • 多线程 OS 中的进程
    作为系统资源分配的单位,可包括多个线程,通常,一个进程都含有多个相对独立的线程,其数目可多可少, 但至少也要有一个线程。由进程为这些线程提供资源及运行环境,使这些线程可并发执行,在OS 中的所有线程都只能属于某一个特定进程。进程不是一个可执行的实体,在多线程 OS 中,是把线程作为独立运行的基本单位。

线程间的同步和通信

  • 互斥锁(mutex)
    是一种比较简单的、用于实现线程间对资源互斥访问的机制,由于操作互斥锁的时间和空间开销都较低,因而较适合于高频度使用的关键共享数据和程序段。互斥锁可以有两种状态,即开锁(unlock)和关锁(lock)状态。相应地,可用两条命令(函数)对互斥锁进行操作。为了减少线程被阻塞的机会,在有的系统中还提供了一种用于 mutex 上的操作命令Trylock,当一个线程在利用 Trylock 命令去访问 mutex 时,若 mutex 处于开锁状态,Trylock 将返回一个指示成功的状态码,反之,若 mutex 处于关锁状态,Trylock 并不会阻塞该线程,而只是返回一个指示操作失败的状态码。

  • 条件变量:
    在许多情况下,只利用 mutex 来实现互斥访问可能会引起死锁。每一个条件变量通常都与一个互斥锁一起使用,在创建一个互斥锁时便联系着一个条件变量,单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入,而条件变量则用于线程的长期等待,直至所等待的资源成为可用的资源,线程首先对 mutex 执行关锁操作,若成功便进入临界区,然后查找用于描述该资源状态的数据结构,以了解资源的情况。只要发现所需资源 R 正处于忙碌状态,线程便转为等待状态,并对 mutex 执行开锁操作后,等待该资源被释放。若资源处于空闲状态,表明线程可以使用该资源,于是将该资源设置为忙碌状态,再对 mutex 执行开锁操作。

    原来占有资源 R 的线程在使用完该资源后,便释放该资源。在大多数情况下,由于所释放的是临界资源,此时所唤醒的只能是在条件变量上等待的某一个线程,其它线程仍继续在该队列上等待。但如果线程所释放的是一个数据文件,该文件允许多个线程同时对它执行读操作,在这种情况下,当一个写线程完成写操作并释放该文件后,如果此时在该条件变量上还有多个读线程在等待,则该线程可以唤醒所有的等待线程。

//
Lock mutex;
check data structures;
while(resource busy)wait(condition variable);
	mark resource as busy;
unlock mutex;
//
Lock mutex  
mark resource as free; 
unlock mutex;                                  
wakeup(condition variable)
  • 信号量机制:为了提高效率,可为线程和进程分别设置相应的信号量

    私用信号量(private samephore) :当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的命令来创建一私用信号量,其数据结构存放在应用程序的地址空间中,私用信号量属于特定的进程所有,OS 并不知道私用信号量的存在,一旦发生私用信号量的占用者异常结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复,也不能将它传送给下一个请求它的线程。

    公用信号量(public semaphort) :公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的,其数据结构是存放在受保护的系统存储区中,由 OS 为它分配空间并进行管理,故也称为系统信号量。如果信号量的占有者在结束时未释放该公用信号量,则 OS 会自动将该信号量空间回收,并通知下一进程(安全)。

线程的实现方式

  • 内核支持线程KST(Kernel Supported Threads)
    内核支持线程KST是在内核的支持下运行的,它们的创建、阻塞、撤消和切换等,也都是在内核空间实现的。为了对内核线程进行控制和管理,在内核空间也为每一个内核线程设置了一个线程控制块。内核根据该控制块而感知某线程的存在,并对其加以控制。当前大多数OS都支持内核支持线程,内核级线程切换的代价要比用户级线程大

    优点:在多处理器系统中,内核能够同时调度同一进程中的多个线程并行执行,如果进程中的一个线程被阻塞了,内核可以调度该进程中的其它线程占有处理器运行,也可以运行其它进程中的线程。内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小,内核本身也可以采用多线程技术,可以提高系统的执行速度和效率。

    缺点是:对于用户的线程切换而言,其模式切换的开销较大。在同一个进程中,从一个线程切换到另一个线程时,需要从用户态转到内核态进行,这是因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的,系统开销较大

    实现:系统在创建一个新进程时,便为它分配一个任务数据区PTDA(Per Task Data Area),其中包括若干个线程控制块TCB空间。

  • 用户级线程 ULT(User Level Threads)
    仅存在于用户空间中,对于这种线程的创建、撤消、线程之间的同步与通信等功能,都无须利用系统调用来实现。对于用户级线程的切换,通常发生在一个应用进程的诸多线程之间,无须内核的支持。由于切换的规则远比进程调度和切换的规则简单,因而使线程的切换速度特别快。由于这些线程的任务控制块都是设置在用户空间,而线程所执行的操作也无须内核的帮助,内核完全不知道用户级线程的存在。对于设置了用户级线程的系统,其调度仍是以进程为单位进行的。在采用轮转调度算法时,各个进程轮流执行一个时间片,假如系统中设置的是内核支持线程,则调度便是以线程为单位进行的。在采用轮转法调度时,是各个线程轮流执行一个时间片。

    优点:线程切换不需要转换到内核空间,对一个进程而言,其所有线程的管理数据结构均在该进程的用户空间中,管理线程切换的线程库也在用户地址空间运行。因此,进程不必切换到内核方式来做线程管理,从而节省了模式切换的开销,也节省了内核的宝贵资源,调度算法可以是进程专用的。用户级线程的实现与操作系统平台无关,因为对于线程管理的代码是在用户程序内的,属于用户程序的一部分,所有的应用程序都可以对之进行共享,用户级线程甚至可以在不支持线程机制的操作系统平台上实现。

    缺点:系统调用的阻塞问题。当线程执行一个系统调用时,不仅该线程被阻塞,而且进程内的所有线程都会被阻塞。在内核支持线程方式中,进程中的其它线程仍然可以运行,在单纯的用户级线程实现方式中,多线程应用不能利用多处理机进行多重处理。

  • 组合方式
    有些操作系统把用户级线程和内核支持线程两种方式进行组合,提供了组合方式 ULT/KST 线程。在组合方式线程系统中,内核支持多 KST 线程的建立、调度和管理,同时,也允许用户应用程序建立、调度和管理用户级线程。组合方式线程中,同一个进程内的多个线程可以同时在多处理器上并行执行,而且在阻塞一个线程时,并不需要将整个进程阻塞。

线程的实现

  • 内核支持线程的实现
    系统在创建一个新进程时,便为它分配一个任务数据区 PTDA(Per Task Data Area),其中包括若干个线程控制块 TCB 空间,在每一个TCB 中可保存线程标识符、优先级、线程运行的 CPU状态等信息,虽然这些信息与用户级线程 TCB 中的信息相同,但现在却是被保存在内核空间中。每当进程要创建一个线程时,便为新线程分配一个 TCB,将有关信息填入该 TCB 中,并为之分配必要的资源,当 PTDA 中的所有 TCB 空间已用完,而进程要创建新的线程时,只要其所创建的线程数目未超过系统的允许值,系统可再为之分配新的 TCB 空间。在撤消一个线程时,也应回收该线程的所有资源和 TCB,在有的系统中为了减少创建和撤消一个线程时的开销,在撤消一个线程时,并不立即回收该线程的资源和 TCB,当以后再要创建一个新线程时,便可直接利用已被撤消但仍保持有资源和 TCB 的线程作为新线程。

  • 用户级线程的实现
    所有的用户级线程都具有相同的结构,它们都运行在一个中间系统上

    运行时系统(Runtime System)
    所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤消线程的函数、线程同步和通信的函数以及实现线程调度的函数等。正因为有这些函数,才能使用户级线程与内核无关,运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。在传统的 OS 中,进程在切换时必须先由用户态转为核心态,再由核心来执行切换任务。而用户级线程在切换时则不需转入核心态,而是由运行时系统中的线程切换过程来执行切换任务,该过程将线程的 CPU 状态保存在该线程的堆栈中,然后按照一定的算法选择一个处于就绪状态的新线程运行,将新线程堆栈中的 CPU 状态装入到 CPU 相应的寄存器中,一旦将栈指针和程序计数器切换后,便开始了新线程的运行。不论在传统的 OS 中,还是在多线程 OS 中,系统资源都是由内核管理的,在传统的 OS 中,进程是利用 OS 提供的系统调用来请求系统资源的,系统调用通过软中断(如 trap) 机制进入OS 内核,由内核来完成相应资源的分配,用户级线程是不能利用系统调用的,当线程需要系统资源时,是将该要求传送给运行时系统,由后者通过相应的系统调用来获得系统资源的。

    内核控制线程
    这种线程又称为轻型进程 LWP(Light Weight Process),每一个进程都可拥有多个 LWP, 同用户级线程一样,每个 LWP 都有自己的数据结构,它们也可以共享进程所拥有的资源。LWP 可通过系统调用来获得内核提供的服务,当一个用户级线程运行时,只要将它连接到一个 LWP 上,此时它便具有了内核支持线程的所有属性,为了节省系统开销,不可能设置太多的 LWP,而把这些 LWP 做成一个缓冲池,称为“线程池”,用户进程中的任一用户线程都可以连接到 LWP 池中的任何一个 LWP 上,为使每一用户级线程都能利用 LWP 与内核通信, 可以使多个用户级线程多路复用一个 LWP。只有当前连接到 LWP 上的线程才能与内核通信,其余进程或者阻塞,或者等待 LWP,每一个 LWP 都要连接到一个内核级线程上,这样,通过 LWP 可把用户级线程与内核线程连接起来。用户级线程可通过 LWP 来访问内核, 但内核所看到的总是多个 LWP 而看不到用户级线程,当用户级线程需要与内核通信时,需借助于 LWP,而且每个要通信的用户级线程都需要一个 LWP。在内核级线程执行操作时,如果发生阻塞,则与之相连接的多个 LWP 也将阻塞,使连接到 LWP 上的用户级线程也被阻塞,如果进程中只包含了一个 LWP,此时进程也应阻塞,但如果在一个进程中含有多个 LWP,则当一个 LWP 阻塞时,进程中的另一个 LWP 可继续执行,即使进程中的所有 LWP 全部阻塞,进程中的线程也仍然能继续执行,只是不能再去访问内核

  • 用户级线程与内核控制线程的连接
    一对一模型
    映射每个用户线程到一个内核线程,当一个线程阻塞时,允许调度另一个线程运行,在多处理机系统中,则有多个线程并行执行,每创建一个用户线程相应地就需要创建一个内核线程,开销较大,因此需要限制整个系统的线程数。

    多对一模型
    该模型是将多个用户线程映射到一个内核控制线程,线程管理是由用户空间的线程库来完成的,因此效率更高。为了管理方便,这些用户线程一般属于一个进程,运行在该进程的用户空间,对这些线程的调度和管理也是在该进程的用户空间中完成。当用户线程需要访问内核时,才将其映射到一个内核控制线程上,但每次只允许一个线程进行映射。 开销小,效率高,但当一个线程在访问内核时发生阻塞,则整个进程都会被阻塞,而且在多处理机系统中,一个进程的多个线程无法实现并行

    多对多模型
    将多个用户线程映射到多个内核控制线程,多对多模型的开发人员可以创建任意多的用户线程,并且相应内核线程能在多处理器系统上并发执行,内核控制线程的数目可以根据应用进程和系统的不同而变化,可以比用户线程少,也可以与之相同,但也允许绑定某个用户线程到一个内核线程。这个变种,有时称为双层模型。

发布了30 篇原创文章 · 获赞 1 · 访问量 407

猜你喜欢

转载自blog.csdn.net/weixin_46265246/article/details/105058108