【操作系统】第二章-进程的描述与控制

第二章、进程的描述与控制

前言

在传统的操作系统中,为了提高资源利用率和系统吞吐量,通常采用多道程序技术,将多个程序同时装入内存,并使之并发运行,传统意义上的程序不再能独立运行。此时,作为资源分配和独立运行的基本单位都是进程。操作系统所具有的四大特征也都是基于进程而形成的,并从进程的角度对操作系统进行研究。可见,在操作系统中,进程是一个极其重要的概念。因此,本章专门对进程进行详细阐述。

1.前趋图和程序执行

1.1 前趋图

在这里插入图片描述

1.2 程序顺序执行

  1. 程序的顺序执行

    下图中I代表输入操作,C代表计算操作,P代表打印操作,用箭头指示操作的先后次序。

    在这里插入图片描述

  2. 程序顺序执行时的特征

    ①顺序性

    ②封闭性

    ③可再现性

1.3 程序并发执行

  1. 程序并发执行

    在这里插入图片描述

  2. 程序并发执行时的特征

    ①间断性

    ②失去封闭性

    ③不可再现性

2.进程的描述

2.1 进程的定义和特征

  1. 进程的定义

    为了使参与并发执行的每个程序(含数据)都能独立地运行,在操作系统中必须为之配置一个专门的数据结构,称为进程控制块(Process Control Block,PCB)。系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。这样,由程序段、相关的数据段和PCB三部分便构成了进程实体(又称进程映像)。一般情况下,我们把进程实体就简称为进程,例如,所谓创建进程,实质上是创建进程实体中的PCB;而撤销进程,实质上是撤销进程的PCB。

    对于进程的定义,从不同的角度可以有不同的定义,其中较典型的定义有:

    (1) 进程是程序的一次执行。

    (2) 进程是一个程序及其数据在处理机上顺序执行时所发生的活动。

    (3) 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。

    在引入了进程实体的概念后,我们可以把传统OS中的进程定义为:“进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位”。

  2. 进程的特征

    (1) 动态性

    (2) 并发性

    (3) 独立性

    (4) 异步性

2.2 进程的基本状态及转换

  1. 进程的三种基本状态

    (1) 就绪(Ready)状态

    (2) 执行(Running)状态

    (3) 阻塞(Block)状态

  2. 三种基本状态的转换

    在这里插入图片描述

  3. 创建状态和终止状态

    (1) 创建状态

    如前所述,进程是由创建而产生。创建一个进程是个很复杂的过程,一般要通过多个步骤才能完成:如首先由进程申请一个空白PCB,并向PCB中填写用于控制和管理进程的信息;然后为该进程分配运行时所必须的资源;最后,把该进程转入就绪状态并插入就绪队列之中。但如果进程所需的资源尚不能得到满足,比如系统尚无足够的内存使进程无法装入其中,此时创建工作尚未完成,进程不能被调度运行,于是把此进程所处的状态称为创建状态。

    (2) 终止状态

    进程的终止也要通过两个步骤:首先,是等待操作系统进行善后处理,最后将其PCB清零,并将PCB空间返还系统。当一个进程达到了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结,它将进入终止状态。进入终止状态的进程以后不能再执行,但在操作系统中依然保留一个记录,其中保存状态码和一些计时统计数据,供其他进程收集。一旦其他进程完成了对其信息的提取之后,操作系统将删除该进程,即将其PCB清零,并将该空白PCB返还系统。下图就是增加了创建状态和终止状态后进程的五种状态及转换关系图。

    在这里插入图片描述

2.3 挂起操作和进程状态的转换

在许多系统中,进程处理就绪、执行和阻塞三种最基本的状态外,为了系统和用户观察和分析进程的需要,还引入了一个对进程的重要操作——挂起操作。当该操作作用于某个进程时,该进程将被挂起,意味着此时该进程处于静止状态。如果进程正在执行,它将暂停执行。若原本处于就绪状态,则该进程此时暂不接受调度。与挂起操作对应的操作是激活操作。

  1. 挂起操作的引入

    引入挂起操作的原因,是基于系统和用户的需要:

    (1) 终端用户需要。

    (2) 父进程请求。

    (3) 负荷调节的需要。

    (4) 操作系统的需要。

  2. 引入挂起原语操作后三个进程状态的转换

    (1) 活动就绪 -> 静止就绪。

    (2) 活动阻塞 -> 静止阻塞。

    (3) 静止就绪 -> 活动就绪。

    (4) 静止阻塞 -> 活动阻塞。

  3. 引入挂起操作后五个进程状态的转换

    (1) NULL -> 创建。

    (2) 创建 -> 活动就绪。

    (3) 创建 -> 静止就绪。

    (4) 执行 -> 终止。

    在这里插入图片描述

