操作系统(2)进程的描述与控制

  一、进程的描述

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

  在引入了程序间的并发执行功能后,虽然提高了系统的吞吐量和资源利用率,但由于它们共享系统资源,以及它们为完成同一项任务而相互合作,致使在这些并发执行的程序之间必将形成相互制约的关系,由此会给程序并发执行带来新的特征。

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

  (2)失去封闭性。当系统中存在着多个可以并发执行的程序时,系统中的各种资源将为它们所共享,而这些资源的状态也由这些程序来改变,致使其中任一程序在运行时,其环境都必然会受到其他程序的影响。程序的运行已失去了封闭性。

  (3)不可再现性。程序在并发执行时,由于失去了封闭性,也将导致其又失去可再现性。即程序经过多次执行后,虽然它们执行时的环境和初始条件相同,但得到的结果却各不相同。

 

  2.进程的定义

  为了能使程序并发执行,并且可以对并发执行的程序加以描述和控制,引入了“进程”的概念。

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

  进程的定义是:

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

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

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

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

  

  3.进程的特征

  (1)动态性。进程的实质是进程实体的执行过程,因此,动态性就是进程的最基本的特征。动态性还表现在:“它由创建而产生,由调度而执行,由撤销而消亡。”

  (2)并发性。多个进程实体同时存在于内存,且能在一段时间内同时运行。并发性是进程的另一个重要特征。

  (3)独立性。进程实体是一个能独立运行、独立获得资源和独立接收调度的基本单位。

  (4)异步性。进程是按异步方式运行的,即按各自独立的、不可预知的速度向前推进。

 

  4.进程的三种基本状态

(1)就绪(Ready)状态。指进程已处于准备好运行的状态,即进程已分配到除 CPU 以外的所有必要资源后,主要再获得 CPU 资源,便可立即执行。如果系统中有许多处于就绪状态的进程,通常将它们按一定的策略(如优先级策略)排成一个队列,称该队列为就绪队列。

(2)执行(Running)状态。指进程已获得 CPU 资源,其程序正在执行的状态。对任何一个时刻而言,在单处理机系统中,只有一个进程处于状态,而在多处理机系统中,则有多个进程处于执行状态。

