跳转至

第 2 章 进程管理

依据:《操作系统概论》(2017 年版)第 2 章、配套自考大纲及附录答案。下文编号指大纲“考核内容与考核要求”;标注“自编”的算例为学习辅助,不是真题。

复习导航:大纲要求与笔记位置

大纲条目 层次 位置 学完应能做到
1.1 顺序执行、1.2 并发执行 识记、领会 第 1 节 对比特点,解释结果为何可能不同
2.1 进程定义与结构、2.2 PCB 识记 第 2 节 定义、组成、PCB 的作用与信息
2.3 三态及转换、2.4 组织 识记 第 3 节 状态、转换条件、链接与索引
3.1–3.4 进程控制 识记 第 4 节 创建、撤销、阻塞、唤醒的条件与步骤
4.1 内核、4.2 中断定义、4.3 中断处理 识记、领会 第 5 节 内核功能、中断含义;断点、现场、中断入口与返回
4.4 两种执行态、系统调用、4.5 调用过程 识记、领会 第 6 节 态转换及系统调用与函数调用的区别
4.6 时钟体系、4.7 时钟机制 识记、领会 第 7 节 RTC、PIT、时钟驱动程序
5.1 线程概念与比较、5.2 线程控制 领会 第 8 节 进程与线程、两种线程实现、控制过程
6.1 同步基本概念与四条准则 领会 第 9 节 临界资源、临界区与协调
6.2 整型信号量、6.3 记录型信号量 综合应用 第 10 节 会设初值、写 P/V、跟踪数值和阻塞
6.4 生产者—消费者、读者—写者 综合应用 第 11 节 能独立建立同步程序
6.5 管程及两类应用 领会 第 12 节 条件变量、缓冲池、哲学家进餐
6.6 进程通信 识记 第 13 节 共享存储器、管道、消息缓冲队列

全章十三节分四块铺开:第 1–4 节定义进程、PCB、三态转换与控制原语;第 5–7 节讲内核支撑机制(中断、系统调用、时钟);第 8 节把调度单位拆到线程;第 9–13 节处理并发协作,涵盖信号量 P/V、管程与进程通信。教材另说明,本章出现的 Linux 内核源码片段不纳入考核范围。

本章学习目标

进程用于描述程序的一次运行,PCB 保存其执行状态和资源信息,三态模型描述它何时就绪、执行或阻塞。内核通过中断、时钟和系统调用控制执行,进程之间再通过同步与通信完成协作。

先掌握进程三态转换和 PCB 的四类信息,再学习进程控制、调度、阻塞与唤醒。信号量部分需要动手建模:设定初值、安排 P/V 次序,并逐步跟踪 value 的变化;改变缓冲区大小或售票规则后,也应能按条件重新推导。

1 顺序执行与并发执行(1.1、1.2,识记、领会)

执行方式 特点 理解
顺序执行 顺序性 前一操作结束,才能进行后一操作
顺序执行 封闭性 在教材的独占资源模型中,运行环境的资源状态仅由本程序改变
顺序执行 可再现性 初始条件和环境相同,重复执行得到相同结果
并发执行 间断性 因共享有限资源,执行过程可能暂停再继续
并发执行 失去封闭性 共享数据和系统状态可由其他进程改变
并发执行 不可再现性 缺乏适当控制时,指令交错顺序不同可能产生不同结果

并发指多个程序在同一时间间隔内推进;单核上靠快速交替实现,并不是同一时刻都在 CPU 上跑。

以共享变量 counter(初值 0)为例,两个进程各执行一次非原子的 counter++。在本例的简化执行模型中,每次自增分为读入寄存器、加 1、写回内存三步。一次可能的交错如下:

步骤 甲进程 乙进程 内存中的 counter
1 读到 0,随后被切换 — 0
2 暂停 读到 0,加 1 并写回 1
3 用先前读到的 0 加 1,再写回 已完成 1

两次自增只得到 1,丢失了一次更新;若甲完成全部三步后乙才执行,结果则为 2。一行高级语言语句未必是原子操作,缺少同步时,结果可能随执行交错而变化。这里用于说明并发机制,不将具体语言的共享变量行为当作保证。

2 进程与进程控制块(2.1、2.2,识记)

程序代码本身不能记录一次运行的执行位置、数据和资源状态,因此需要引入进程。进程是允许并发执行的程序在某个数据集合上的一次运行过程,其实体由三部分组成:

  • 正文段:被执行的机器指令。
  • 用户数据段:执行时直接操作的数据。
  • PCB(进程控制块):记录运行环境与管理信息,供操作系统控制进程。

某一时刻进程在内存中的全部内容合称进程映像。进程具备并发性、动态性、独立性、异步性和结构特征五项属性。在未引入线程的传统模型里,进程一身兼两职,既是资源分配的基本单位,也是 CPU 调度运行的基本单位;引入线程后,这两项职责才拆开(第 8 节详述)。

比较 程序 进程
性质 静态指令集合 程序的一次动态执行过程
存在时间 可长期保存在外存 创建后存在,结束时撤销
实体 程序代码 正文段、用户数据段、PCB
对应关系 同一程序可供多个进程执行 一个进程也可先后执行不同程序

每个进程都有一个 PCB,操作系统通过它识别、管理和控制进程。创建进程需要建立 PCB,撤销进程需要回收 PCB,因此教材称 PCB 是进程存在的唯一标志。