2.4 进程管理中的数据结构

  1. 操作系统中用于管理控制的数据结构

    在计算机系统中,对于每个资源和每个进程都设置了一个数据结构,用于表征其实体,我们称之为资源信息表或进程信息表,其中包含了资源或进程的标识、描述、状态等信息以及一批指针。通过这些指针,可以将同类资源或进程的信息表,或者同一进程所占用的资源信息表分类链接成不同的队列,便于操作系统进行查找。如下图,OS管理的这些数据结构一般分为以下四类:内存表、设备表、文件表和用于进程管理的进程表,通常进程表又被称为进程控制块PCB。本节着重介绍PCB,其它的表将在后面的章节陆续介绍。

    在这里插入图片描述

  2. 进程控制块PCB的作用

    为了便于系统描述和管理进程的运行,在OS的核心为每个进程专门定义了一个数据结构——进程控制块PCB(Process Control Block)。PCB作为进程实体的一部分,记录了操作系统所需的,用于描述进程的当前情况以及管理进程运行的全部信息,是操作系统中最重要的记录性数据结构。

    PCB的作用是是一个在多道程序环境下不能独立运行的程序(含数据)成为一个能独立运行的基本单位,一个能与其他进程并发执行的进程。下面对PCB的具体作用作进一步阐述:

    (1) 作为独立运行基本单位的标志。

    (2) 能实现间断性运行方式。

    (3) 提供进程管理所需的信息。

    (4) 提供进程调度所需的信息。

    (5) 实现与其它进程的同步与通信。

  3. 进程控制块中的信息

    在进程控制块中,主要包括下述四个方面的信息。

    (1) 进程标识符

    进程标识符用于唯一地标识一个进程。一个进程通常有两种标识符:

    • 外部标识符。为了方便用户(进程)对进程的访问,须为每一个进程设置一个外部标识符。它是由创建这提供的,通常由字母、数字组成。为了描述进程的家族关系,还应设置父进程标识及子进程标识。此外,还可以设置用户标识,以指示拥有该进程的用户。
    • 内部标识符。为了方便系统对进程的使用,在OS中又为进程设置了内部标识符,即赋予每一个进程一个唯一的数字标识符,它通常是一个进程的序号。

    (2) 处理机状态

    处理机状态信息也称为处理机的上下文,主要是由处理机的各种寄存器中的内容组成的。这些寄存器包括:①通用寄存器,又称为用户可视寄存器,它们是用户程序可以访问的,用于暂存信息,在大多数处理机中,有8~32个通用寄存器,在RISC结构的计算机中可超过100个;②指令计数器,其中存放了要访问的下一条指令地址;③程序状态字PSW,其中含有状态信息,如条件码、执行方式、中断屏蔽标志等;④用户栈指针,指每个用户进程都有一个或若干个与之相关的系统栈,用于存放过程和系统调用参数及调用地址。栈指针指向该栈的栈顶。处理机处于执行状态时,正在处理的许多信息都是放在寄存器中。当进程被切换时,处理机状态信息都必须保存在相应的PCB中,以便在该进程重新执行时能再从断电继续执行。

    (3) 进程调度信息

    在OS进行调度时,必须了解进程的状态及有关进程调度的信息,这些信息包括:①进程状态,指明进程的当前状态,它是作为进程调度和兑换时的依据;②进程优先级,是用于描述进程使用处理机的优先级别的一个整数,优先级高的进程应优先获得处理机;③进程调度所需的其他信息,它们与所采用的进程调度算法有关,比如,进程已等待CPU的时间总和、进程已执行的时间总和等;④事件,是指进程由执行状态变为阻塞状态所等待发生的事件,即阻塞原因。

    (4) 进程控制信息

    是指用于进程控制所必须的信息,它包括:①程序和数据的地址,进程实体中的程序和数据的内存或外存地(首)址,以便再调度到该进程执行时,能从PCB中找到其程序和数据;②进程同步和通信机制,这是实现进程同步和进程通信时必需的机制,如消息队列指针、信号量等,它们可能全部或部分地放在PCB中;③资源清单,在该清单中列出了进程在运行期间所需的全部资源(除CPU以外),另外还有一张已分配到该进程的资源的清单;④链接指针,它给出了本进程(PCB)所在队列中的下一个进程的PCB的首地址。

  4. 进程控制块的组织方式

    在一个系统中,通常可拥有数十个、数百个乃至数千个PCB。为了能对它们加以有效的管理,应该用适当的方式将这些PCB组织起来。目前常用的组织方式有以下三种。

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

    在这里插入图片描述

    (2) 链接方式,即把具有相同状态进程的PCB分别通过PCB中的链接子链接成一个队列。这样,可以形成就绪队列、若干个阻塞队列和空白队列等。对就绪队列而言,往往按进程的优先级将PCB从高到低进行排序,将优先级高的进程PCB排在队列的前面。同样,也可把处于阻塞状态进程的PCB根据其阻塞原因的不同,排成多个阻塞队列,如等待I/O操作完成的队列和等待分配内存的队列等,如下图。

    在这里插入图片描述

    (3) 索引方式,即系统根据所有进程状态的不同,建立几张索引表,例如,就绪索引表、阻塞索引表等,并把索引表在内存的首地址记录在内存的一些专用单元中。在每个索引表的表目中,记录具有相应状态的某个PCB在PCB表中的地址,如下图。

    在这里插入图片描述

3.进程控制

进程控制是进程管理中最基本的功能,主要包括创建新进程、终止已完成的进程、将因发生异常情况而无法继续运行的进程置于阻塞状态、负责进程运行中的状态转换等功能。如当一个正在执行的进程因等待某事件而暂时不能继续执行时,将其转变为阻塞状态,而在该进程所期待的事件出现后,又将该进程转换为就绪状态等。进程控制一般是由OS的内核中的原语来实现的。

3.1 操作系统内核

不同类型和规模的OS,它们的内核所包含的功能间存在着一定的差异,但大多数OS内核都包含了以下两大方面的功能:

  1. 支撑功能

    (1) 中断处理。

    (2) 时钟管理。

    (3) 原语操作。

  2. 资源管理功能

    (1) 进程管理。

    (2) 存储器管理。

    (3) 设备管理。

3.2 进程的创建

  1. 进程的层次结构

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

  2. 进程图

    为了形象地描述一个进程的家族关系而引入了进程图(Process Gragh)。

    在这里插入图片描述

  3. 引起创建进程的事件

    为使进程之间能并发运行,应先为它们分别创建进程。导致一个进程去创建另一个进程的典型事件有四类:

    (1) 用户登录。

    (2) 作业调度。

    (3) 提供服务。

    (4) 应用请求。

  4. 进程的创建(Creation of Process)

    在系统中每当出现了创建新进程的请求后,OS便调用进程创建原语Creat按下述步骤创建一个新进程:

    (1) 申请空白PCB,为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB。

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

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

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

3.3 进程的终止

  1. 引起进程终止(Termination of Process)的事件

    (1) 正常结束,表示进程的任务已经完成,准备退出运行。

    (2) 异常结束,是指进程在运行时发生了某种异常事件,使程序无法继续运行。

    (3) 外界干预,是指进程应外界的请求而终止运行。

  2. 进程的终止过程

    如果系统中发生了要求终止进程的某事件,OS便调用进程终止原语,按下述过程去终止指定的进程:

    (1) 根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程的状态;

    (2) 若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真,用于指示该进程被终止后应重新进程调度;

    (3) 若该进程还有子孙进程,还应将其所有子孙进程也都予以终止,以防它们成为不可控的进程;

    (4) 将被终止进程(PCB)从所在队列(或链表)中移除,等待其他程序来搜集信息。