(3)阻塞(Block)状态。指正在执行的进程由于发生某事件(如 I/O 请求、申请缓冲区失败等)暂时无法继续执行时的状态,即进程的执行收到阻塞。此时引起进程调度, OS 会把处理机分配给另一个就绪进程,而让受阻进程处于暂停状态,一般将这种状态称为阻塞状态,也称为等待状态或封锁状态。通常系统将处于阻塞状态的进程也排成一个队列,称该队列为阻塞队列。

 

  5.三种基本状态的转换

  (1)处于就绪状态的进程,在调度程序为之分配了处理机之后便可执行,相应地,其状态就由就绪状态变为执行状态;

  (2)正在执行的进程(当前进程)如果因分配给它的时间片已完而被剥夺处理机暂停执行时,其状态便由执行转为就绪

  (3)如果因发生某事件,致使当前进程的执行受阻(例如进程访问某临界资源,而该资源整备其他进程访问时),使之无法继续执行,则该进程状态将由执行转为阻塞。

 

  6.创建状态和终止状态

  为了满足进程控制块对数据以及操作的完整性要求以及增强管理的灵活性,通常在系统中又为进程引入了两种常见的状态:创建状态和终止状态。

  (1)创建状态

    创建一个进程的过程:

  • 首先由进程申请一个空白PCB,并向 PCB 中填写用于控制和管理进程的信息;
  • 然后为该进程分配运行时所必须的资源
  • 把该进程转入就绪状态并插入就绪队列之中。

    如果进程所需的资源尚不能得到满足,必须系统尚无足够的内存使进程无法装入其中,此时创建工作尚未完成,进程不能被调度运行,此时就称为创建状态。

  引入创建状态,是为了保证进程的调度必须在创建工作完成后进行,以确保对进程控制块操作的完整性。同时,创建状态的引入,也增加了管理的灵活性,操作系统可以根据系统性能或主存容量的限制,推迟创建状态进程的提交。对于处于创建状态的进程,获得了其所必需的资源,以及对其PCB初始化工作完成后,进程状态便可由创建状态转入就绪状态。

  (2)终止状态

  终止一个进程的过程:

  • 等待操作系统进行善后处理
  • 将其 PCB 清零,并将 PCB 空间返还系统。

  当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结,它将进入终止状态。进入终止态的进程以后不能再执行,但在操作系统中依然保留一个记录,其中保存状态码和一些计时统计数据,供其它进程收集。一旦其它进程完成了对终止状态进程的信息提取之后,操作系统将删除该进程。

 

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

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

  (1)挂起操作的引入原因

  • 终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时,希望暂时使自己的程序静止下来。亦即,使正在执行的进程暂停执行;若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。我们把这种静止状态称为挂起状态。
  • 父进程请求。有时父进程希望挂起自己的某个子进程,以便考查和修改该子进程,或者协调各子进程间的活动。
  • 负荷调节的需要。当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
  • 操作系统的需要。操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或进行记账。

  (2)引入挂起原语 Suspend 和激活原语 Active 后三个进程状态的转换

  • 活动就绪→静止就绪。当进程处于未被挂起的就绪状态时,称此为活动就绪状态,表示为 Readya。当用挂起原语 Suspend 将该进程挂起后,该进程便转变为静止就绪状态,表示为 Readys,处于 Readys 状态的进程不再被调度执行。
  • 活动阻塞→静止阻塞。当进程处于未被挂起的阻塞状态时,称它是处于活动阻塞状态,表示为 Blockeda。当用 Suspend 原语将它挂起后,进程便转变为静止阻塞状态,表示为Blockeds。处于该状态的进程在其所期待的事件出现后,将从静止阻塞变为静止就绪。
  • 静止就绪→活动就绪。处于 Readys 状态的进程,若用激活原语 Active 激活后,该进程将转变为 Readya 状态。
  • 静止阻塞→活动阻塞。处于 Blockeds 状态的进程,若用激活原语 Active 激活后,该进程将转变为 Blockeda 状态。

    

  (3)引入挂起原语 Suspend 和激活原语 Active 后五个进程状态的转换

  • NULL→创建:一个新进程产生时,该进程处于创建状态。
  • 创建→活动就绪:在当前系统的性能和内存的容量均允许的情况下,完成对进程创建的必要操作后,相应的系统进程将进程的状态转换为活动就绪状态。
  • 创建→静止就绪:考虑到系统当前资源状况和性能要求,并不分配给新建进程所需资源,主要是主存资源,相应的系统进程将进程状态转为静止就绪状态,对换到外存,不再参与调度,此时进程创建工作尚未完成。
  • 执行→终止:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结,进程即进终止状态。
  •  

  8.进程管理中的数据结构

  (1)操作系统中用于管理控制的数据接口

    在操作系统中,对于每个系统和每个进程都设置了一个数据结构,用于表征其实体,称之为资源信息表或进程信息表。

    OS 管理的这些数据结构一般分为四类:内存表、设备表、文件表和用于进程管理的进程表。其中,进程表又被称为进程控制块 PCB。

  (2)进程控制块 PCB 的作用。

  进程控制块的作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位,一个能与其它进程并发执行的进程。或者说,OS 是根据 PCB 来对并发执行的进程进行控制和管理的。

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

  (3)进程控制块 PCB 中的信息

  • 进程标识符。进程标识符用于惟一地标识一个进程。一个进程通常有两种标识符:①内部标识符。在所有的操作系统中,都为每一个进程赋予了一个惟一的数字标识符,它通常是一个进程的序号。设置内部标识符主要是为了方便系统使用。②外部标识符。它由创建者提供,通常是由字母、数字组成,往往是由用户(进程)在访问该进程时使用。为了描述进程的家族关系,还应设置父进程标识及子进程标识。此外,还可设置用户标识,以指示拥有该进程的用户。
  • 处理机状态。处理机状态信息主要是由处理机的各种寄存器中的内容组成的。处理机在运行时,许多信息都放在寄存器中。当处理机被中断时,所有这些信息都必须保存在 PCB 中,以便在该进程重新执行时,能从断点继续执行。这些寄存器包括:① 通用寄存器,又称为用户可视寄存器,它们是用户程序可以访问的,用于暂存信息,在大多数处理机中,有 8~32 个通用寄存器,在 RISC 结构的计算机中可超过 100 个;② 指令计数器,其中存放了要访问的下一条指令的地址;③ 程序状态字 PSW,其中含有状态信息,如条件码、执行方式、中断屏蔽标志等;④ 用户栈指针,指每个用户进程都有一个或若干个与之相关的系统栈,用于存放过程和系统调用参数及调用地址,栈指针指向该栈的栈顶。
  • 进程调度信息。在 PCB 中还存放一些与进程调度和进程对换有关的信息,包括:① 进程状态,指明进程的当前状态,作为进程调度和对换时的依据;② 进程优先级,用于描述进程使用处理机的优先级别的一个整数,优先级高的进程应优先获得处理机;③ 进程调度所需的其它信息,它们与所采用的进程调度算法有关,比如,进程已等待 CPU 的时间总和、进程已执行的时间总和等;④ 事件,指进程由执行状态转变为阻塞状态所等待发生的事件,即阻塞原因。
  • 进程控制信息。进程控制信息包括:① 程序和数据的地址,指进程的程序和数据所在的内存或外存地(首)址,以便再调度到该进程执行时,能从 PCB 中找到其程序和数据;② 进程同步和通信机制,指实现进程同步和进程通信时必需的机制,如消息队列指针、信号量等,它们可能全部或部分地放在 PCB 中;③ 资源清单,即一张列出了除 CPU 以外的、进程所需的全部资源及已经分配到该进程的资源的清单;④ 链接指针,它给出了本进程(PCB)所在队列中的下一个进程的 PCB 的首地址。

  (4)进程控制块的组织方式

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

  • 线性方式。即将系统中所有的 PCB 都组织在一张线性表中,将该表的首地址存放在内存的一个专用区域中。该方式实现简单、开销小,但每次查找时都需要扫描整张表,因此适合进程数据不多的系统。
  • 链接方式。即把具有相同状态进程的 PCB 分别通过 PCB 中的链接字链接成一个队列。这样,可以形成就绪队列、若干个阻塞队列和空白队列等。对就绪队列来说,往往按进程的优先级将 PCB 从高到低进行排列,将优先级高的进程 PCB 排在队列的前面。同样,也可以把处于阻塞状态进程的 PCB 根据其阻塞原因的不同,排成多个队列,如等待 I/O 操作完成的队列和等待分配内存的队列等。
  • 索引方式。

 

  二、进程的控制

  进程控制是进程管理中最基本的功能,主要包括创建新进程、终止已完成的进程、将因发生异常情况而无法继续运行的进程置于阻塞状态、负责进程运行中的状态转换等功能。

  如当一个正在执行的进程因等待某事件而暂时不能继续执行时,将其转换为阻塞状态,而当该进程所期待的事件出现时,又将该进程转换为就绪状态等等。进程控制一般是由 OS 的内核中的原语来实现的。

  1.操作系统内核  

  现代操作系统一般将 OS 划分为若干层次,再将 OS 的不同功能分别设置在不同的层次中。通常将一些与硬件紧密相关的模块(如中断处理程序等)、各种常用设备的驱动程序以及运行频率较高的模块(如时钟管理、进程调度和许多模块所公用的一些基本操作),都安排在紧靠硬件的软件层次中,将它们常驻内存,即通常被称为 OS 内核。这种安排方式的目的在于:①便于对这些软件进行保护,防止遭受其他应用程序的破坏;②可以提高 OS 的运行效率。

  相对应的是,为了防止 OS 本身及关键数据(如 PCB 等)遭受到应用程序有意或无意的破坏,通常也将处理机的执行状态分为系统态和用户态两种:

  • 系统态。又称为管态,也称为内核态。它具有较高的特权,能执行一切指令,访问所有寄存器和存储区,传统的 OS 都在系统态运行。
  • 用户态。又称为目态。它是具有较低特权的执行状态,仅能执行规定的指令,访问指定的寄存器和存储区。一般情况下,应用程序只能在用户态运行,不能去执行 OS 指令以及 访问 OS 区域。这样可以防止应用程序对 OS 的破坏。

  大多数 OS 内核都包含了两个方面的功能:

  (1)支撑功能

该功能是提供给 OS 其他众多模块所需要的一些基本功能,以便支撑这些模块工作、其中三种最基本的支撑功能是:中断处理、时钟管理和原语操作。

  • 中断处理。中断处理时内核最基本的功能,是整个操作系统赖以活动的基础、OS 中许多重要的活动,如各种类型的系统调用、键盘命令的输入、进程调度、设备驱动等,无不依赖于中断。通常,为减少处理机中断的时间,提高程序执行的并发性,内核在对中断进行“有限处理”后,便转入相关的进程,由这些进程继续完成后续的处理工作。
  • 时钟管理。时钟管理是内核的一项基本功能,在 OS 中的许多活动都需要得到它的支撑,如在时间片轮转调度中,每当时间片 用完时,便由时钟管理产生一个中断信号,促使调度程序重新进行调度。同样,在实时系统中的截止时间控制、批处理系统中的最长运行时间控制等,也无不依赖于时钟管理功能。
  • 原语操作。原语(Primitive)是由若干条指令组成的,用于完成一定功能的一个过程。它与一般过程的区别在于:它们是“原子操作(Action Operation)”。所谓原子操作,是指一个操作中的所
    有动作要么全做,要么全不做。换言之,它是一个不可分割的基本单位,因此,在执行过程中不允许被中断。原子操作在管态下执行,常驻内存。原语的作用是为了实现进程的通信和控制,系统对进程的控制如不使用原语,就会造成其状态的不确定性,从而达不到进程控制的目的。

  (2)资源管理功能

  • 进程管理。在进程管理中,或者由于各个功能模块的运行频率较高,如进程的调度与分派、进程的创建与撤销等;或者由于它们为多种功能模块所需要,如用于实现进程同步的原语、常用的进程通信原语等。通常都将它们放在内核中,以提高 OS 的性能。
  • 存储器管理。存储器管理软件的运行频率也比较高,如用于实现将用户控件的逻辑地址变换为内存空间的物理地址的地址转换机构、内存分配与回收的功能模块以及实现内存保护和对换功能的模块等。通常也将它们放在内核中,以保证存储器管理具有较高的运行速度。
  • 设备管理。由于设备管理与硬件(设备)紧密相关,因此其中很大部分也都设置在内核中。如各类设备的驱动程序、用于缓存 CPU 与 I/O 速度不匹配矛盾的缓存管理、用于实现设备分配和设备独立性功能的模块等。

  2.进程的创建

  (1)引起进程创建的事件