信息类别 主要内容 用途
标识符信息 本进程、父进程、子进程标识 识别进程及关系
处理机状态信息 通用寄存器、PC、PSW、栈指针等 保存与恢复执行现场
调度信息 状态、优先级、调度所需数据 决定何时运行、运行多久
控制信息 程序和数据地址、同步通信、资源清单、链接指针 管理资源、通信及队列

切换进程时,内核保存旧进程的通用寄存器、PC、PSW 等状态,再恢复新进程的已保存状态,使其从原来的执行位置继续运行。本节以 PCB 保存这些管理信息来说明过程。

3 三种基本状态与进程组织(2.3、2.4,识记)

PCB 的状态字段记录进程所处阶段。三态模型依据是否具备运行条件、是否已获得 CPU,将进程分为就绪、执行、阻塞。

状态 已满足的条件 还在等待什么
就绪态 具备运行条件 CPU
执行态 已获得 CPU 正在执行
阻塞态 因资源或事件暂不能运行 I/O 完成、数据到达、资源可用等

图2-1 进程状态转换图

图2-1 进程状态转换图

转换 原因
就绪 → 执行 被调度并获得 CPU
执行 → 就绪 时间片用完或被抢占
执行 → 阻塞 等待资源或事件
阻塞 → 就绪 等待条件满足,被唤醒

三态模型中没有阻塞 → 执行和就绪 → 阻塞两条直接转换。等待 I/O 的进程被唤醒后,先进入就绪队列,再由调度程序分配 CPU;就绪进程尚未执行,也就不能主动发起 I/O 请求或执行 wait 而进入阻塞态。数量上,单核 CPU 同一时刻最多有 1 个进程处于执行态,N 核最多 N 个;阻塞或尚未调度时,执行态也可以为 0,就绪队列则可能为空,也可能容纳全部进程。

操作系统管进程,落到数据结构上就是管 PCB。常用两种排法:链接方式靠 PCB 里的指针把同状态的进程串成链表(就绪队列常按优先级拆成多条,阻塞队列常按等待的事件拆成多条);索引方式则按状态各建一张索引表,表项里存各个 PCB 的地址。

4 进程控制(3.1–3.4,识记)

进程创建、终止和状态转换由内核的进程控制原语完成。关键状态和队列更新需要保证原子性,避免其他执行流看到尚未完成的中间状态。

操作 触发条件 教材的一般步骤
创建 用户登录、作业调度、提供服务、应用请求 申请空白 PCB → 分配资源 → 初始化 PCB → 插入就绪队列
阻塞 请求服务未满足、等待操作完成、数据未到达、等待消息等 状态改阻塞 → 插入相应阻塞队列 → 转进程调度
唤醒 所等待的事件发生或资源可用 移出阻塞队列 → 改就绪态 → 插入就绪队列
撤销 正常结束、其他进程请求终止等;异常也可能导致结束 读取状态 → 若在执行则停止 → 按系统策略处理子孙进程 → 释放资源 → 移出 PCB

创建其他进程的进程称为父进程,被创建的称为子进程,同一父进程的子进程互称兄弟进程。父子进程可以并发执行,父进程也可等待子进程结束。地址空间及其他资源是复制、共享还是重新建立,取决于创建方式和系统实现。

“父进程结束,子进程必定终止”并非普遍规则。有些系统采用级联终止;在教材介绍的 UNIX 模型中,失去父进程的孤儿进程由 init 接管,子进程可以继续运行。另外,fork 用于创建新进程,execve 则用于替换当前进程执行的程序,两者作用不同。

5 操作系统内核与中断(4.1–4.3,识记、领会)

操作系统并非全部代码都需常驻内存。经常执行、直接支撑硬件与进程管理的核心模块组成内核,主要承担两类工作:

  • 支撑功能:中断处理、时钟管理、原语操作。
  • 资源管理功能:进程管理、存储器管理、设备管理。

原语是具有原子性的一段操作序列,其关键操作对其他执行流呈现为不可分割的整体。普通用户态代码不会仅因写在同一个函数中,就自动具有这种保证。

中断使 CPU 暂停当前顺序执行,转去处理事件。设备就绪后发出中断,CPU 可在等待期间执行其他任务,不必持续查询设备状态;这是支持多道程序的重要机制。

类型 来源 例子
同步中断/内部中断/异常 与当前指令执行有关 除零、溢出、调试等
异步中断/外部中断 外部硬件产生 I/O 完成、时钟等
外部可屏蔽中断 可受中断屏蔽控制 常见 I/O 中断
外部不可屏蔽中断 紧急硬件事件 某些硬件故障

“关中断”通常指屏蔽外部可屏蔽中断。清除中断允许位并不能屏蔽当前指令引发的内部异常,也不能屏蔽掉电、内存校验错等事件可能触发的不可屏蔽中断(NMI)。外部中断通常在指令执行结束时检查,内部异常则与当前指令的执行过程有关。

单重中断处理过程:本次处理期间不响应新的中断,教材假定处理后返回原断点。

整个过程分为硬件响应和软件处理两段。区分两段的执行主体,有助于记清各步骤的次序。

第一段:硬件的中断隐指令。全自动,不可被打断。

  1. 关中断。清除 PSW 中的中断允许位 IF,此后到达的可屏蔽中断只能排队等着。
  2. 保护断点。把当前 PC(下一条待执行指令的地址)和 PSW 压入内核栈。
  3. 按中断向量号索引中断向量表,取出该中断服务程序的入口地址(32 位 x86 上是 IDT)。
  4. 入口地址送 PC。程序计数器一改,CPU 下一条取的就是服务程序的第一条指令。