3.4 进程的阻塞与唤醒

  1. 引起进程阻塞和唤醒的事件

    (1) 向系统请求共享资源失败。

    (2) 等待某种操作的完成。

    (3) 新数据尚未到达。

    (4) 等待新任务的到达。

  2. 进程阻塞过程

    正在执行的进程,如果发生了上述某事件,进程便通过调用阻塞原语block将自己阻塞。可见,阻塞是进程自身的一种主动行为。进入block过程后,由于该进程还处于执行状态,所以应先立即停止执行,把进程控制块中的现行状态由“执行”改为阻塞,并将PCB插入阻塞队列。如果系统中设置了因不同事件而阻塞的多个阻塞队列,则应将本进程插入到具有相同事件的阻塞队列。最后,转调度程序进行重新调度,将处理机分配给另一个就绪进程,并进行切换,亦即,保留被阻塞进程的处理机状态,按新进程的PCB中的处理机状态设置CPU环境。

  3. 进程唤醒过程

    当被阻塞进程所期待的事件发生时,比如它所启动的I/O操作已完成,或其所期待的数据已经到达,则由有关进程(比如提供数据的进程)调用唤醒原语wakeup,将等待该事件的进程唤醒。wakeup执行的过程:首先把被阻塞的进程从等待该事件的阻塞队列中移出,将其PCB中的现行状态由阻塞改为就绪,然后再将该PCB插入到就绪队列中。

3.5 进程的挂起与激活

  1. 进程的挂起

    当系统中出现了引起进程挂起的事件时,OS将利用挂起原语suspend将指定进程或处于阻塞状态的进程挂起。suspend的执行过程是:首先检查被挂起进程的状态,若处于活动就绪状态,便将其改为静止就绪;对于活动阻塞状态的进程,则将之改为静止阻塞;为了方便用户或父进程考查该进程的运行情况,而把该进程的PCB复制到某指定的内存区域;最后,若被挂起的进程正在执行,则转向调度程序重新调度。

  2. 进程的激活过程

    当系统中发生激活进程的事件时,OS将利用激活原语active,将指定进程激活。激活原语先将进程从外存调入内存,检查该进程的现行状态,若是静止就绪,便将之改为活动就绪;若为静止阻塞,便将之改为活动阻塞。假如采用的是抢占调度策略,则每当有静止就绪进程被激活而插入就绪队列时,便应检查是否要进行重新调度,即由调度程序将被激活的进程与当前进程两者的优先级进行比较,如果被激活进程的优先级低,就不必重新调度;否则,立即剥夺当前进程的运行,把处理机分配给刚刚被激活的进程。

4.进程同步

4.1 进程同步的基本概念

进程同步机制的主要任务,是对多个相关进程在执行次序上进行协调,使并发执行的诸进程之间能依照一定的规则(或时序)共享系统资源,并能很好的相互合作,从而使程序的执行具有可再现性。

  1. 两种形式的制约关系

    在多道程序环境下,对于同处于一个系统中的多个进程,由于它们共享系统中的资源,或为完成某一任务而相互合作,它们之间可能存在着以下两种形式的制约关系:

    (1) 间接相互制约关系

    (2) 直接相互制约关系

  2. 临界资源(Critical Resource)

    在第一章我们曾介绍过,许多硬件资源如打印机、磁带机等,都属于临界资源,诸进程间应采取互斥方式,实现对这种资源的共享。

  3. 临界区(Critical section)

    由前述可知,不论是硬件临界资源还是软件临界资源,多个进程必须互斥地对它进行访问。人们把在每个进程中访问临界资源的那段代码称为临界区。

  4. 同步机制应遵循的规则

    (1) 空闲让进。

    (2) 忙则等待。

    (3) 有限等待。

    (4) 让权等待。

4.2 硬件同步机制

  1. 关中断

    关中断是实现互斥的最简单的方法之一。在进入锁测试之前中断,直到完成锁测试并上锁之后才能打开中断。

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

    这是一种借助一条硬件指令——“测试并建立”指令TS(Test-and-Set)以实现互斥的方法。TS指令的一般性描述如下:

    boolean TS(boolean *lock) {
    	Boolean old;
    	old = *lock;
    	*lock = TRUE;
    	return old;
    }
    

    利用TS指令实现互斥的循环进程结构可描述如下:

    do {
    	...
    	while TS(&lock);
    	critical section;
    	lock = FALSE;
    	remainder section;
    }while(TRUE);
    
  3. 利用Swap指令实现进程互斥

    该指令称为对换指令,用于交换两个字的内容。其处理过程描述如下:

    void swap(boolean *a, boolean *b) {
    	boolean temp;
    	temp = *a;
    	*a = *b;
    	*b = temp;
    }
    

    用对换指令可以简单有效地实现互斥,方法是为每个临界资源设置一个全局的布尔变量lock,其初值为false,在每个进程中再利用一个局部布尔变量key。利用Swap指令实现进程互斥的循环进程可描述如下:

    do {
    	key = TRUE;
    	do {
    		swap(&lock, &key);
    	}while(key!=FALSE);
    	临界区操作;
    	lock=FALSE;
    	...
    }while(TRUE);
    