在多道程序环境中,只有(作为)进程(时)才能在系统中运行。因此,为使程序能运行,就必须为它创建进程。导致一个进程去创建另一个进程的典型事件,可有以下四类:

  • 用户登录。在分时系统中,用户在终端键入登录命令后,如果是合法用户,系统将为该终端建立一个进程,并把它插入就绪队列中。
  • 作业调度。在批处理系统中,当作业调度程序按一定的算法调度到某作业时,便将该作业装入内存,为它分配必要的资源,并立即为它创建进程,再插入就绪队列中。
  • 提供服务。当运行中的用户程序提出某种请求后,系统将专门创建一个进程来提供用户所需要的服务,例如,用户程序要求进行文件打印,操作系统将为它创建一个打印进程,这样,不仅可使打印进程与该用户进程并发执行,而且还便于计算出为完成打印任务所花费的时间。
  • 应用请求。在上述三种情况下,都是由系统内核为它创建一个新进程;而第 4 类事件则是基于应用进程的需求,由它自己创建一个新进程,以便使新进程以并发运行方式完成特定任务。例如,某应用程序需要不断地从键盘终端输入数据,继而又要对输入数据进行相应的处理,然后,再将处理结果以表格形式在屏幕上显示。该应用进程为使这几个操作能并发执行,以加速任务的完成,可以分别建立键盘输入进程、表格输出进程。

  (2)进程的创建过程