以上四步由硬件完成,不属于操作系统代码。中断控制器提供向量号后,硬件据此索引表项并取得入口地址;此时中断服务程序尚未开始执行。

第二段:软件的中断服务程序 ISR。

  1. 保护通用寄存器现场。断点 PC 和 PSW 已由硬件压栈,通用寄存器的值要软件自己存。
  2. 执行具体的设备服务,比如把网卡收到的帧搬进缓冲区、把键盘扫描码翻译成字符。
  3. 恢复通用寄存器现场。
  4. 执行中断返回指令(如 IRET)。它从栈里弹回 PSW 与 PC,处理器回到断点。

在这一单重中断模型中,中断允许状态随最后的返回操作恢复。IRET 从保存的 PSW 中恢复原进程的 IF 位,并恢复 PC,不应笼统写成“先开中断、再返回”。分开执行会留下中断已允许、返回过程尚未完成的时间窗口。教材将过程合为四步,本节为说明分工,展开为“硬件隐指令四步 + ISR 四步”。

口径说明。 教材 §6 把查中断向量排在保护现场之后,§7 又写 CPU 按中断号取得中断向量值,两处叙述次序不一致。本笔记正文按 §7 说明:硬件在第 3 步查表,取得入口后才执行软件服务程序。若题目要求教材 §6 的处理流程,应按该节四步作答;图2-8 采用正文口径,分为两个硬件框和两个软件框。

图2-8 单重中断处理流程

图2-8 单重中断处理流程

断点记录程序该从哪儿恢复执行,现场则是恢复执行所需的寄存器状态。中断向量标识中断源,中断描述符表 IDT 存着各服务程序的入口信息。教材 32 位 x86 实例里,表项地址是 IDT 基址+8×中断向量,8 字节正好一个表项——第 3 步硬件算的就是这个下标。

6 用户态、系统态与系统调用(4.4、4.5,识记、领会)

关中断、修改页表、读写磁盘扇区等操作会影响整个系统,因此需要硬件特权保护。教材区分两种 CPU 执行状态:用户态只能执行非特权指令、访问允许的用户空间;系统态(又称核心态)拥有执行内核代码所需的硬件特权。用户程序通过系统调用请求内核提供受保护的服务。

比较 用户程序的一般函数调用 系统调用
执行状态 通常保持用户态 进入系统态执行内核服务
入口方式 转移到函数入口 通过受控的陷入/系统调用入口
查找依据 函数地址 系统调用号等
开销 常规调用开销 另有执行态转换、现场处理等开销

一次系统调用通常经过以下步骤。具体入口和参数传递方式取决于处理器架构与操作系统;传统 x86 的 int 0x80 是一种陷入方式。

  1. 用户程序调用库函数,由库函数准备系统调用号和参数。
  2. 程序执行陷入指令,处理器切换到系统态。
  3. 内核保存所需状态,根据系统调用号分派到相应服务例程,例如处理 getpid() 的内核服务。
  4. 服务完成后,内核返回结果并恢复用户态执行。

库函数、系统调用和进程切换需要分开理解。strcpy() 这样的库函数可以完全在用户态执行;教材以 getpid() 为例说明系统调用时,进入内核的仍是当前进程自己,只是从用户态切换到系统态执行内核代码,完成后通常返回当前进程。只有调用导致阻塞,或返回前需要调度等情况下,才可能发生进程切换。

教材把系统调用归成五类:进程控制、文件操作、设备管理、信息维护和通信,作用都是把内核服务收拢到统一且带权限检查的入口后面。

7 时钟体系与时钟机制(4.6、4.7,识记、领会)

在抢占式系统中,时钟中断为时间片调度提供触发时机,使 CPU 密集型进程也能按规则让出处理器。操作系统还利用时钟维护日期时间、生成时间戳、处理定时报警,并统计各进程的 CPU 使用时间。

部分 作用
RTC/CMOS 时钟 电池供电,断电也不停,开机时供系统读取初始日期时间
OS 时钟 开机后由操作系统接手维护,教材用 PIT 周期中断来说明
PIT 可编程间隔定时器 由晶振、计数器、保持寄存器构成,周期性向 CPU 发时钟中断
时钟驱动程序 响应每次时钟中断,更新系统时间与各类定时计数

PIT 的硬件节奏很简单:晶振按固定频率吐出脉冲,每来一个脉冲计数器减 1;减到 0 就触发一次时钟中断,同时把保持寄存器里的初值重新装回计数器,周而复始。

图2-12 PIT 的逻辑结构

图2-12 PIT 的逻辑结构

每次时钟中断到来,时钟驱动程序主要处理四件事:更新日期和时间;递减当前进程的时间片余量,减到 0 时设置调度标志;累计当前进程的 CPU 使用时间;递减定时报警计数器。时间片用完后,进程让出 CPU 并返回就绪态,并不意味着进程被撤销。

例如,PIT 每秒产生 100 次中断,则一个滴答(tick)为 10 ms,50 ms 的时间片对应 5 个滴答。教材提到的 Linux 2.4 变量 jiffies 记录开机以来累计的滴答数,而非年月日时分秒。

8 线程(5.1、5.2,领会)

进程同时承担资源管理和执行任务。同一应用若需要并发接收网络数据、渲染画面和响应用户操作,为每项任务建立独立进程会增加地址空间、页表及切换开销。引入线程后,进程仍是拥有资源的基本单位,线程成为调度和分派的基本单位。

同一进程中的线程共享地址空间、打开的文件和全局变量;每个线程保留自己的 PC、寄存器和私有栈。因此,线程的创建、撤销及同一进程内的切换开销通常小于进程。