4.3 信号量机制

  1. 整型信号量

    最初由Dijkstra把整型信号量定义为一个用于表示资源数目的整型量S,它与一般整型量不同,除初始化外,仅能通过两个标准的原子操作wait(S)和signal(S)来访问。很长时间以来,这两个操作一直被分别称为P、V操作。wait和signal操作可描述如下:

    wait(S) {
    	while(S<=0);
    	S--;
    }
    signal(S) {
    	S++;
    }
    
  2. 记录型信号量

    在整型信号量机制中的wait操作,只要是信号量S≤0,就会不断地测试。因此,该机制并未遵循“让权等待”的准则,而是使进程处于“忙等”的状态。记录型信号量机制则是一种不存在“忙等”现象的进程同步机制。但在采取了“让权等待”的策略后,又会出现多个进程等待访问同一临界资源的情况。为此,在信号量机制中,除了需要一个用于代表资源数目的整型变量value外,还应增加一个进程链表指针list,用于链接上述的所有等待进程。记录型信号量是由于它采用了记录型的数据结构而得名的。它所包含的上述两个数据项可描述如下:

    typedef struct {
          
          
        int value;
        struct process_control_block *list;
    } semaphore;
    

    相应地,wait(S)和signal(S)操作可描述如下:

    wait(semaphore *S) {
          
          
        S->value--;
        if (S->value < 0) block(S->list);
    }
    signal(semaphore *S) {
          
          
        S->value++;
        if (S->value<=0) wakeup(S->list);
    }
    
  3. AND型信号量

    AND同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源也不分配给它。亦即,对若干个临界资源的分配采取原子操作方式:要么把它所请求的资源全部分配到进程,要么一个也不分配。由死锁理论可知,这样就可避免上述死锁情况的发生。为此,在wait操作中增加一个“AND”条件,故称为AND同步,或称为同时wait操作,即Swait(Simultaneous wait)定义如下:

    Swait(S1,S2,...,Sn) {
          
          
        while (TRUE) {
          
          
            if (Si>=1 && ... && Sn>=1) {
          
          
                for (i=1; i<=n; i++) Si--;
                break;
            } 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
            }
        }
    }
    Ssignal(S1,S2,...Sn) {
          
          
        while (TRUE) {
          
          
            for (i=1; i<=n; i++) {
          
          
                Si++;
                Remove all the process waiting in the queue associates with Si into the ready queue
            }
        }
    }
    
  4. 信号量集

    在前面所述的记录型信号量机制中,wait(S)或signal(S)操作仅能对信号量施以加以或减1操作,意味着每次只能对某类临界资源进行一个单位的申请或释放。当一次需要N个单位时,便要进行N次wait(S)操作,这显然是低效的,甚至会增加死锁的概率。此外,在有些情况下,为确保系统的安全性,当所申请的资源数量低于某一下限值时,还必须进行管制,不予以分配。因此,当进程申请某类临界资源时,在每次分配之前,都必须测试资源的数量,判断是否大于可分配的下限值,决定是否予以分配。

    基于上述两点,可以对AND信号量机制加以扩充,对进程所申请的所有资源以及每类资源不同的资源需求量,在一次P、V原语操作中完成申请或释放。进程对信号量Si的测试值不再是1,而是该资源的分配下限值ti,即要求Si≥ti,否则不予以分配。一旦允许分配,进程对该资源的需求值为di,即表示资源占用量,进行Si=Si-di操作,而不是简单的Si=Si-1。由此形成一般化的“信号量集”机制。对应的Swait和Ssignl格式为:

    Swait(S1,t1,d1,...,Sn,tn,dn);
    Ssignal(S1,d1,...,Sn,dn);
    

    一般“信号量集”还有下面几种特殊情况:

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

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

    (3) Swait(S,1,0)。这是一种很特殊且很有用的信号量操作。当S≥1时,允许多个进程进入某特定区;当S变为0后,将阻止任何进程进入特定区。换而言之,它相当于一个可控开关。

4.4 信号量的应用

  1. 利用信号量实现进程互斥

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

  2. 利用信号量实现前趋关系

    还可以用信号量来描述程序或语句之间的前趋关系。设有两个并发执行的进程P1和P2。P1中有语句S1;P2中有语句S2。我们希望在S1执行后再执行S2。为实现这种前趋关系,只需使进程P1和P2共享一个公用信号量S,并赋予初值为0,将signal(S)操作放在语句S1后面,而在S2语句前面插入wait(S)操作,即

    在进程P1中,用S1;signal(S);

    在进程P2中,用wait(S);S2

4.5 管程机制

虽然信号量机制是一种既方便、又有效的进程同步机制,但每个要访问临界资源的进程都必须自备同步操作wait(S)和signal(S)。这就使大量的同步操作分散在各个进程中。这不仅给系统的管理带来了麻烦,而且还会因同步操作的使用不当而导致系统死锁。这样,在解决上述问题的过程中,便产生了一种新的进程同步工具——管程(Monitors)。

  1. 管程的定义

    系统中的各种硬件资源和软件资源均可用数据结构抽象地描述其资源特性,即用少量信息和对该资源所执行的操作来表征该资源,而忽略它们内部结构和实现细节。因此,可以利用共享数据结构抽象地表示系统中的共享资源,并且将对该共享数据结构实施的特定操作定义为一组过程。进程对共享资源的申请、释放和其它操作必须通过这组过程,简接地对共享数据结构实现操作。对于请求访问共享资源的诸多并发进程,可以根据资源的情况接收或阻塞,确保每次仅有一个进程进入管程,执行这组过程,使用共享资源,达到对共享资源所有访问的统一管理,有效地实现进程互斥。

    代表共享资源的数据结构以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序共同构成了一个操作系统的资源管理模块,我们称之为管程。管程被请求和释放资源的进程所调用,如下图。

    在这里插入图片描述

    管程是一种程序设计语言的结构成分,它和信号量有同等的表达能力,从语言的角度看,管程主要有以下特性:①模块化,即管程是一个基本程序单位,可以单独编译;②抽象数据类型,指管程中不仅有数据,而且有对数据的操作;③信息掩蔽,指管程中的数据结构只能被管程中的过程访问,这些过程也是在管程内定义的,供管程外的进程调用,而管程中的数据结构以及过程(函数)的具体实现外部不可见。

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

  2. 条件变量

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

    但是仅仅有上述的同步工具是不够的,考虑一种情况:当一个进程调用了管程,在管程中时被阻塞或挂起,直到阻塞或挂起的原因解除,而在此期间,如果该进程不释放管程,则其它进程无法进入管程,被迫长时间等待。为了解决这个问题,引入了条件变量condition。通常,一个进程被阻塞或挂起的条件(原因)可有多个,因此在管程中设置了多个条件变量,对这些条件变量的访问只能在管程中进行。

    管程中对每个条件变量都须予以说明。其形式为:condition x, y;对条件变量的操作仅仅是wait和signal,因此条件变量也是一种抽象数据类型,每个条件变量保存了一个链表,用于记录因该条件变量而阻塞的所有进程,同时提供的两个操作即可表示为x.wait和x.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,如何确定哪个执行哪个等待,可采用下述两种方式之一进行处理:

    (1) P等待,直至Q离开管程或等待另一条件。

    (2) Q等待,直到P离开管程或等待另一条件。