一旦操作系统发现了要求创建新进程的事件后,便调用进程创建原语 Creat 按下述步骤创建一个新进程。

  • 申请空白 PCB。为新进程申请获得惟一的数字标识符,并从 PCB 集合中索取一个空白 PCB。
  • 为新进程分配资源。为新进程的程序和数据以及用户栈分配必要的内存空间。显然,此时操作系统必须知道新进程所需内存的大小。对于批处理作业,其大小可在用户提出创建进程要求时提供。若是为应用进程创建子进程,也应是在该进程提出创建进程的请求中给出所需内存的大小。对于交互型作业,用户可以不给出内存要求而由系统分配一定的空间。如果新进程要共享某个已在内存的地址空间(即已装入内存的共享段),则必须建立相应的链接。
  • 初始化进程控制块。PCB 的初始化包括:① 初始化标识信息,将系统分配的标识符和父进程标识符填入新 PCB 中;② 初始化处理机状态信息,使程序计数器指向程序的入口地址,使栈指针指向栈顶;③ 初始化处理机控制信息,将进程的状态设置为就绪状态或静止就绪状态,对于优先级,通常是将它设置为最低优先级,除非用户以显式方式提出高优先级要求。
  • 将新进程插入就绪队列,如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列。

  3.进程的终止

  (1)引起进程终止的事件

  • 正常结束。表示进程的任务已经完成,准备退出运行。在任何计算机系统中,都应有一个用于表示进程已经运行完成的指示。在批处理系统中,通常在程序的最后安排一条 Holt 指令或终止的系统调用。当程序运行到 Holt 指令时,将产生一个中断,去通知 OS 本进程已经完成。在分时系统中,用户可利用 Logs off 去表示进程运行完毕,此时同样可产生一个中断,去通知 OS 进程已运行完毕。
  • 异常结束。是指进程在运行时发生了某种异常事件,使程序无法继续运行。这类异常事件很多,常见的有下述几种:
  1. 越界错误。这是指程序所访问的存储区已越出该进程的区域。
  2. 保护错。这是指进程试图去访问一个不允许访问的资源或文件,或者以不适当的方式进行访问,例如,进程试图去写一个只读文件。
  3. 非法指令。这是指程序试图去执行一条不存在的指令。出现该错误的原因,可能是程序错误地转移到数据区,把数据当成了指令。
  4. 特权指令错。这是指用户进程试图去执行一条只允许 OS 执行的指令。
  5. 运行超时。这是指进程的执行时间超过了指定的最大值。
  6. 等待超时。这是指进程等待某事件的时间超过了规定的最大值。
  7. 算术运算错。这是指进程试图去执行一个被禁止的运算,例如被 0 除。
  8. I/O 故障。这是指在 I/O 过程中发生了错误等。
  • 外界干预。外界干预并非指在本进程运行中出现了异常事件,而是指进程因外界的请求而终止运行。这些干预有:
  1. 操作员或操作系统干预。由于某种原因,例如,发生了死锁,由操作员或操作系统终止该进程。
  2. 父进程请求。由于父进程具有终止自己的任何子孙进程的权力,因而当父进程提出请求时,系统将终止该进程。
  3. 父进程终止。当父进程终止时,OS 也将它的所有子孙进程终止。

  (2)进程的终止过程

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

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

  4.进程的阻塞与唤醒

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

  • 向系统请求共享资源失败。进程在向系统请求共享资源时,由于系统已无足够的资源分配给它,此时进程因不能继续运行而转变为阻塞状态。例如,一进程请求使用某资源,如打印机,由于系统已将打印机分配给其他进程而不能分配给请求进程,这时请求者进程只能被阻塞,仅在其他进程在释放出打印机的同时,才将请求进程唤醒。
  • 等待某种操作的完成。当进程启动某种操作后,如果该进程必须在该操作完成之后才能继续执行,则必须先使该进程阻塞,以等待该操作完成。例如,进程启动了某 I/O 设备,如果只有在 I/O 设备完成了指定的 I/O 操作任务后进程才能继续执行,则该进程在启动了 I/O 操作后,便自动进入阻塞状态去等待。在 I/O 操作完成后,再由中断处理程序或中断进程将该进程唤醒。
  • 新数据尚未到达。对于相互合作的进程,如果其中一个进程需要先获得另一(合作)进程提供的数据后才能对数据进行处理,则只要其所需数据尚未到达,该进程只有(等待)阻塞。例如,有两个进程,进程 A 用于输入数据,进程 B 对输入数据进行加工。假如 A 尚未将数据输入完毕,则进程B 将因没有所需的处理数据而阻塞;一旦进程 A 把数据输入完毕,便可去唤醒进程 B。
  • 等待新任务的到达。在某些系统中,特别是网络环境下的 OS,往往设置一些具有某特定功能的系统进程,每当这种进程完成任务后,便把自己阻塞起来以等待新任务到来。例如,在网络环境中的发送进程,其主要任务是发送数据包,若已有的数据包已全部发送完成而又无新的发送请求,这时(发送)进程将使自己进入阻塞状态; 仅当又有进程提出新的发送请求时,才将发送进程唤醒。

  (2)进程阻塞过程

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

  (3)进程唤醒过程

  当被阻塞进程所期待的事件出现时,如 I/O 完成或其所期待的数据已经到达,则由有关进程(比如用完并释放了该 I/O 设备的进程)调用唤醒原语 wakeup,将等待该事件的进程唤醒。唤醒原语执行的过程是:首先把被阻塞的进程从等待该事件的阻塞队列中移出,将其 PCB 中的现行状态由阻塞改为就绪,然后再将该 PCB 插入到就绪队列中。应当指出,block 原语和 wakeup 原语是一对作用刚好相反的原语。因此,如果在某进程中调用了阻塞原语,则必须在与之相合作的另一进程中或其他相关的进程中安排唤醒原语,以能唤醒阻塞进程;否则,被阻塞进程将会因不能被唤醒而长久地处于阻塞状态,从而再无机会继续运行。

  5.进程的挂起与激活

  (1)进程的挂起

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

  (2)进程的激活

  当发生激活进程的事件时,例如,父进程或用户进程请求激活指定进程,若该进程驻留在外存而内存中已有足够的空间时,则可将在外存上处于静止就绪状态的该进程换入内存。这时,系统将利用激活原语 active( )将指定进程激活。激活原语先将进程从外存调入内存,检查该进程的现行状态,若是静止就绪,便将之改为活动就绪;若为静止阻塞,便将之改为活动阻塞。假如采用的是抢占调度策略,则每当有新进程进入就绪队列时,应检查是否要进行重新调度,即由调度程序将被激活进程与当前进程进行优先级的比较,如果被激活进程的优先级更低,就不必重新调度;否则,立即剥夺当前进程的运行,把处理机分配给刚被激活的进程。

 

  三、进程的同步

  在 OS 中引入进程后,一方面可以使系统中的多道程序并发执行,这不仅能有效地改善资源利用率,还可以显著地提高系统的吞吐量,但另一方面却使系统变得更加复杂。如果不能采取有效的措施,对多个进程的运行进行妥善的管理,必然会因为这些进程对系统资源的无序争夺给系统造成混乱。致使每次处理的结果存在着不确定性,即显现出不可再现性。

  为保证多个进程能有条不紊地运行,在多道程序系统中,必须引入进制同步机制。进程同步进制分为硬件同步机制、信号量机制和管程机制等。

  (1)进程同步的基本概念

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

  • 间接相互制约的关系。同处于一个系统中的进程,通常都共享着某种系统资源,如共享 CPU、共享 I/O 设备等。所谓间接相互制约即源于这种资源共享,例如,有两个进程 A和 B,如果在 A 进程提出打印请求时,系统已将惟一的一台打印机分配给了进程 B,则此时进程 A 只能阻塞;一旦进程 B 将打印机释放,则 A 进程才能由阻塞改为就绪状态。
  • 直接相互制约的关系。这种制约主要源于进程间的合作。例如,有一输入进程 A 通过单缓冲向进程 B 提供数据。当该缓冲空时,计算进程因不能获得所需数据而阻塞,而当进程 A 把数据输入缓冲区后,便将进程 B 唤醒;反之,当缓冲区已满时,进程 A 因不能再向缓冲区投放数据而阻塞,当进程 B 将缓冲区数据取走后便可唤醒 A。

    许多硬件资源如打印机、磁带机等,都属于临界资源(Critical Resouce),诸进程间应采取互斥方式,实现对这种资源的共享。

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

    为实现进程互斥地进入自已的临界区,可用软件方法,更多的是在系统中设置专门的同步机构来协调各进程间的运行。所有同步机制都应遵循下述四条准则:

  • 空闲让进。当无进程处于临界区时,表明临界资源处于空闲状态,应允许一个请求进入临界区的进程立即进入自己的临界区,以有效地利用临界资源。
  • 忙则等待。当已有进程进入临界区时,表明临界资源正在被访问,因而其它试图进入临界区的进程必须等待,以保证对临界资源的互斥访问。
  • 有限等待。对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,以免陷入“死等”状态。
  • 让权等待。当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”状态。

  (2)硬件同步机制

  • 关中断。实现互斥最简单的方法之一。在进入锁测试之前关闭中断,直到完成锁测试并上锁之后才能打开终端。这样,进程在临界区执行期间,计算机系统不响应终端,从而不会引发调度,也就不会发生进程或线程切换。由此,保证了对锁的测试和关锁操作的连续性和完整性,有效地保证了互斥。
  • 利用 Test-and-Set 指令实现互斥。借助硬件指令-- TS 指令实现互斥。
  • 利用 Swap 指令实现进程互斥。该指令称为对换指令,在 Inter 80x86 中又称为 XCHG 指令,用于交换两个字的内容。

  (3)信号量机制

    信号量从整型型号量经记录信号量,进而发展为“信号量集”机制。现在,信号量机制已被广泛地应用于单处理机和多处理机系统以及计算机网络中。

    1.整型信号量

  整型信号量定义为一个用于表示资源数目的整型量 S,它与一般整形量不同,除初始化外,仅能通过两个标准的原子操作 wait(S) 和 signal(S)来对 S 进行加或减操作。这两个操作被称为 P、V操作。由于 wait(S) 和 signal(S) 是两个原子操作,因此,它们在执行时是不可中断的。即当一个进程在修改某信号量时,没有其他进程可同时对该信号量进行修改。

wait(S) {
    wait(S <= 0);
    S--;
}

signal(S) {
    S++;
}

2.记录性信号量

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

    3.AND 型信号量

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