比较角度 进程 同一进程中的线程
基本角色 资源拥有的基本单位 执行、调度与分派的基本单位
地址空间 进程间通常相互独立 共享所属进程地址空间
通信 使用进程通信机制 可直接读写共享变量,但仍须同步互斥
并发 不同进程间可以并发 同一进程内不同线程也可以并发
开销 创建、撤销、切换开销较大 通常较小,尤其同进程线程切换

对应 PCB,每个线程也有自己的线程控制块 TCB,记着线程标识、寄存器状态、调度优先级和记账信息,照样走就绪、运行、阻塞三态转换,同状态 TCB 串成队列管理。

按“内核知不知道线程存在”,教材对比了两种纯粹的实现模型:

比较 用户级线程(教材的纯用户线程模型) 内核级线程
谁管理 用户空间线程库管,内核眼里只有外层进程 内核亲自建 TCB 并管理
谁调度 内核把 CPU 分给进程,线程库再在进程内部排班 内核直接按线程调度
切换 用户态内就能切完,不用陷入内核,快 要进出内核态,开销大些
阻塞式系统调用 一个线程发起阻塞调用,整个进程跟着一起挂起 只阻塞发起调用的那个线程,同进程其他线程照跑
CPU 时间分配 同进程的多个线程合分这一个进程的时间片 按线程拿时间片,线程多的进程分到的总时间更多
多处理器利用 内核只按进程派核,同进程线程没法在多核上真并行 同进程的多个线程可同时派到不同 CPU 核上并行

在本节的多对一用户级线程模型中,内核只管理外层进程。若一个线程发起阻塞式系统调用,整个进程可能被阻塞,其他用户级线程也无法运行。应注意这一结论的模型前提。

线程控制包括创建、终止、阻塞和唤醒:创建时建立 TCB 并加入就绪队列;终止时停止执行、移出队列并准备回收;阻塞时保存现场、加入阻塞队列并调度其他线程;唤醒时移回就绪队列。用户级线程的这些操作由用户态线程库完成,内核级线程则由操作系统内核完成。

9 同步与互斥的基本概念(6.1,领会)

并发访问共享数据需要避免冲突,合作任务也可能存在先后依赖,例如甲必须等乙产生结果后才能继续。同步机制处理这两类关系:让争用临界资源的进程互斥进入,以及让合作进程按约定次序协调推进(狭义同步)。

概念 含义
临界资源 一次只允许一个进程访问的共享资源,如打印机、共享计数器
临界区 进程中访问临界资源的那段代码
进入区 临界区前面检查并申请进入许可的代码
退出区 临界区后面归还许可、通知等待者的代码
互斥 同一临界资源任意时刻至多一个进程在内
协调/狭义同步 合作进程按逻辑先后推进,如生产者先放数据、消费者再取

互斥方案通过进入区申请访问临界资源的权限,在退出区释放权限。衡量方案时,主要看四条准则:空闲让进(临界区空闲时允许进入)、忙则等待(已有进程访问时,其他进程等待)、有限等待(申请者不能无限期等待)、让权等待(无法进入时让出 CPU,避免忙等)。

“忙则等待”和“让权等待”分别约束等待的条件与方式。前者要求临界资源已被占用时,其他申请者必须等待;后者要求等待者让出 CPU。整型信号量中的 while S <= 0: 忙等 满足前者,却一直占用 CPU,因此不满足后者。

10 信号量与 P/V 操作(6.2、6.3,综合应用)

信号量表示资源数量、使用状况或事件关系。除初始化外,通过原子的 P/V 操作修改(P 即 wait,V 即 signal);以下操作均针对共享的同一个信号量对象。

10.1 整型信号量

P(S):
    while S <= 0:
        忙等
    S = S - 1

V(S):
    S = S + 1

P 操作中的条件检查和资源取得需要由底层机制保证原子性。上述代码仅表示操作语义;若直接用普通循环和赋值实现,多核环境中仍可能出现竞争。互斥信号量初值为 1,N 个同类资源初值为 N。整型机制的问题是忙等,不满足让权等待。

10.2 记录型信号量

S = { value: 整数, L: 阻塞队列 }

P(S):
    S.value = S.value - 1
    if S.value < 0:
        把当前进程加入 S.L 并阻塞,放弃 CPU

V(S):
    S.value = S.value + 1
    if S.value <= 0:
        从 S.L 唤醒一个进程

记录型 P 操作先将 value 减 1,再根据结果是否小于 0 决定阻塞。V 操作则先加 1,结果小于等于 0 时唤醒等待者。阻塞的是调用 P 的当前进程;被唤醒的进程转为就绪,不等于立即取得 CPU,也不应重新执行一次已经完成扣减的 P。

  • value ≥ 0:表示当前可用资源数量。
  • value < 0:其绝对值表示该信号量队列中的等待进程数。教材相关句子省略绝对值,理解时需补全。
操作示例,初值 1 操作后 value 结果
A 执行 P 0 A 取得许可
B 执行 P −1 B 阻塞
C 执行 P −2 C 阻塞
A 执行 V −1 唤醒一个等待者
获许可者完成并 V 0 唤醒另一个等待者
最后一个完成并 V 1 无等待者,许可归还

10.3 两种基本建模

互斥:mutex = 1,每个访问同一资源的进程都执行 P(mutex) → 临界区 → V(mutex)。

先后关系:要求 A 的 S1 完成后,B 才能执行 S2,设 done = 0;A 执行 S1 → V(done),B 执行 P(done) → S2。V 位于产生事件的进程,P 位于等待事件的进程,两者配合表达这段先后约束。