5.经典进程的同步问题

5.1 生产者–消费者问题

  1. 利用记录型信号量解决生产者-消费者问题

    假定在生产者和消费者之间的公用缓冲池中具有n个缓冲区,这时可利用互斥信号量metex实现诸进程对缓冲池的互斥使用;利用信号量empty和full分别表示缓冲池中空缓冲区和满缓冲区的数量。又假定这些生产者和消费者相互等效,只要缓冲池未满,生产者便可将消息送入缓冲池;只要缓冲池为空,消费者便可从缓冲池中取走一个消息。对生产者-消费者问题可描述如下:

    int in=0,out=0;
    item buffer[n];
    semaphore mutex=1,empty=n,full=0;
    void proceducer() {
          
          
        do {
          
          
            producer an item nextp;
            ...
            wait(empty);
            wait(mutex);
            buffer[in]=nextp;
            in=(in+1)%n;
            signal(mutex);
            signal(full);
        } while(TRUE);
    }
    void consumer() {
          
          
        do {
          
          
            wait(full);
            wait(mutex);
            nextp=buffer[out];
            out=(out+1)%n;
            signal(mutex);
            signal(empty);
            comsumer the item in nextc;
            ...
        }while(TRUE);
    }
    void main() {
          
          
        cobegin
            proceducer(); consumer();
        coend
    }
    
  2. 利用AND信号量解决生产者-消费者问题

    利用AND信号量来解决生产者-消费者问题的算法中的生产者和消费者可描述如下:

    int in=0,out=0;
    item buffer[n];
    semaphore mutex=1,empty=n,full=0;
    void proceducer() {
          
          
        do {
          
          
            producer an item nextp;
            ...
            Swait(empty, mutex);
            buffer[in]=nextp;
            in=(in+1)%n;
            Ssignal(mutex, full);
        }while(TRUE);
    }
    void consumer() {
          
          
        do {
          
          
            Swait(full, mutex);
            nextp=buffer[out];
            out=(out+1)%n;
            Ssignal(mutex, empty);
            consumer the item in nextc;
            ...
        }while(TRUE);
    }
    
  3. 利用管程解决生产者-消费者问题

    在利用管程方法来解决生产者-消费者问题时,首先便是为它们建立一个管程,并命名为producerconsumer,简称为PC。其中包括两个过程:

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

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

    对于条件变量notfull和notempty,分别有两个过程cwait和csignal对它们进行操作:

    (1) cwait(condition)过程:当管程被一个进程占用时,其他进程调用该过程时阻塞,并挂在条件condition的队列上。

    (2) csignal(condition)过程:唤醒在cwait执行后阻塞在条件condition队列上的进程,如果这样的进程不止一个,则选择其中一个实施唤醒操作;如果队列为空,则无操作而返回。

    PC管程可描述如下:

    Monitor producerconsumer {
          
          
        item buffer[N];
        int in,out;
        condition notfull,notempty;
        int count;
        public:
        void put(item x) {
          
          
            if (count>=N) cwait(notfull);
            buffer[in]=x;
            in=(in+1)%N;
            count++;
            csignal(notempty);
        }
        void get(item x) {
          
          
            if (count<=0) cwait(notempty);
            x=buffer[out];
            out=(out+1)%N;
            count--;
            csignal(notfull);
        }
        {
          
           in=0;out=0;count=0; }
    }PC;
    

    在利用管程解决生产者-消费者问题时,其中的生产者和消费者可描述为:

    void proceducer() {
          
          
        item x;
        while(TRUE) {
          
          
            ...
            produce an item in nextp;
            PC.put(x);
        }
    }
    void consumer() {
          
          
        item x;
        while(TRUE) {
          
          
            PC.get(x);
            consume the item in nextc;
            ...
        }
    }
    void main() {
          
          
        cobegin
        proceducer(); consumer();
        coend
    }
    

5.2 哲学家进餐问题

由Disjkstra提出并解决的哲学家进餐问题是典型的同步问题。该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐毕,放下筷子继续思考。

  1. 利用记录型信号量解决哲学家进餐问题

    经分析可知,放在桌子上的筷子是临界资源,在一段时间只允许一位哲学家使用。为了实现对筷子的互斥使用,可以用一个信号量表示一只筷子,由这五个信号量构成信号量数组。其描述如下:

    semaphore chopstick[5] = {1,1,1,1,1};

    所有信号量均被初始化为1,第i为哲学家的活动可描述为:

    do {
          
          
        wait(chopstick[i]);
        wait(chopstick[(i+1)%5]);
        ...
        //eat
        ...
        signal(chopstick[i]);
        signal(chopstick[(i+1)%5]);
        ...
        //think
        ...
    }while(TRUE);
    

    假如五位哲学家同时饥饿而各自拿起左边的筷子时,就会使五个信号量chopstick均为0;当他们再试图去拿右边的筷子时,都将因无筷子可拿而无限期地等待。对于这样的死锁问题,可采取以下几种解决办法:

    (1) 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。

    (2) 仅当哲学家的左/右两只筷子均可用时,才允许他拿起筷子进餐。

    (3) 规定奇数号哲学家先拿起它左边的筷子,然后再去拿右边的筷子;而偶数号哲学家则相反。

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

    在哲学家进餐问题中,要求每个哲学家先获得两个临界资源(筷子)后方能进餐。

    semaphore chopstick chopstick[5]={
          
          1,1,1,1,1};
    do {
          
          
        ...
        //think
        ...
        Sswait(chopstick[(i+1)%5], chopstick[i]);
        ...
        //eat
        ...
        Ssignal(chopstick[(i+1)%5], chopstick[i]);
    }while(TRUE);
    

5.3 读者-写者问题