4.信号量集

  在记录型信号量机制中,wait(S)或 signal(S)操作仅能对信号量施以加 1 或减 1 操作,意味着每次只能获得或释放一个单位的临界资源。而当一次需要 N 个某类临界资源时,便要进行 N 次 wait(S)操作,显然这是低效的。此外,在有些情况下,当资源数量低于某一下限值时,便不予以分配。因而,在每次分配之前,都必须测试该资源的数量,看其是否大于其下限值。基于上述两点,可以对 AND 信号量机制加以扩充,形成一般化的“信号量集”机制。

  (4)管程机制

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

  系统中的各种硬件资源和软件资源,均可用数据结构抽象地描述其资源特性,即用少量信息和对该资源所执行的操作来表征该资源,而忽略了它们的内部结构和实现细节。例如,对一台电传机,可用与分配该资源有关的状态信息(busy 或 free)和对它执行请求与释放的操作,以及等待该资源的进程队列来描述。又如,一个 FIFO 队列,可用其队长、队首和队尾以及在该队列上执行的一组操作来描述。
  利用共享数据结构抽象地表示系统中的共享资源,而把对该共享数据结构实施的操作定义为一组过程,如资源的请求和释放过程 request 和 release。进程对共享资源的申请、释放和其它操作,都是通过这组过程对共享数据结构的操作来实现的,这组过程还可以根据资源的情况,或接受或阻塞进程的访问,确保每次仅有一个进程使用共享资源,这样就可以统一管理对共享资源的所有访问,实现进程互斥。

  代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,共同构成了一个操作系统的资源管理模块,我们称之为管程。管程被请求和释放资源的进程所调用。Hansan 为管程所下的定义是:“一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据”。
  由上述的定义可知,管程由四部分组成:① 管程的名称;② 局部于管程内部的共享数据结构说明;③ 对该数据结构进行操作的一组过程;④ 对局部于管程内部的共享数据设置初始值的语句。

 

  四、进程通信

  进程通信,是指进程之间的信息交换。由于进程的互斥和同步,需要在进程间交换一定的信息,故不少学者也将他们归为进程通信,但只能它它们成为低级进程通信。在进程互斥中,进程通过只修改信号量来向其他进程表明临界资源是否可用。信号量机制作为同步工具是卓有成效的,但作为通信工具,则不够理想,主要表现在下述两方面:(1) 效率低,生产者每次只能向缓冲池投放一个产品(消息),消费者每次只能从缓冲区中取得一个消息;(2) 通信对用户不透明,OS 置为进程之间的通信提供了共享存储器。而关于进程之间通信所需之共享数据结构的设置、数据的传送、进程的互斥与同步,都必须由程序员去实现,显然,对于用户而言,是非常不方便的。

  在进程之间要传送大量数据时,应道利用 OS 提供的高级通信工具,该工具的主要特点是:(1)使用方便。OS 隐藏了实现进程通信的具体细节,向用户提供了一组用于实现高级通信的命令(原语),用户可方便地直接利用它实现进程之间的通信。或者说,通信过程对用户是透明的。(2)高效地传送大量数据。用户可直接利用高级通信命令(原语)高效地传送大量的数据。

  目前,高级通信机制可归结为四大类:共享存储器系统、管道通信系统、消息传递系统以及客户机-服务器系统。

  1.共享存储器系统

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

  • 基于共享数据结构的通信方式。在这种通信方式中,要求诸进程公用某些数据结构,借以实现诸进程间的信息交换。如在生产者—消费者问题中,就是用有界缓冲区这种数据结构来实现通信的。这里,公用数据结构的设置及对进程间同步的处理,都是程序员的职责。这无疑增加了程序员的负担,而操作系统却只须提供共享存储器。因此,这种通信方式是低效的,只适于传递相对少量的数据。
  • 基于共享存储区的通信方式。为了传输大量数据,在存储器中划出了一块共享存储区,诸进程可通过对共享存储区中数据的读或写来实现通信。这种通信方式属于高级通信。进程在通信前,先向系统申请获得共享存储区中的一个分区,并指定该分区的关键字;若系统已经给其他进程分配了这样的分区,则将该分区的描述符返回给申请者,继之,由申请者把获得的共享存储分区连接到本进程上;此后,便可像读、写普通存储器一样地读、写该公用存储分区。

  2.管道通信系统

  所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名 pipe 文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。这种方式首创于UNIX 系统,由于它能有效地传送大量数据,因而又被引入到许多其它的操作系统中。为了协调双方的通信,管道机制必须提供以下三方面的协调能力:

  •  互斥,即当一个进程正在对 pipe 执行读/写操作时,其它(另一)进程必须等待。
  • 同步,指当写(输入)进程把一定数量(如 4 KB)的数据写入 pipe,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程读一空 pipe 时,也应睡眠等待,直至写进程将数据写入管道后,才将之唤醒。
  • 确定对方是否存在,只有确定了对方已存在时,才能进行通信。

  3.消息传递系统

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

  该方式隐藏了通信实现细节,使通信过程对用户透明化,降低了通信程序设计的复杂性和错误率,称为当前应用最为广泛的一类进程间通信的机制。例如:在计算机网络中,又把 message 称为报文;在当今最为流行的微内核操作系统中,微内核与服务器之间的通信,无一例外地都采用了消息传递机制。又由于它能很好地支持多处理机系统、分布式系统和计算机网络,因此它也成为这些领域最主要的通信工具。消息传递系统的通信方式属于高级通信方式。又因其实现方式的不同而进一步分成直接通信方式和间接通信方式两种。

  • 直接通信方式。是指发送进程利用 OS 所提供的发送原语,直接把消息发送给目标进程。
  • 间接通信方式。是指发送和接收进程,都通过共享中间实体(称为邮箱)的方式进行消息的发送和接收,完成进程间的通信。

  4.客户机-服务器系统

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

 

  五、线程的概述

  在 20 世纪 60 年代中期,人们在设计多道程序 OS 时,引入了进程的概念后,从而解决了单处理机环境下的程序并发执行问题。在此后长达20年的时间里,在多道程序 OS 中一直都是以进程作为能拥有资源和独立调度(运行)的基本单位的。直到 20 世纪 80 年代中期,人们又提出了比进程更小的能独立运行的基本单位——线程(Threads),试图用它来提高系统内程序并发执行的程度,从而可进一步提高系统的吞吐量。特别是在进入 20 世纪 90 年代后,多处理机系统得到迅速发展,线程能比进程更好地提高程序的并行执行程度,充分地发挥多处理机的优越性,因而在近几年所推出的多处理机 OS 中也都引入了线程,以改善 OS 的性能。

  1.线程的引入

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

  (1)进程的两个基本属性

进程有两个基本属性,正是由于进程有了这两个基本属性,才使进程称为一个能独立运行的基本单位,从而也就构成了进程并发执行的基础。

  • 进程是一个可拥有资源的独立单位,一个进程要能独立运行,它必须拥有一定的资源,包括用于存放程序正文、数据的磁盘和内存地址空间,以及它在运行时所需要的 I/O 设备、已打开的文件、信号量等;
  • 进程同时又是一个可独立调度和分派的基本单位,一个进程要能独立运行,它还必须是一个可独立调度和分派的基本单位。每个进程在系统中有唯一的 PCB,系统可根据其 PCB 感知进程的存在,也可以根据其 PCB 中的信息,对进程进行调度,还可将端点信息保存在其 PCB 中。反之,再利用进程 PCB 中的信息来恢复进程运行的现场。

  (2)程序并发执行所需付出的时空开销

  为使程序能并发执行,系统必须进行以下的一系列操作:

  • 创建进程,系统在创建一个进程时,必须为它分配其所必须的、除处理机以外的所有资源,如内存空间、I/O 设备,以及建立相应的 PCB。
  • 撤销进程,系统在撤销进程时,又必须先对其所占有的资源执行回收操作,然后再撤销 PCB。
  • 进程切换,对进程进行上下文切换时,需要保留当前进程的 PCB 环境,设置新选中进程的 PCB 环境,因此须花费不少的处理机时间。

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

  (3)线程-作为调度和分派的基本单位

  如何能使多个程序更好地并发执行,同时又尽量减少系统的开销,已成为近年来设计操作系统时所追求的重要目标。有不少研究操作系统的学者们想到,若能将进程的上述两个属性分开,由操作系统分开处理,亦即对于作为调度和分派的基本单位,不同时作为拥有资源的单位,以做到“轻装上阵”;而对于拥有资源的基本单位,又不对之进行频繁的切换。正是在这种思想的指导下,形成了线程的概念。
  随着 VLSI 技术和计算机体系结构的发展,出现了对称多处理机(SMP)计算机系统。它为提高计算机的运行速度和系统吞吐量提供了良好的硬件基础。但要使多个 CPU 很好地协调运行,充分发挥它们的并行处理能力,以提高系统性能,还必须配置性能良好的多处理机 OS。但利用传统的进程概念和设计方法,已难以设计出适合于 SMP 结构的计算机系统的 OS。其最根本的原因是进程“太重”,致使实现多处理机环境下的进程调度、分派和切换时,都需花费较大的时间和空间开销。如果在 OS 中引入线程,以线程作为调度和分派的基本单位,则可以有效地改善多处理机系统的性能。因此,一些主要的 OS(UNIX、OS/2、Windows)厂家都又进一步对线程技术做了开发,使之适用于 SMP 的计算机系统。

  

  2.线程与进程的比较

  线程具有许多传统进程所具有的特征,所以又称为轻型进程(Light-Weight Process)或进程元。相应地,把传统进程称为重型进程(Heavy-Weight Process),传统进程相当于只有一个线