写同步题时,先确定资源和先后关系,再定义信号量、写初值、标出临界区并安排 P/V。记录型信号量消除了忙等,但错误的 P/V 顺序仍可能造成死锁,也不保证公平。

教材另介绍 AND 型信号量,用一次操作同时申请多个资源;大纲 6.2、6.3 明确列出的应用重点是整型和记录型机制。

10.4 PV 建模五步

建立 PV 模型时,可按以下五步整理条件和操作:

  1. 列出资源与先后约束:按时间顺序写出各进程的动作,标出共享资源以及必须等待的事件。
  2. 定义信号量并设初值:互斥信号量初值为 1;资源计数按可用资源数设置;表示尚未发生事件的同步信号量初值为 0。
  3. 写出进程动作:暂不插入 P/V,先按执行顺序列出操作,并标明临界区。
  4. 插入 P/V 操作:先等待资源或事件,再申请互斥;先释放互斥,再发出资源或事件许可。具体次序仍以题设的并发条件为准。
  5. 检查死锁条件:检查是否有进程持有其他进程所需的资源,同时等待对方持有的资源;若有,调整申请次序或采用合适的集中申请方式。

先列动作顺序,再放置 P/V。这样可以把“进程要做什么”和“何时申请资源”分开检查。

11 两个经典同步模型(6.4,综合应用)

11.1 生产者—消费者

生产者把数据放入含 N 个槽位的缓冲池,消费者从中取出数据。双方共享缓冲区和读写指针,访问这些状态时需要互斥。缓冲池满时生产者等待,缓冲池空时消费者等待,这又构成资源数量上的同步约束。

信号量 含义 初值
mutex 缓冲池访问许可 1
empty 可申请的空缓冲区 N
full 可申请的消息 0

生产者循环:

在私有变量中生产 item
P(empty)
P(mutex)
buffer[in] = item
in = (in + 1) mod N
V(mutex)
V(full)

消费者循环:

P(full)
P(mutex)
从 buffer[out] 取到私有变量 item
out = (out + 1) mod N
V(mutex)
V(empty)
在临界区外消费 item

图2-18 生产者—消费者共享缓冲池

图2-18 生产者—消费者共享缓冲池

必须先申请资源,再申请互斥权。缓冲池已满时,若生产者先 P(mutex) 再 P(empty),它会持有 mutex 并等待空位。消费者此时取不到 mutex,也就取不走数据、无法释放空位。双方因此死锁。缓冲池为空时,消费者反过来也会遇到同样的问题。

生产和消费放在临界区外,临界区只保护缓冲区及共享指针。P/V 执行过程中,资源可能已被预留但尚未发布,因此不能在任意时刻用 \(\text{empty.value} + \text{full.value} = N\) 判断状态;稳定状态下,实际空位数和消息数之和才是 N。

11.2 读者—写者

多个读者可以同时读取数据;只要有写者访问数据区,就需要排除其他读者和写者。设 readcount = 0,rmutex = 1 保护计数器,wmutex = 1 控制共享数据区。

可以用自习室理解这几个变量:wmutex 控制整间数据区,readcount 记录读者人数,rmutex 只保护人数登记,避免多个读者同时修改计数。

第一个读者进入时,若 readcount == 0,便申请 wmutex,阻止写者进入;后续读者只需登记人数,可以并发读取。最后一个读者离开、使 readcount 减到 0 时,释放 wmutex。写者则在写入前申请 wmutex,完成后释放,全程独占数据区。

rmutex 只保护 readcount 的修改,不限制同时读取的人数。实际读操作应放在 rmutex 临界区外;否则读者会被串行化,失去并发读取的效果。

这类“计数器 + 互斥量”的结构也可用于分析通行问题,但不能直接照搬。遇到独木桥、单行桥或窄道题目时,应重新核对哪些对象可以并发、哪些必须互斥,以及是否存在容量或优先级限制,再决定计数器和 P/V 操作的位置。

读者:

P(rmutex)
if readcount == 0: P(wmutex)
readcount++
V(rmutex)

读共享数据

P(rmutex)
readcount--
if readcount == 0: V(wmutex)
V(rmutex)

写者:

P(wmutex)
写共享数据
V(wmutex)

第一个读者替整群读者申请数据区,最后一个读者归还;中间进出的读者完全不碰 wmutex,这就是读者能并发的来源。而 readcount 本身是所有读者共改的共享变量,增减前后那对 rmutex 一步也不能省。

注意这套写法是读者优先:只要屋里还有读者没走完,后来的读者就能一直插队进来,守在门外的写者可能长期饿死。题目若要求写者优先,就得再加一层拦住后续新读者的锁。

11.3 教材综合题:单缓冲区读文件并打印

pA 每次从磁盘读一条记录到公共缓冲区,pB 每次取出并打印一条;缓冲区只容一条。它是 \(N=1\) 的生产者—消费者问题,设 empty=1, full=0, mutex=1。

pA 循环:

从文件读记录到私有变量
P(empty)
P(mutex)
写入公共缓冲区
V(mutex)
V(full)

pB 循环:

P(full)
P(mutex)
从公共缓冲区复制到私有变量
V(mutex)
V(empty)
打印私有变量中的记录

与附录一致,取出到私有变量后就可以归还 empty,不必等待打印完成;如果直接从公共缓冲区打印,则必须保证打印结束前不会被覆盖。终止协议由题目另外约定,以上表达逐条交替的同步关系。

11.4 填空式 PV 专项