一个数据文件或记录可被多个进程共享,我们把只要求读该文件的进程称为“Reader进程”,其他进程则称为“Writer进程”。允许多个进程同时读一个共享对象,因为读操作不会使数据文件混乱。但不允许一个Writer进程和其他Reader进程或Writer进程同时访问共享对象,因为这种访问将会引起混乱。所谓“读者-写者问题”是指保证一个Writer进程必须与其他进程互斥地访问共享对象的同步问题。读者-写者问题常被用来测试新同步原语。

  1. 利用记录型信号量解决读者-写者问题

    为实现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。

    读者-写者问题可描述如下:

    semaphore rmutex=1, wmutex=1;
    int readcount=0;
    void reader() {
          
          
        do {
          
          
            wait(rmutex);
            if (readcount==0) wait(wmutex);
            readcount++;
            signal(rmutex);
            ...
            perform read operation;
            ...
            wait(rmutex);
            readcount--;
            if (readcount==0) signal(wmutex);
            signal(rmutex);
        }while(TRUE);
    }
    void writer() {
          
          
        do {
          
          
            wait(wmutex);
            perform write operation;
            signal(wmutex);
        }while(TRUE);
    }
    void main() {
          
          
        cobegin
            read(); writer();
        coend
    }
    
  2. 利用信号量集机制解决读者-写者问题

    这里的读者-写者问题,与前面的略有不同,它增加了一个限制,即最多只允许RN个读者同时读。为此,又引入了一个信号量L,并赋予其初值为RN,通过执行wait(L,1,1)操作来控制读者的数目,每当有一个读者进入时,就要先执行wait(L,1,1)操作,使L的值减1。当有RN个读者进入读后,L便减为0,第RN+1个读者要进入读时,必然会因wait(L,1,1)操作失败而阻塞。对利用信号量集来解决读者-写者问题的描述如下:

    int RN;
    semaphore L=RN,mx=1;
    void reader() {
          
          
        do {
          
          
            Swait(L,1,1);
            Swait(mx,1,0);  // 开关作用
            ...
            perform read operation;
            ...
            Ssignal(L,1);
        }while(TRUE);
    }
    void writer() {
          
          
        do {
          
          
            Swait(mx,1,1;L,RN,0);  // 仅当既无writer进程在写操作、又无reader进程在读操作时,writer进程才能进入临界区进行写操作
            perform write operation;
            Ssignal(mx,1);
        }while(TRUE);
    }
    void main() {
          
          
        cobegin
            reader(); writer();
        coend
    }
    

6.进程通信

进程通信是指进程之间的信息交换。由于进程的互斥与同步,需要在进程间交换一定的信息,故不少学者将它们也归为进程通信,但只能把它们称为低级进程通信。我们以信号量机制为例来说明,它们之所以低级的原因在于:①效率低,生产者每次只能向缓冲池投放一个产品(消息),消费者每次只能从缓冲池中取得一个消息;②通信对用户不透明,OS只为进程之间的通信提供了共享存储器,而关于进程之间通信所需的共享数据结构的设置、数据的传送、进程的互斥与同步,都必须由程序员去实现,显然,对于用户而言,这是非常不方便的。

在进程之间要传送大量数据时,应当利用OS提供的高级通信工具,该工具最主要的特点是:

(1) 使用方便。OS隐藏了实现进程通信的具体细节,向用户提供了一组用于实现高级通信的命令(原语),用户可方便地直接利用它实现进程之间的通信。或者说,通信过程对用户是透明的。这样就大大减少了通信程序编制上的复杂性。

(2) 高效地传送大量数据。用户可直接利用高级通信命令(原语)高效地传送大量的数据。

6.1 进程通信的类型

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

    在共享存储器系统中,相互通信的进程共享某些数据结构或共享存储区,进程之间能够通过这些空间进行通信。据此,又可把它们分成以下两种类型:

    (1) 基于共享数据结构的通信方式。

    (2) 基于共享存储区的通信方式。

  2. 管道(pipe)通信系统

    所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。

  3. 消息传递系统(Message passing system)

    在该机制中,进程不必借助任何共享存储区或数据结构,而是以格式化的消息(message)为单位,将通信的数据封装在消息中,并利用操作系统提供的一组通信命令(原语),在进程间进行消息传递,完成进程间的数据交换。

    基于消息传递系统的通信方式属于高级通信方式,因其实现方式的不同,可进一步分成两类:

    (1) 直接通信方式,是指发送进程利用OS所提供的发送原语,直接把消息发送给目标进程;

    (2) 间接通信方式,是指发送和接收进程,都通过共享中间实体(称为邮箱)的方式进行消息的发送和接收,完成进程间的通信。

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

    前面所述的共享内存、消息传递等技术,虽然也可以用于实现不同计算机间进程的双向通信,但客户机-服务器系统的通信机制,在网络环境的各种应用领域已成为当前主流的通信实现机制,其主要的实现方式分为三类:套接字、远程过程调用和远程方法调用。

    (1) 套接字(Socket)

    一个套接字就是一个通信标识类型的数据结构,包含了通信目的的地址、通信使用的端口号、通信网络的传输层协议、进程所在的网络地址,以及针对客户或服务器提供的不同系统调用(或API函数)等,是进程通信和网络通信的基本构件。套接字是为客户/服务器模型而设计的,通常,套接字包括两类:

    ①基于文件型:通信进程都运行在同一台机器的环境中,套接字是基于本地文件系统支持的,一个套接字关联到一个特殊的文件,通信双方通过对这个特殊文件的读写实现通信,其原理类似于前面所讲的管道。

    ②基于网络型:该类型通常采用的是非对称方式通信,即发送者需要提供接收者命名。通信双方的进程运行在不同主机的网络环境下,被分配了一对套接字,一个属于接收进程(或服务器端),一个属于发送进程(或客户端)。一般的,发送进程发出连接请求时,随机申请一个套接字,主机为之分配一个端口,与该套接字绑定,不再分配给其他进程。接收进程拥有全局公认的套接字和指定端口(如ftp服务器监听端口为21,Web或http服务器监听端口为80),并通过监听端口等待客户请求。因此,任何进程都可以向他发出连接请求和信息请求,以方便进程间通信连接的建立。接收进程一旦收到请求,就接受来自发送进程的连接,完成连接,即在主机间传输的数据可以准确地发送到通信进程,实现进程间的通信;当通信结束,系统通过关闭接收进程的套接字撤销连接。

    (2) 远程过程调用和远程方法调用

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

    负责处理远程过程调用的进程由两个,一个是本地客户进程,另一个是远程服务器进程,这两个进程通常也被称为网络守护进程,主要负责在网络间的消息传递,一般情况下,这两个进程都是处于阻塞状态,等待消息。

    为了使远程过程调用看上去与本地过程调用一样,即希望实现RPC的透明性,使得调用者感觉不到此次调用的过程是在其他主机(远程)上执行的,RPC引入一个存根(stub)的概念:在本地客户端,每个能够独立运行的远程过程都拥有一个客户存根(client stubborn),本地进程调用远程过程实际是调用该过程关联的存根;与此类似,在每个远程进程所在的服务器端,其所对应的实际可执行进程也存在一个服务器存根(stub)与其关联。本地客户存根与对应的远程服务器存根一般也是处于阻塞状态,等待消息。