程的任务。在引入了线程的操作系统中,通常一个进程都拥有若干个线程,至少也有一个线程。下面我们从调度性、并发性、系统开销和拥有资源等方面对线程和进程进行比较。

  (1)调度的基本单位

  在传统的 OS 中,进程是作为独立调度和分派的基本单位,因而进程是能独立运行的基本单位。在每次被调度时,都需要进行上下文切换,开销较大。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,因而线程是能独立运行的基本单位。而进程作为资源拥有的基本单位,把传统进程的两个属性分开,使线程基本上不拥有资源,这样线程便能轻装前进,从而可显著地提高系统的并发程度。

  当线程切换时,仅需保存和设置少量寄存器内容,切换代价远低于进程。在同一进程中,线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,将会引起进程的切换。

(2)并发性

  在引入线程的 OS 中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,甚至还允许在一个进程中所有线程都能并发执行。同样,不同进程中的线程也能并发执行。这使得 OS 具有更好的并发性,从而能更加有效地提高系统资源的利用率和吞吐量。例如,在文字处理器进程中可以设置三个线程:第一个线程用于显示文字和图形,第二个线程从键盘读入数据,第三个线程在后台进行拼写和语法检查。又如,在网页浏览器中,可以设置一个线程来显示图像或文本,再设置一个线程用于从网络中接收数据。

  此外,有的应用程序需要执行多个相似的任务。例如,一个网络服务器经常会接到许多客户的请求,如果仍采用传统的单线程的进程来执行该任务,则每次只能为一个客户服务。但如果在一个进程中可以设置多个线程,将其中一个专用于监听客户的请求,则每当有一个客户请求时,便立即创建一个线程来处理该客户的请求。

  (3)拥有资源

  进程可以拥有资源,并作为系统中拥有资源的一个基本单位。然而,线程本身不拥有系统资源,而是仅有一点必不可少的、能保证独立运行的资源。比如,在每个线程中都应具有一个用于控制线程运行的线程控制块 TCB、用于只是被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

  线程除了拥有自己的少量资源外,还允许多个线程共享该进程所拥有的资源,这首先表现在:属于统一进程的所有线程都具有相同的地址空间,这意味着,线程可以访问该地址空间中的每一个虚地址;此外,还可以访问进程中所拥有的资源,如已经打开的文件、定时器、信号量机构等的内存空间和它所申请到的 I/O 设备等。

  (4)独立性

  在同一进程的不同线程之间的独立性要比不同进程之间到的独立性低得多,这是因为,为防止进程之间彼此干扰和破坏,每个进程都拥有一个独立的地址空间和其他资源,除了共享全局变量外,不允许其他进程的访问。但是同一进程中的不同线程往往是为了提高并发性以及进行相互之间的合作而创建的,它们共享进程的内存地址空间和其他资源。如每个线程都可以访问它们所属进程地址空间中的所有地址,如一个线程的堆栈可以被其他线程读/写,甚至可以完全清除。由一个线程打开的文件可以供其他线程读/写。

  (5)系统开销

  在创建或撤销进程时,系统都要为之分配和回收进程控制块、分配或回收其他资源,如内存空间和 I/O 设备等。OS 为此所付出的开销,明显大于线程创建或撤销时所付出的开销。类似地,在进程切换时,涉及到进程上下文的切换,而线程的切换代价也远低于进程的。例如,在 Solaris 2 OS 中,线程的创建要比进程的创建块 30 倍,而线程上下文切换要比进程上下文切换块 5 倍。此外,由于一个进程中的多个线程具有相同的地址空间,线程之间的同步和通信也比进程的简单。因此,在一些 OS 中,线程的切换、同步和通信都无需操作系统内核的干预。

  (6)支持多处理机系统

  在多处理机系统中,对于传统的进程,即单线程进程,不管有多少个处理机,该进程只能运行在一个处理机上。但对于多线程进程,就可以将一个进程中的多个线程分配到多个处理机上,使它们并行执行,这无疑将加速进程的完成。因此,现代多处理机 OS 都无一例外地引入了多线程。

 

  3.线程的状态和线程控制块

  (1)线程运行的三个状态

  • 执行状态,表示线程已获得处理机而正在运行。
  • 就绪状态,指线程已具备了各种执行条件,只需再获得 CPU 便可立即执行;
  • 阻塞状态,指线程在执行中因某事件受阻而处于暂停状态,例如,当一个线程执行从键盘读入数据的系统调用时,该线程就被阻塞。

    线程状态之间的转换和进程状态之间的转换是一样的。

  (2)线程控制块 TCB

  如同每个进程有一个进程控制块一样,系统也为每个线程配置了一个线程控制块 TCB,将所有用于控制和管理线程的信息记录在线程控制块中。线程控制块通常有这样几项:

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

  (3)多线程 OS 中的进程属性