两卷的综合 36 均为代码填空:202410 为“火车订票”,10 空、每空 1 分;202504 为“售票”,5 空、每空 2 分。练习时需核对每次 wait、signal 的对象和次序。以下两题为自编,题型参考这两卷,题面可直接用于练习。

自编题一(自编,题型同 202410 综合 36,10 空):火车订票。 某车站有两个售票窗口 W1、W2,一个退票窗口 R 和一个统计进程 S,共用一个余票记录 remain,初值 100。任何时刻只允许一个进程访问 remain。售票窗口在余票为 0 时等待,退票窗口每退回 1 张就唤醒一个等待中的售票窗口,自己不必等待。统计进程每轮读一次 remain 并累加,读的时候同样不许别人改。请补齐下面程序中的 ①—⑩,一空只写一个 wait 或一个 signal。

semaphore mutex = ___①;      /* 保护余票记录 remain */
semaphore tickets = ___②;    /* 当前可售的余票数 */

售票窗口 Wi:
    while (1) {
        取到一个乘客请求;
        ___③;
        ___④;
        remain = remain - 1;
        打印车票;
        ___⑤;
    }

退票窗口 R:
    while (1) {
        收到一张退票;
        ___⑥;
        remain = remain + 1;
        ___⑦;
        ___⑧;
    }

统计进程 S:
    while (1) {
        收到一次统计命令;
        ___⑨;
        读出 remain 并累加;
        ___⑩;
    }

自编题二(自编,题型同 202504 综合 36,5 空):两个售票点卖同一种票。 两个售票点 A、B 和一个退票点 R 共用余票记录 remain,初值 200,任何时刻只允许一个进程访问;余票为 0 时售票点等待,退票点每退回 1 张就唤醒一个售票点。已知 semaphore tickets = 200;、semaphore mutex = 1;,退票点末尾的 signal(tickets) 已给出,只补 ①—⑤。

售票点 A:
    while (1) {
        ___①;
        ___②;
        售出一张车票,remain = remain - 1;
        ___③;
    }

退票点 R:
    while (1) {
        ___④;
        退回一张车票,remain = remain + 1;
        ___⑤;
        signal(tickets);
    }

填写本组 P/V 空格时,可依次检查以下三点:

  1. 本组练习用 mutex 保护共享记录,每条访问路径都应先 wait(mutex),退出临界区后再 signal(mutex)。检查正常分支与条件分支是否遗漏释放。
  2. 依据具体约束确定谁等待、谁发信号。例如,售票窗口等待余票,退票窗口增加余票后发出许可。不能推广为“同一信号量的 P/V 必须分处两个进程”;资源计数信号量也可能在同一进程的不同阶段被申请和归还。
  3. 题设限定每空只填一个 wait 或一个 signal。若候选答案不符合空格数量,应回查初值、已有语句和约束;空格数量本身不能证明模型正确。

参考答案

自编题一:
① 1              ② 100            ③ wait(tickets)   ④ wait(mutex)
⑤ signal(mutex)  ⑥ wait(mutex)    ⑦ signal(mutex)   ⑧ signal(tickets)
⑨ wait(mutex)    ⑩ signal(mutex)

自编题二:
① wait(tickets)  ② wait(mutex)    ③ signal(mutex)
④ wait(mutex)    ⑤ signal(mutex)

两题需避免两种错误:把 wait(mutex) 放在 wait(tickets) 之前,或在售票窗口执行后增加 signal(tickets)。前者会在无票时持有互斥量等待,使退票窗口无法进入,也就无法补回票;后者会增加并不存在的余票,使信号量计数与实际票数不符。

12 管程(6.5,领会)

信号量的 P/V 操作分散在各个进程中,遗漏操作或颠倒次序都可能导致死锁。管程将共享数据结构及其操作过程封装在同一模块中,外部进程只能通过入口过程访问;管程机制保证任意时刻最多只有一个进程在管程内部活跃执行,由此集中处理互斥。

条件不满足时,管程使用条件变量等待:x.wait() 阻塞当前进程,并释放管程的独占权;x.signal() 唤醒队列中的一个等待者。它与信号量的 V 操作不同:V 会使计数加 1,即使没有等待者也会保留这个计数;条件变量不保存许可,若调用 x.signal() 时队列为空,这次通知不会留给后来的等待者。

12.1 管程实现生产者—消费者

管程将缓冲池、计数器 count 以及 put(放入)、get(取出)过程封装在一起。教材的条件变量 full 和 empty 与 11.1 节信号量同名,但含义不同:信号量 full 表示可取消息数,条件变量 full 表示“缓冲池已满”这一等待条件:

  • 放入过程:见 count == N 就在 full 上 wait();有空位后写入数据、count 加 1,再调 empty.signal() 叫醒因池空而等待的消费者。
  • 取出过程:见 count == 0 就在 empty 上 wait();有数据后取出数据、count 减 1,再调 full.signal() 叫醒因池满而等待的生产者。

教材伪代码用 if 检查 count == N 和 count == 0,其唤醒语义见 12.2 节。采用 Java、C++、POSIX 条件变量编写实际代码时,应以 while 循环重新检查等待条件。

12.2 哲学家进餐

5 人围坐,每人进餐需要左右两支筷子。若所有人各拿一支再等另一支,就会死锁。教材管程改成“左右两支都空闲才一次拿走”:为每人设 thinking、hungry、eating 三种状态和条件变量 self[i],左右邻居下标分别是 \((i+4) \bmod 5\) 与 \((i+1) \bmod 5\)。