6.2 消息传递通信的实现方式

在进程之间通信时,源进程可以直接或间接地将消息传送给目标进程,因此可将进程通信分为直接和间接通信方式。常见的直接消息传递系统和信箱通信就是分别采用这两种通信方式。

  1. 直接消息传递系统

    在直接消息传递系统中采用直接通信方式,即发送进程利用OS所提供的发送命令(原语),直接把消息发送给目标进程。

  2. 信箱通信

    信箱通信属于间接通信方式,即进程之间的通信需要通过某种中间实体(如共享数据结构等)来完成。该实体建立在随机存储器的公用缓冲区上,用来暂存发送进程发送给目标进程的消息;接收进程可以从该实体中取出发送给自己的消息,通常把这种中间实体称为邮箱(或信箱),每个邮箱都有一个唯一的标识符。消息在邮箱中可以安全地保存,只允许核准的目标用户随时读取。因此,利用邮箱通信方式既可以实现实时通信,又可实现非实时通信。

6.3 直接消息传递系统实例

  1. 消息缓冲队列通信机制中的数据结构

    (1) 消息缓冲区。在消息缓冲队列通信方式中,主要利用的数据结构是消息缓冲区。它可描述如下:

    typedef struct message_buffer {
          
          
        int sender;   // 发送者进程标识符
        int size;     // 消息长度
        char *text;   // 消息正文
        struct message_buffer *next;  // 指向下一个消息缓冲区的指针
    }
    

    (2) PCB中有关通信的数据项。在操作系统中采用了消息缓冲队列通信机制时,除了需要为进程设置消息缓冲队列外,还应在进程的PCB中增加消息队列队首指针,用于对消息队列进行操作,以及用于实现同步的互斥信号量mutex和资源信号量sm。在PCB中应增加的数据项可描述如下:

    typedef struct processcontrol_block {
          
          
        ...
        struct message_buffer *mq;  // 消息队列队首指针
        semaphore mutex;            // 消息队列互斥信号量
        semaphore sm;               // 消息队列资源信号量
        ...
    }PCB;
    
  2. 发送原语

    发送进程在利用发送原语发送消息之前,应先在自己的内存空间设置一发送区a,如下图,把待发送的消息正文、发送进程标识符、消息长度等信息填入其中,然后调用发送原语,把消息发送给目标(接收)进程。发送原语首先根据发送区a中所设置的消息长度a.size来申请一缓冲区i,接着,把发送区a中的信息复制到缓冲区i中。为了能将i挂在接收进程的消息队列mq上,应先获得接收进程的内部标识符j,然后将i挂在j.mq上。由于该队列属于临界资源,故在执行insert操作的前后都要执行wait和signal操作。

    在这里插入图片描述

    发送原语可描述如下:

    void send(receiver, a) {
          
             // receiver为接收进程标识符,a为发送区首址
        getbuf(a.size, i);  // 根据a.size申请缓冲区
        copy(i.sender, a.sender);  // 将发送区a中的信息复制到消息缓冲区i中
        i.size=a.size;
        copy(i.text, a.text);
        i.next=0;
        getid(PCBset, receiver.j);  // 获得接收进程内部的标识符
        wait(j.mutex);
        insert(&j.mq, i);  // 将消息缓冲区插入消息队列
        signal(j.mutex);
        signal(j.sm);
    }
    
  3. 接收原语

    接收进程调用接收原语receive(b),从自己的消息缓冲队列mq中摘下第一个消息缓冲区i,并将其中的数据复制到以b为首址的指定消息接收区。接收原语描述如下:

    void receive(b) {
          
          
        j=internal name;  // j为接收进程内部的标识符
        wait(j.sm);
        wait(j.mutex);
        remove(j.mq, i);  // 将消息队列中第一个消息移出
        signal(j.mutex);
        copy(b.sender, i.sender);  // 将消息缓冲区i中的信息复制到接收区b
        b.size=i.size;
        copy(b.text, i.text);
        releasebuf(i);  // 释放消息缓冲区
    }
    

7.线程的基本概念

7.1 线程的引入

在OS中引入进程的目的是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量,而在OS中再引入线程,则是为了减少程序在并发执行时所付出的时空开销,使OS具有更好的并发性。

由于进程是一个资源的拥有者,因而在创建、撤销和切换中,系统必须为之付出较大的时空开销。要设法将进程的上述两个属性分开,由OS分开处理,亦即并不把作为调度和分派的基本单位也同时作为拥有资源的单位,而对于拥有资源的基本单位,又不对之施以频繁的切换。正是在这种思想的指导下,形成了线程的概念。

7.2 线程与进程的比较

  1. 调度的基本单位
  2. 并发性
  3. 拥有资源
  4. 独立性
  5. 系统开销
  6. 支持多处理机系统