通常在多线程 OS 中的进程都包含了多个线程,并为它们提供资源。OS 支持在一个进程中的多个线程能并发执行,但此时的进程就不再作为一个执行的实体。多线程 OS 中进程的属性:

  • 进程是一个可拥有资源的基本单位。在多线程 OS 中,进程仍是作为系统资源分配的基本单位,任一进程中所拥有的资源都包括:受到分别保护的用户地址空间、用于实现进程(线程)间同步和通信的机制、已打开的文件和已申请到的 I/O 设备,以及一张由核心进程维护的地址映射表,该表用于实现用户程序的逻辑地址到其内存物理地址的映射。
  • 多个线程可并发执行。通常,一个进程都含有若干个相对独立的线程,其数目可多可少,但至少也要有一个线程。由进程为这些(个)线程提供资源及运行环境,使它们能并发执行。在 OS 中的所有线程都只能属于某一个特定进程。实际上,现在把传统进程执行方法称为单线程方法。如传统的 UNIX 系统能支持多用户进程,但只支持单线程方法。反之,将每个进程支持多个线程执行的方法称为多线程方法。如 Java 的运行环境是单进程多线程的,而Windows 2000,Solaris、Mach 等采用的则是多进程多线程的方法。
  • 进程已不是一个可执行的实体。在多线程 OS 中,是把线程作为独立运行(或称调度)的基本单位,而此时的进程已不再是一个可执行的实体。虽然如此,进程仍具有与执行相关的状态。例如,所谓进程处于“执行”状态,实际上是指该进程中的某线程正在执行。此外,对进程所施加的与进程状态有关的操作,也对其线程起作用。例如,在把某个进程挂起时,该进程中的所有线程也都将被挂起;又如,在把某进程激活时,属于该进程的所有线程也都将被激活。

  

  六、线程的实现

  1.线程的实现方式

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

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

  这种线程实现方式的四个优点:

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

    这种线程实现方式的缺点:

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

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

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

  值得说明的是,对于设置了用户级线程的系统,其调度仍是以进程为单位进行的。在采用轮转调度算法时,各个进程轮流执行一个时间片,这对诸进程而言似乎是公平的。但假如在进程 A 中包含了一个用户级线程,而在另一个进程 B 中含有 100 个用户级线程,这样,进程 A 中线程的运行时间将是进程 B 中各线程运行时间的 100 倍;相应地,其速度要快上 100 倍。

  假如系统中设置的是内核支持线程,则调度便是以线程为单位进行的。在采用轮转法调度时,是各个线程轮流执行一个时间片。同样假定进程 A 中只有一个内核支持线程,而在进程 B 中有 100 个内核支持线程。此时进程 B 可以获得的 CPU 时间是进程 A 的 100 倍,且进程 B 可使 100 个系统调用并发工作。

  使用用户级线程实现方式有很多优点:

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

用户级线程实现方式的主要缺点在于如下两个方面:

  • 系统调用的阻塞问题。在基于进程机制的操作系统中,大多数系统调用将阻塞进程,因此,当线程执行一个系统调用时,不仅该线程被阻塞,而且进程内的所有线程都会被阻塞。而在内核支持线程方式中,则进程中的其它线程仍然可以运行。
  • 在单纯的用户级线程实现方式中,多线程应用不能利用多处理机进行多重处理的优点。内核每次分配给一个进程的仅有一个 CPU,因此进程中仅有一个线程能执行,在该线程放弃 CPU 之前,其它线程只能等待。

  (3)组合方式

  有些操作系统把用户级线程和内核支持线程两种方式进行组合,提供了组合方式ULT/KST 线程。在组合方式线程系统中,内核支持多 KST 线程的建立、调度和管理,同时,也允许用户应用程序建立、调度和管理用户级线程。一些内核支持线程对应多个用户级线程,程序员可按应用需要和机器配置对内核支持线程数目进行调整,以达到较好的效果。组合方式线程中,同一个进程内的多个线程可以同时在多处理器上并行执行,而且在阻塞一个线程时,并不需要将整个进程阻塞。所以,组合方式多线程机制能够结合 KST 和 ULT两者的优点,并克服了其各自的不足。

 

  2.线程的实现

  不论是进程还是线程,都必须直接或间接地取得内核的支持。由于内核支持线程可以直接利用系统调用为它服务,故线程的控制相当简单;而用户级线程必须借助于某种形式的中间系统的帮助方能取得内核的服务,故在对线程的控制上要稍复杂些。

  (1)内核支持线程的实现

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

  每当进程要创建一个线程时,便为新线程分配一个TCB,将有关信息填入该 TCB 中,并为之分配必要的资源,如为线程分配数百至数千个字节的栈空间和局部存储区,于是新创建的线程便有条件立即执行。当 PTDA中的所有 TCB 空间已用完,而进程又要创建新的线程时,只要其所创建的线程数目未超过系统的允许值(通常为数十至数百个),系统可再为之分配新的 TCB 空间;在撤消一个线程时,也应回收该线程的所有资源和 TCB。可见,内核支持线程的创建、 撤消均与进程的相类似。在有的系统中为了减少创建和撤消一个线程时的开销,在撤消一个线程时,并不立即回收该线程的资源和 TCB,当以后再要创建一个新线程时,便可直接利用已被撤消但仍保持有资源和 TCB 的线程作为新线程。

  内核支持线程的调度和切换与进程的调度和切换十分相似,也分抢占式方式和非抢占方式两种。在线程的调度算法上,同样可采用时间片轮转法、优先权算法等。当线程调度选中一个线程后,便将处理机分配给它。当然,线程在调度和切换上所花费的开销,要比进程的小得多。

  (2)用户级线程的实现

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

  • 运行时系统。①所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤消线程的函数、 线程同步和通信的函数以及实现线程调度的函数等。正因为有这些函数,才能使用户级线程与内核无关。运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。②在传统的 OS 中,进程在切换时必须先由用户态转为核心态,再由核心来执行切换任务;而用户级线程在切换时则不需转入核心态,而是由运行时系统中的线程切换过程来执行切换任务。该过程将线程的 CPU 状态保存在该线程的堆栈中,然后按照一定的算法选择一个处于就绪状态的新线程运行,将新线程堆栈中的 CPU 状态装入到 CPU 相应的寄存器中,一旦将栈指针和程序计数器切换后,便开始了新线程的运行。由于用户级线程的切换无需进入内核,且切换操作简单,因而使用户级线程的切换速度非常快。③不论在传统的 OS 中,还是在多线程 OS 中,系统资源都是由内核管理的。在传统的OS 中,进程是利用 OS 提供的系统调用来请求系统资源的,系统调用通过软中断(如 trap)机制进入 OS 内核,由内核来完成相应资源的分配。用户级线程是不能利用系统调用的。当线程需要系统资源时,是将该要求传送给运行时系统,由后者通过相应的系统调用来获得系统资源的。
  • 内核控制线程。①这种线程又称为轻型进程 LWP(Light Weight Process)。每一个进程都可拥有多个 LWP,同用户级线程一样,每个 LWP 都有自己的数据结构(如 TCB),其中包括线程标识符、优先级、状态,另外还有栈和局部存储区等。它们也可以共享进程所拥有的资源。LWP 可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只要将它连接到一个LWP 上,此时它便具有了内核支持线程的所有属性。这种线程实现方式就是组合方式。②在一个系统中的用户级线程数量可能很大,为了节省系统开销,不可能设置太多的LWP,而把这些 LWP 做成一个缓冲池,称为“线程池”。用户进程中的任一用户线程都可以连接到 LWP 池中的任何一个 LWP 上。为使每一用户级线程都能利用 LWP 与内核通信,可以使多个用户级线程多路复用一个 LWP,但只有当前连接到 LWP 上的线程才能与内核通信,其余进程或者阻塞,或者等待 LWP。而每一个 LWP 都要连接到一个内核级线程上,这样,通过 LWP 可把用户级线程与内核线程连接起来,用户级线程可通过 LWP 来访问内核,但内核所看到的总是多个 LWP 而看不到用户级线程。亦即,由 LWP 实现了在内核与用户级线程之间的隔离,从而使用户级线程与内核无关。③当用户级线程不需要与内核通信时,并不需要 LWP;而当要通信时,便需借助于 LWP,而且每个要通信的用户级线程都需要一个 LWP。例如,在一个任务中,如果同时有 5 个用户级线程发出了对文件的读、写请求,这就需要有 5 个 LWP 来予以帮助,即由 LWP 将对文件的读、写请求发送给相应的内核级线程,再由后者执行具体的读、写操作。如果一个任务中只有 4 个 LWP,则只能有 4 个用户级线程的读、写请求被传送给内核线程,余下的一个用户级线程必须等待。④在内核级线程执行操作时,如果发生阻塞,则与之相连接的多个 LWP 也将随之阻塞,进而使连接到 LWP 上的用户级线程也被阻塞。如果进程中只包含了一个 LWP,此时进程也应阻塞。这种情况与前述的传统 OS 一样,在进程执行系统调用时,该进程实际上是阻塞的。但如果在一个进程中含有多个 LWP,则当一个 LWP 阻塞时,进程中的另一个 LWP 可继续执行;即使进程中的所有 LWP 全部阻塞,进程中的线程也仍然能继续执行,只是不能再去访问内核。
  •  

  3.线程的创建和终止

  如同进程一样,线程也是具有生命期的,它由创建而产生,由调度而执行,由终止而消亡。相应的,在 OS 中也就有用于创建线程的函数(或系统调用)和用于终止线程的函数(或系统调用)。

  (1)线程的创建

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

  (2)线程的终止

  终止线程的方式有两种:一种是在线程完成了自己的工作后自愿退出;另一种是线程在运行中出现异常情况或由于某种原因而被其它线程强行终止,由终止线程通过调用相应的函数(或系统调用)对它执行终止操作。但有些线程(主要是系统线程),在它们一旦被建立起来之后,便一直运行下去而不再被终止。在大多数的 OS 中,线程被终止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了分离函数后,被终止的线程才与资源分离,此时的资源才能被其它线程利用。
  虽已被终止但尚未释放资源的线程,仍可以被需要它的线程所调用,以使被终止线程重新恢复运行。为此,调用者线程须调用一条被称为“等待线程终止”的连接命令,来与该线程进行连接。如果在一个调用者线程调用“等待线程终止”的连接命令试图与指定线程相连接时,若指定线程尚未被终止,则调用连接命令的线程将会阻塞,直至指定线程被终止后才能实现它与调用者线程的连接并继续执行;若指定线程已被终止,则调用者线程不会被阻塞而是继续执行。

 