test(i):
    if state[i] == hungry and 左右邻居都不是 eating:
        state[i] = eating
        self[i].signal()

pickup(i):
    state[i] = hungry
    test(i)
    while state[i] != eating:
        self[i].wait()

putdown(i):
    state[i] = thinking
    test(左邻居)
    test(右邻居)

test 中的 if 仅负责检查并更新状态,可以保留;pickup 中的等待判断则应改用 while。这一区别与管程的唤醒语义有关。

语义 signal 之后 典型实现 等待处写什么
Hoare,signal-and-wait 唤醒者立刻让出管程,被唤醒者马上接手运行,条件此时必然成立 教材未命名语义;其 if 写法隐含 Hoare 式不可抢占假设(据代码推断) if 够用
Mesa,signal-and-continue 唤醒者继续把管程过程跑完,被唤醒者只回到就绪队列重新抢锁 Java、C++、POSIX 条件变量的实际实现 必须 while

Mesa 语义下,被唤醒者需要重新竞争管程锁。从发出 signal 到重新获得锁之间,其他线程可能已经改变状态,使等待条件再次失效。此外,实际条件变量实现还可能允许“虚假唤醒”,即没有对应通知也从 wait 返回。因此,返回后必须重新检查条件。

若实际代码只用 if,从 wait 返回后就会直接继续执行,即使 state[i] 已不再是 eating。教材的 if 写法依赖 Hoare 式唤醒后立即交权的假设;按教材模型作答时需说明这一前提,使用 Mesa 语义的实际代码则应写 while。

留意哲学家思考和吃饭都在管程外头,进管程只为改状态、验邻居。由于相邻两人绝不可能同时进入 eating 状态,这套方案根除了“人手一支筷子互等”的死锁;但若左右邻居轮番交替进餐,夹在中间的人仍可能长期轮不上,光靠这三段代码还不能保证绝对不饥饿。

13 进程通信(6.6,识记)

信号量和管程传递的只是“能不能走”的信号,真要在进程之间搬运成块的数据,得靠进程通信(IPC)机制。大纲点名要求识记共享存储器、管道和消息缓冲队列三项,教材另把消息传递作为大类一并交代:

方式 主要思想 注意
共享存储器 划出共享数据结构或共享内存区,各进程直接读写交换数据 系统只给场地,读写互斥与先后同步得由进程自己用 P/V 等机制兜底
消息传递 调用操作系统提供的 send/receive 原语收发格式化消息 分直接通信(点名发给目标进程)和间接通信(发往中间信箱)
管道 连接读写进程的特殊文件(pipe 文件),按先进先出的字节流传送 虽挂着“文件”名头,实则在内存缓冲区周转,不等于落盘保存;半双工、读空写满会阻塞
消息缓冲队列 发送方申请消息缓冲区填好正文,挂入接收方 PCB 下的消息队列 队列本身是临界资源,挂入和摘取同样离不开互斥信号量与消息计数信号量

消息缓冲区里一般装着四样东西:发送进程标识、消息长度、消息正文和指向下一缓冲区的指针;接收进程的 PCB 里则要相应配齐消息队列队首指针、保护队列的互斥信号量,以及统计队列里现有几条消息的资源信号量。

14 考核要求

本章共 25 条考核条目:识记 14 条、领会 8 条、综合应用 3 条。3 条综合应用集中在 6.2–6.4(整型与记录型信号量、生产者—消费者、读者—写者)。复习时先掌握进程组成、三态转换、单重中断处理流程及两类线程模型,再练习 PV 建模与代码填空,并区分管程条件变量和信号量是否保留计数、唤醒后是否需要重新检查条件。

15 教材本章习题

题目取自教材第 2 章“习题”(共 16 题),答案取自教材附录“参考答案”(全部收录)。

15.1 单项选择题

1. 下列选项中,操作系统提供给应用程序的接口是【 】。

  • A. 系统调用

  • B. 中断

  • C. 库函数

  • D. 原语

答案与解析

参考答案: A。系统调用是应用程序取得内核服务的程序接口;库函数可能只是对系统调用的封装。

2. wait(s) 操作中的 block(s, L) 阻塞的进程是【 】。

  • A. 申请资源的进程

  • B. 申请临界资源的进程

  • C. 调用 wait(s) 的进程

  • D. 提供 wait(s) 操作的系统进程

答案与解析

参考答案: C。执行 P 操作后发现资源不可用时,当前调用 P 的进程进入阻塞队列。

3. 在一个单处理机系统中存在 5 个进程,处于就绪队列中的就绪进程数最多为【 】。

  • A. 0 个

  • B. 4 个

  • C. 5 个

  • D. 6 个

答案与解析

参考答案: C。题目只问就绪队列,5 个进程可以全部处于就绪态;单处理机最多同时执行其中 1 个,并不意味着必须有 1 个执行态进程。

4. 若记录型信号量 s 初值为 2,当前值为 −1,则 s 的阻塞队列中有【 】个进程。

  • A. 0 个

  • B. 1 个

  • C. 2 个

  • D. 3 个

答案与解析

参考答案: B。记录型信号量值为负时,负值的绝对值表示阻塞队列中的进程数。

15.2 填空题

5. 进程是由正文段、___和___构成的实体。

答案与解析

参考答案: 用户数据段;进程控制块。

6. 进程的 3 种基本状态是执行态、___态和___态。

答案与解析

参考答案: 就绪;阻塞。

7. 整型信号量的值只能被___和___改变。

答案与解析

参考答案: wait 操作;signal 操作。

8. 临界区是指访问___的代码。

答案与解析