7.3 线程的状态和线程控制块

  1. 线程运行的三个状态

    (1) 执行状态

    (2) 就绪状态

    (3) 阻塞状态

  2. 线程控制块TCB

    如同每个进程有一个进程控制块一样,系统也为每个线程配置了一个线程控制块TCB,将所有用于控制和管理线程的信息记录在线程控制块中。线程控制块通常有这样几项:①线程标识符,为每个线程赋予一个唯一的线程标识符;②一组寄存器,包括程序计数器PC、状态寄存器和通用寄存器的内容;③线程运行状态,用于描述线程正处于何种运行状态;④优先级,描述该线程执行的优先程度;⑤线程专有存储区,用于线程切换时存放现场保护信息,和与该线程相关的统计信息等;⑥信息屏蔽,即对某些信号加以屏蔽;⑦堆栈指针,在线程运行时,经常会进行过程调用,而过程的调用通常会出现多重嵌套的情况,这样,就必须将每次过程调用中所使用的局部变量以及返回地址保存起来。为此,应为每个线程设置一个堆栈,用它来保存局部变量和返回地址。相应地,在TCB中,也须设置两个指向堆栈的指针:指向用户自己堆栈的指针和指向核心堆栈的指针。前者是指当线程运行在用户态时,使用用户自己的用户栈来保存局部变量和返回地址,后者是指当线程运行在核心态时使用系统的核心栈。

  3. 多线程OS中的进程属性

    (1) 进程是一个可拥有资源的基本单位。

    (2) 多个线程可并发执行。

    (3) 进程已不是可执行的实体。

8.线程的实现

8.1 线程的实现方式

  1. 内核支持线程KST(Kernel Supported Threads)

    在OS中的所有进程,无论是系统进程还是用户进程,都是在操作系统内核的支持下运行的,是与内核紧密相关的。而内核支持线程KST同样也是在内核的支持下运行的,它们的创建、阻塞、撤销和切换等,也都是在内核空间实现的。为了对内核线程进行控制和管理,在内核空间也为每一个内核线程设置了一个线程控制块,内核根据该控制块而感知某线程的存在,并对其加以控制。当前大多数OS都支持内核支持线程。

  2. 用户级线程ULT(User Level Threads)

    用户级线程是在用户空间中实现的。对线程的创建、撤销、同步与通信等功能,都无需内核的支持,即用户级线程是与内核无关的。在一个系统中的用户级线程的数目可以达到数百至数千个。由于这些线程的任务控制块都是设置在用户空间,而线程所执行的操作也无需内核的帮助,因而内核完全不知道用户级线程的存在。

  3. 组合方式

    有些OS把用户级线程和内核支持线程两种方式进行组合,提供了组合方式ULT/KST线程。在组合方式线程系统中,内核支持多个内核支持线程的建立、调度和管理,同时,也允许用户应用程序建立、调度和管理用户级线程。一些内核支持线程对应多个用户级线程,这是用户级线程通过时分多路复用内核支持线程来实现的。即将用户级线程对部分或全部内核支持线程进行多路复用,程序员可按应用需要和机器配置,对内核支持线程数目进行调整,以达到较好效果。组合方式线程中,同一个进程内的多个线程可以同时在多处理机上并行执行,而且在阻塞一个线程时并不需要将整个进程阻塞。所以,组合方式多线程机制能够结合KST和ULT两者的优点,并克服了其各自的不足。由于用户级线程和内核支持线程连接方式的不同,从而形成了三种不同的模型:多对一模型、一对一模型和多对多模型:

    (1) 多对一模型,即将用户线程映射到一个内核控制线程,如下图。

    在这里插入图片描述

    (2) 一对一模型,即将每一个用户级线程映射到一个内核支持线程,如下图。

    在这里插入图片描述

    (3) 多对多模型,即将许多用户级线程映射到同样数量或更少数量的内核线程上,如下图。

    在这里插入图片描述

8.2 线程的实现

  1. 内核支持线程的实现

    在仅设置了内核支持线程的OS中,一种可能的线程控制方法是,系统在创建一个新进程时,便为它分配一个任务数据区PTDA(Per Task Data Area),其中包括若干个线程控制块TCB空间,如下图。在每个TCB中可保存线程标识符、优先级、线程运行的CPU状态等信息。虽然这些信息与用户级线程TCB中的信息相同,但现在却是被保存在内核空间中。

    在这里插入图片描述

  2. 用户级线程的实现

    用户级线程是在用户空间实现的。所有的用户级线程都具有相同的结构,它们都运行在一个中间系统上。当前有两种方式实现中间系统,即运行时系统和内核控制线程。

    (1) 运行时系统(Runtime System)

    所谓“运行时系统”实质上是用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤销线程的函数、线程同步和通信的函数,以及实现线程调度的函数等。正因为有这些函数,才能使用户级线程与内核无关。运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。

    (2) 内核控制线程

    这种线程又称为轻型进程LWP(Light Weight Process)。每一个进程都可拥有多个LWP,同用户级线程一样,每个LWP都有自己的数据结构(如TCB),其中包括线程标识符、优先级、状态,另外还有栈和局部存储区等。LWP也可以共享进程所拥有的资源。LWP可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只须将它连接到一个LWP上,此时,它便具有了内核支持线程的所有属性。这种线程实现方式就是组合方式。

    在一个系统中的用户级线程数量可能很大,为了节省系统开销,不可能设置太多的LWP,而是把这些LWP做成一个缓冲池,称为“线程池”。用户进程中的任一用户线程都可以连接到LWP池中的任何一个LWP上。为使每一用户级线程都能利用LWP与内核通信,可以使多个用户级线程多路复用一个LWP,但只有当前连接到LWP上的线程才能与内核通信,其余进程或者阻塞,或者等待LWP。而每一个LWP都要连接到一个内核级线程上,这样,通过LWP可把用户级线程与内核线程连接起来,用户级线程可通过LWP来访问内核,但内核所看到的总是多个LWP而看不到用户级线程,如下图。

    在这里插入图片描述

8.3 线程的创建和终止

  1. 线程的创建

    应用程序在启动时,通常仅有一个线程在执行,人们把线程称为“初始化线程”,它的主要功能是用于创建新线程。在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。在线程的创建函数执行完后,将返回一个线程标识符以后使用。

  2. 线程的终止

    当一个线程完成了自己的任务(工作)后,或是线程在运行中出现异常情况而须被强行终止时,由终止线程通过调用相应的函数(或系统调用)对它执行终止操作。但有些线程(主要是系统线程),它们一旦被建立起来之后,便一直运行下去而不被终止。在大多数的OS中,线程被中止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了分离函数后,被终止的线程才与资源分离,此时的资源才能被其他线程利用。

猜你喜欢

转载自blog.csdn.net/m0_50833438/article/details/114889749