4.线程间的同步和通信

  为使系统中的多线程能有条不紊地运行,在系统中必须提供用于实现线程间同步和通信的机制。为了支持不同频率的交互操作和不同程度的并行性,在多线程 OS 中通常提供多种同步机制,如互斥锁、条件变量、计数信号量以及多读、单写锁等。

(1)互斥锁

  互斥锁是一种比较简单的、用于实现线程间对资源互斥访问的机制。由于操作互斥锁的时间和空间开销都较低,因而较适合于高频度使用的关键共享数据和程序段。互斥锁可以有两种状态,即开锁(unlock)和关锁(lock)状态。相应地,可用两条命令(函数)对互斥锁进行操作。其中的关锁 lock 操作用于将 mutex 关上,开锁操作 unlock 则用于打开 mutex。
  当一个线程需要读/写一个共享数据段时,线程首先应为该数据段所设置的 mutex 执行关锁命令。命令首先判别 mutex 的状态,如果它已处于关锁状态,则试图访问该数据段的线程将被阻塞;而如果 mutex 是处于开锁状态,则将 mutex 关上后便去读/写该数据段。在线程完成对数据的读/写后,必须再发出开锁命令将 mutex 打开,同时还须将阻塞在该互斥锁上的一个线程唤醒,其它的线程仍被阻塞在等待 mutex 打开的队列上。
  另外,为了减少线程被阻塞的机会,在有的系统中还提供了一种用于 mutex 上的操作命令 Trylock。当一个线程在利用 Trylock 命令去访问 mutex 时,若 mutex 处于开锁状态,Trylock 将返回一个指示成功的状态码;反之,若 mutex 处于关锁状态,则 Trylock 并不会阻塞该线程,而只是返回一个指示操作失败的状态码。

(2)条件变量

  在许多情况下,只利用 mutex 来实现互斥访问可能会引起死锁,我们通过一个例子来说明这一点。有一个线程在对 mutex 1 执行关锁操作成功后,便进入一临界区 C,若在临界区内该线程又须访问某个临界资源 R,同样也为 R 设置另一互斥锁 mutex 2。假如资源 R 此时正处于忙碌状态,线程在对 mutex 2 执行关锁操作后必将被阻塞,这样将使 mutex 1 一直保持关锁状态;如果保持了资源 R 的线程也要求进入临界区 C,但由于 mutex 1 一直保持关锁状态而无法进入临界区,这样便形成了死锁。为了解决这个问题便引入了条件变量。每一个条件变量通常都与一个互斥锁一起使用,亦即,在创建一个互斥锁时便联系着一个条件变量。单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入。而条件变量则用于线程的长期等待,直至所等待的资源成为可用的资源。
  如何利用互斥锁和条件变量来实现对资源 R 的访问。线程首先对 mutex执行关锁操作,若成功便进入临界区,然后查找用于描述该资源状态的数据结构,以了解资源的情况。只要发现所需资源 R 正处于忙碌状态,线程便转为等待状态,并对 mutex 执行开锁操作后,等待该资源被释放;若资源处于空闲状态,表明线程可以使用该资源,于是将该资源设置为忙碌状态,再对 mutex 执行开锁操作。

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

(3)信号量机制

  前面所介绍的用于实现进程同步的最常用工具——信号量机制,也可用于多线程 OS中,实现诸线程或进程之间的同步。为了提高效率,可为线程和进程分别设置相应的信号量。

  • 私用信号量(private samephore)。当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的命令来创建一私用信号量,其数据结构存放在应用程序的地址空间中。私用信号量属于特定的进程所有,OS 并不知道私用信号量的存在,因此,一旦发生私用信号量的占用者异常结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复为 0(空),也不能将它传送给下一个请求它的线程。
  • 公用信号量(public semaphort)。公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。由于它有着一个公开的名字供所有的进程使用,故而把它称为公用信号量。其数据结构是存放在受保护的系统存储区中,由 OS 为它分配空间并进行管理,故也称为系统信号量。如果信号量的占有者在结束时未释放该公用信号量,则 OS 会自动将该信号量空间回收,并通知下一进程。可见,公用信号量是一种比较安全的同步机制。
posted @ 2018-10-17 11:31  BigJunOba  阅读(1997)  评论(0编辑  收藏  举报