参考答案: 临界资源。

15.3 简答题

9. 什么是进程?说明进程与程序的区别和联系。

答案与解析

参考答案: 进程是允许并发执行的程序在某个数据集合上的运行过程,由正文段、用户数据段和 PCB 组成。程序是静态的指令集合,进程是动态的执行过程;程序可长期保存在外存,进程有创建和撤销;一个程序可以对应多个进程,一个进程通常对应一次程序执行。

10. 操作系统在什么时候创建进程?如何创建?

答案与解析

参考答案: 用户登录、作业调度、提供服务、应用请求时可创建。步骤为:申请空白 PCB → 分配资源 → 初始化 PCB → 插入就绪队列。

11. 时钟中断信号如何产生?时钟驱动程序有什么功能?

答案与解析

参考答案: PIT 的晶振产生固定频率脉冲,计数器随脉冲递减,减到 0 后产生时钟中断,并从保持寄存器重新装载计数值。时钟驱动程序维护日期时间,递减并检查当前进程时间片,记录 CPU 使用情况,递减报警计数器。

12. 请说明单重中断的处理过程。

答案与解析

参考答案: 硬件阶段先关中断(清 IF),把断点 PC 与 PSW 压栈,再按中断向量号索引中断向量表取出服务程序入口,入口送 PC;随后进入软件阶段,保护通用寄存器,执行设备服务,恢复现场,最后执行中断返回指令,由硬件原子恢复 PSW 与 PC。教材把这两段合成四步叙述,顺序上硬件响应在前、软件处理在后。

13. 进程具有哪些特征?

答案与解析

参考答案: 并发性、动态性、独立性、异步性、结构特征。

14. 什么是线程?为什么要引入线程?

答案与解析

参考答案: 线程是进程中的实体,是独立调度和分派的基本单位,拥有 PC、寄存器、栈等少量资源并共享所属进程资源。引入线程可以减少进程创建、撤销和切换的开销,提高并发程度。

15. 实现进程互斥的基本原理是什么?

答案与解析

参考答案: 把访问临界资源的代码置于临界区;进入前申请访问许可,临界区内一次只允许一个进程,退出时释放许可并唤醒等待者,同时满足空闲让进、忙则等待、有限等待、让权等待。

15.4 综合应用

16. 有两个进程 pA、pB 合作解决文件打印问题:pA 将文件记录从磁盘读入主存缓冲区,每次读一个记录;pB 将缓冲区内容打印出来,每次打印一个记录。缓冲区大小等于一个记录大小。请用记录型信号量的 wait(s) 和 signal(s) 保证正确打印,并写出同步代码。

答案与解析

参考答案: 设置 mutex 保护缓冲区,empty 表示空缓冲区,full 表示已有记录。初值为:

\[ \text{mutex.value} = 1,\qquad \text{empty.value} = 1,\qquad \text{full.value} = 0 \]

pA:

while 还有记录:
    从磁盘读一条记录到私有变量
    wait(empty)
    wait(mutex)
    将私有变量中的记录写入公共缓冲区
    signal(mutex)
    signal(full)

pB:

while 还有记录:
    wait(full)
    wait(mutex)
    从公共缓冲区复制一条记录到私有变量
    signal(mutex)
    signal(empty)
    打印私有变量中的记录

要点:三个信号量的含义和初值正确;生产者先申请 empty,消费者先申请 full;访问公共缓冲区时使用 mutex;取出记录后释放 empty。

16 闭卷复习与跨章联系(补充)

一、闭卷自答(合上书逐题口述)

  1. 顺序执行与并发执行在封闭性、可再现性上差在哪,为什么同一段程序并发执行结果可能不同?
  2. 进程由哪三部分组成,PCB 的四类信息各在什么时候用到?
  3. 三态之间的四条基本转换各由什么事件触发,被唤醒的进程一定在运行吗?
  4. 中断隐指令四步与中断服务程序四步分别是什么,开中断在哪一步完成、由谁完成?
  5. 用户级线程与内核级线程在被谁调度、阻塞式系统调用上的表现有什么差别?
  6. 记录型信号量 value 为负时,绝对值表示什么?在生产者—消费者模型中,若先申请互斥再等待空位或消息,为什么可能死锁?

二、本章易错

  1. 忙则等待管的是“进不去必须等”,让权等待管的是“等的时候要把 CPU 让出去”;整型信号量忙等满足前者,却违反后者(第 9 节)。
  2. 查中断向量表由硬件隐指令完成,开中断则随中断返回指令(如 IRET)恢复 PSW 一并原子完成,别写成软件里先开中断再返回(第 5 节)。
  3. 记录型信号量 P 操作阻塞的是调用它的当前进程;被 V 唤醒的进程只是回到就绪队列,不会立刻抢到 CPU,醒来后也不再重复扣减 value(第 10.2 节)。

三、本章骨架

层 一句话
记账单位 进程=正文段+用户数据段+PCB,PCB 是系统感知进程存在的唯一标志
状态 就绪、执行、阻塞,只走四条基本转换,唤醒只回到就绪
接管 内核用中断隐指令、时钟中断和系统调用控制执行过程
并发协作 信号量 P/V 表达互斥与先后,记录型消除忙等,管程用条件变量封装等待
拆分 线程是调度与分派单位,进程是资源拥有单位

四、跨章指路

  1. 与后续章节的衔接:就绪队列选谁上 CPU 见第 3 章;访存缺页触发阻塞等 I/O 见第 4 章;设备公共缓冲池的管理直接复用本章的资源信号量与队列互斥套路,见第 6 章。