408 · 操作系统 · 第 2 章(上)

进程与调度

这一页回答两个问题:多个程序怎么“同时”跑(进程、线程),CPU 只有一个时轮到谁用(调度)。进程是为了让并发可控而造出来的,线程是为了让切换更便宜,调度算法则是在“公平、快、不饿死”之间做取舍。

资源给进程,CPU 给线程引入线程后:进程是资源分配的基本单位,线程是调度的基本单位。没有线程的系统里,两个角色都由进程担任。
状态转换看“缺什么”就绪只缺 CPU;阻塞缺的是 CPU 以外的东西(I/O、资源、消息)。所以阻塞醒来只能回就绪,不能直接上 CPU。
调度算法看两件事按什么排队(到达 / 运行时间 / 优先级 / 响应比 / 轮转),能不能半路抢。饥饿、公平、响应快慢都由这两点推出来。
一 · 演进导图

每一步都在解决上一步留下的问题

进程与调度让多个程序共享一个 CPU:能暂停、能恢复、互不干扰,还要轮得公平
进程与线程:谁来用 CPU从“程序”到“进程”再到“线程”

程序顺序执行

封闭性、可再现
做法
一个程序独占 CPU 和全部资源,跑完再换下一个
新问题
程序做 I/O 时 CPU 只能干等,利用率很低

多道程序并发

失去封闭性不可再现
做法
内存里放多道程序,一道去做 I/O,CPU 立刻转去执行另一道
新问题
程序们共享资源、走走停停,一道的结果会受另一道影响,同样输入可能得出不同结果。“程序”只是一段静态代码,描述不了“执行到哪儿、占着什么”

引入进程

PCB 是唯一标志
做法
给每道运行中的程序配一个 PCB,记下状态、CPU 现场、占有的资源。进程 = PCB + 程序段 + 数据段,是程序的一次执行过程
解决
能随时暂停、保存现场、再恢复,并发变得可描述、可控制
新问题
进程既拥有资源又被调度。每次切换都要换地址空间、换整套资源,时空开销大

引入线程

调度单位
做法
把“拥有资源”和“被调度”拆开:进程只管资源,线程只管上 CPU 执行。同一进程的线程共享进程的资源
好处
同一进程内线程切换不用换地址空间,开销小;一个进程内部也能并发
代价
线程共享数据,要做同步(见下一页“进程同步”)
处理机调度:CPU 给谁用就绪进程多于 CPU,就得有规则

三级调度

做法
作业调度决定谁进内存,内存调度决定谁暂时换出,进程调度决定谁上 CPU
要选
进程调度最频繁,也是必须有的一级。选谁,就是下面这些算法

先来先服务 FCFS

不饥饿
做法
按到达顺序,一个跑完再下一个
新问题
短作业排在长作业后面要等很久,平均周转时间大

短作业优先 SJF / SRTF

会饥饿
做法
运行时间(或剩余时间)短的先跑
效果
平均等待、平均周转时间最短
新问题
短作业不断到来,长作业一直轮不到

高响应比优先 HRRN

不饥饿
做法
响应比 = (等待 + 要求服务) ÷ 要求服务。短的先上,等久了的长作业响应比也会涨上来
新问题
非抢占,交互进程要等别人跑完,响应慢

时间片轮转 RR

不饥饿
做法
每个进程轮流跑一个时间片,没跑完就回队尾
解决
分时系统里每个用户都能很快得到响应
新问题
不分轻重缓急;时间片大小难定

多级反馈队列

会饥饿
做法
多个队列,优先级逐级降低、时间片逐级变长;新进程进最高级,用完时间片就降级
效果
短进程、交互进程在高级队列里很快完成,长进程在低级队列拿大时间片
代价
新进程源源不断时,低级队列里的进程会饥饿

下一步 → 进程同步:进程、线程并发地读写共享数据,结果会出错。于是要有临界区、信号量(P/V 操作)和管程。

二 · 进程的组成与 PCB

操作系统靠 PCB 认识一个进程

创建进程就是创建 PCB,撤销进程就是撤销 PCB。操作系统管进程,实际是在管一张张 PCB。

进程实体 = PCB + 程序段 + 数据段

  • PCB:给操作系统用的,记录管理进程所需的全部信息。进程存在的唯一标志。
  • 程序段:要执行的代码。多个进程可以运行同一个程序(共享同一程序段)。
  • 数据段:进程处理的原始数据、中间结果、全局变量等。

进程和进程实体的区别:进程实体是静态的“那堆东西”,进程是它的一次运行过程,是动态的。

进程的特征

  • 动态性(最基本):有创建、有消亡,状态不断变化。
  • 并发性:多个进程在一段时间内同时推进。
  • 独立性:独立运行、独立获得资源、独立接受调度(未引入线程时)。
  • 异步性:各自以不可预知的速度推进,所以需要同步机制。

用 C 结构体看 PCB 里有什么

typedef struct PCB {
    int  pid, uid;        // 描述信息:进程 ID、用户 ID
    int  state;           // 控制管理:当前状态
    int  priority;        // 控制管理:优先级
    int  cpu_time;        // 控制管理:已占用 CPU 时间
    Context ctx;           // 处理机相关:通用寄存器、PC、PSW、栈指针
    void *code, *data;     // 资源清单:程序段、数据段位置
    void *stack;           // 资源清单:栈
    File *files[NOFILE];   // 资源清单:打开的文件、I/O 设备
    struct PCB *next;      // 链入就绪队列 / 某个阻塞队列
} PCB;

切换进程时,CPU 里的寄存器值被存进旧进程的 ctx,再从新进程的 ctx 恢复,这就是“保存现场 / 恢复现场”。

进程描述信息进程标识符 PID、用户标识符 UID
进程控制和管理信息当前状态、优先级、代码入口地址、外存地址、进入内存时间、CPU 占用时间、信号量使用
资源分配清单代码段 / 数据段 / 堆栈段指针,文件描述符,键盘、鼠标等设备
处理机相关信息通用寄存器、地址寄存器、控制寄存器、标志寄存器(PSW)的值,即 CPU 现场
PCB 组织 · 链接方式同一状态的 PCB 链成一个队列:执行指针、就绪队列指针、阻塞队列指针(阻塞队列常按阻塞原因分成多条)
PCB 组织 · 索引方式按状态建索引表(就绪表、阻塞表),表项指向对应 PCB
三 · 五状态模型

点一个事件,看它走哪条边

记状态转换不要背箭头,问一句:这时进程缺什么?只缺 CPU 就是就绪,缺别的就是阻塞。再问一句:这个动作谁能做?只有在 CPU 上执行的进程才能“主动”做事。

✕ 不存在 ✕ 不存在 ✕ 不存在 创建完成 被调度 时间片用完 / 被抢占 请求 I/O · 等待事件 I/O 完成 · 事件发生 运行结束 创建态 就绪态 运行态 终止态 阻塞态 单 CPU:运行态最多 1 个;就绪、阻塞可以有很多个(各自排成队列)

合法转换(点事件)

非法转换(为什么不可能)

五个状态各缺什么

运行 正在 CPU 上执行

就绪 万事俱备,只缺 CPU

阻塞 在等 I/O、资源或消息,给它 CPU 也跑不了

创建 正在建 PCB、分资源,还没进就绪队列

终止 正在回收资源、撤销 PCB

四 · 进程控制与进程通信

状态转换靠原语完成,进程之间靠 OS 搭桥说话

原语是一段不可中断的程序(执行期间关中断)。状态转换要同时改 PCB 和移动队列,做到一半被打断,PCB 状态和它所在的队列就对不上了。

原语对应转换做了什么(按顺序)什么时候用
创建无 → 创建 → 就绪申请空白 PCB → 分配资源(内存不够就停在创建态)→ 初始化 PCB → 插入就绪队列用户登录、作业调度、系统提供服务、应用请求(如 fork())
终止(撤销)运行 / 就绪 / 阻塞 → 终止按 PID 找到 PCB → 若在运行,剥夺 CPU 给别人 → 终止其所有子孙进程 → 归还全部资源 → 删除 PCB正常结束、异常结束(越界、非法指令)、外界干预(用户杀进程)
阻塞 block运行 → 阻塞找到 PCB → 保护现场,状态改为阻塞 → 插入该事件的阻塞队列请求资源失败、等 I/O 完成、等合作进程的数据。由进程自己调用
唤醒 wakeup阻塞 → 就绪在阻塞队列中找到 PCB → 移出,状态改为就绪 → 插入就绪队列等的事件发生了。由别的进程或中断处理程序调用
切换运行 ↔ 就绪 等把 CPU 现场存入旧 PCB → 改旧 PCB 状态并移入相应队列 → 选新进程,改其 PCB 状态 → 更新内存管理数据结构 → 恢复新进程现场时间片到、有更高优先级进程到达、当前进程阻塞或终止

block 和 wakeup 必须成对出现:进程因为某事件阻塞,就得有别的进程在事件发生时唤醒它,否则它永远睡下去。

进程通信的三种方式

共享存储

OS 在内存里划一块两个进程都能访问的空间,进程直接读写。

  • 低级方式:共享一种数据结构,格式受限,速度慢
  • 高级方式:共享一块存储区,格式随意,速度快
  • 访问要互斥,由进程自己用 P/V 等同步工具保证;OS 只负责提供空间和工具

消息传递

以格式化消息(消息头 + 消息体)为单位,用 OS 提供的 send / receive 原语收发。

  • 直接通信:发送方把消息直接挂到接收进程的消息缓冲队列上
  • 间接通信:先发到中间实体信箱,接收方再从信箱取。也叫信箱通信

管道

连接读写进程的一个特殊共享文件(pipe 文件),本质是内存里一块固定大小的缓冲区。

  • 半双工:同一时刻只能单向传。要双向通信就建两根管道
  • 写满时写进程阻塞,读空时读进程阻塞
  • 数据一旦被读走就从管道里消失,所以一根管道不适合多个读进程
  • 读写互斥由 OS 保证
五 · 线程

把“拥有资源”和“上 CPU”拆开

线程是一个轻量级的执行单元:自己只有很少的私有部分(够它在 CPU 上跑就行),其余资源都向所在进程借用。ULT 和 KLT 的全部区别,都来自一个问题:内核知不知道这个线程存在。

对比项用户级线程 ULT内核级线程 KLT
谁管理应用程序里的线程库,内核看不到线程,只看到进程操作系统内核,内核为每个线程建 TCB
线程切换要不要进内核不需要 在用户态完成,开销小需要 要从用户态切到核心态,开销较大
一个线程阻塞整个进程阻塞 内核以为是进程在等 I/O只阻塞它自己 同进程其他线程照常运行
能否多核并行不能 内核把 CPU 分给进程,一个进程同一时刻只在一个核上能 多个线程可分到多个核
调度单位 / CPU 分配单位进程内核级线程

多线程模型:用户级线程怎么映射到内核级线程

多对一 用户空间内核空间 UUU K 一对一 UUU KKK 多对多(n ≥ m) UUUU KK U = 用户级线程 K = 内核级线程

多对一

多个 ULT 映射到 1 个 KLT。

  • 优:切换在用户态完成,开销小
  • 缺:一个线程阻塞,整个进程阻塞;不能多核并行

一对一

每个 ULT 对应一个 KLT。

  • 优:一个阻塞,别的照跑;可多核并行,并发能力强
  • 缺:每建一个用户线程就要建一个内核线程,线程管理要进内核,开销大

多对多

n 个 ULT 映射到 m 个 KLT(n ≥ m)。

  • 折中:克服多对一并发度低,又不像一对一那样每个线程都占内核线程

同一进程的线程:共享什么,私有什么

归属内容为什么
共享(属于进程)地址空间:代码段、数据段(全局变量、静态变量)、堆;打开的文件;I/O 设备等其他资源资源分配的单位是进程,线程只是借用
私有(属于线程)线程 ID 和 TCB、寄存器(含 PC)、栈(局部变量、函数调用链)、线程状态每个线程要独立执行,执行到哪里(PC)、用到哪些临时值(寄存器、栈)必须各有一份
同进程内线程切换不引起进程切换,只换寄存器和栈,开销小
不同进程的线程切换会引起进程切换(换地址空间),开销和进程切换一样
线程也有状态就绪、运行、阻塞,和进程一样转换
线程不拥有系统资源但可以访问所属进程的全部资源;线程也能创建和撤销其他线程
六 · 处理机调度的基本概念

三级调度、什么时候能调度、用什么指标评价

层级做什么在哪之间频率引起的状态变化
高级调度(作业调度)从外存后备队列选作业调入内存,建立进程。每个作业只调入一次、调出一次外存 → 内存最低无 → 创建 → 就绪
中级调度(内存调度)把暂时不能运行的进程调到外存等待(挂起),内存宽裕时再调回(激活)。目的是提高内存利用率和系统吞吐量内存 ↔ 外存(对换区)中等挂起态 ↔ 就绪 / 阻塞
低级调度(进程调度)从就绪队列选一个进程,把 CPU 分给它。最基本、不可缺少内存 → CPU最高(几十毫秒一次)就绪 → 运行

七状态模型:多了两个挂起态

  • 就绪挂起:本来就绪,但被换到外存。激活后回到就绪。
  • 阻塞挂起:本来阻塞,被换到外存。等的事件发生了,变成就绪挂起(还在外存)。
  • 挂起和阻塞的区别:挂起是进程映像被调到外存;阻塞是在等事件,映像可以还在内存。
  • 挂起由中级调度负责,这就是“中级调度”对应的状态。

调度方式

  • 非抢占式(非剥夺):只有当前进程主动放弃(结束、阻塞)才调度。开销小,适合早期批处理;来了紧急任务也只能干等。
  • 抢占式(剥夺):有更重要的进程需要 CPU 时,立即暂停当前进程。要遵循优先权、短进程优先、时间片等原则。适合分时、实时系统。
  • 闲逛进程(idle):没有就绪进程时,调度程序选它运行。优先级最低,只要有进程就绪就让出 CPU;不需要 CPU 以外的资源,不会被阻塞;执行中周期性检查中断。

什么时候需要调度

  • 当前进程主动放弃:正常终止、异常终止、主动请求 I/O 或资源而阻塞
  • 当前进程被动放弃:时间片用完、更高优先级进程进入就绪队列、有更紧急的事要处理(如 I/O 中断)

什么时候不能调度与切换

  • 处理中断的过程中:中断处理与具体进程无关,逻辑上也不能被打断
  • 进程在操作系统内核程序临界区中:内核临界区(如就绪队列)没访问完就切走,内核数据会乱
  • 其他需要完全屏蔽中断的原子操作过程中:如加锁、解锁、中断现场保护与恢复、原语执行中

这些情况下即使出现了调度条件,也要置请求调度标志,等过程结束后再调度。

普通临界区里能不能调度?

能。进程在访问打印机这类普通临界资源时,访问可能很慢。如果不让调度,CPU 会一直空等打印机。只有内核程序的临界区才不能被打断,因为它通常很短、访问的是内核数据结构,而且直接影响 OS 管理工作。

评价指标

CPU 利用率忙碌时间 ÷ 总时间总时间包括 CPU 空闲的时间
系统吞吐量完成的作业数 ÷ 总时间单位时间内完成多少道作业
周转时间完成时间 − 到达(提交)时间包括在外存后备队列、就绪队列等待,运行,以及做 I/O 的全部时间
带权周转时间周转时间 ÷ 实际运行时间一定 ≥ 1,越接近 1 说明等得越少
等待时间周转时间 − 运行时间(− I/O 时间)在就绪队列里等 CPU 的时间总和
响应时间首次产生响应 − 提交请求交互式系统最看重它

调度算法改变不了一个作业的运行时间和 I/O 时间,只能改变它在就绪队列里等多久。所以比较算法时,看等待时间最直接。

七 · 调度算法对比

按什么排队 × 能不能抢

选择题最常考:哪个会饥饿、哪个是抢占式、哪个对长作业/短作业有利、哪个适合分时。表里每一格都能从“排队依据”推出来。

算法排队依据抢占性会饥饿吗优点缺点适用
FCFS 先来先服务到达先后非抢占不会公平、实现简单对短作业不利(排在长作业后面);对 I/O 繁忙型不利作业调度、进程调度都可;有利于长作业、CPU 繁忙型
SJF / SPF 短作业(进程)优先要求运行时间最短非抢占会平均等待时间、平均周转时间最短(所有作业同时到达时最优)对长作业不利;运行时间是用户估计的;不考虑紧迫程度作业调度、进程调度
SRTF 最短剩余时间优先剩余运行时间最短抢占(新进程到达时比较)会平均等待、平均周转比 SJF 更短同 SJF,且切换更频繁进程调度
优先级调度优先级(本页约定:数字小 = 优先级高)抢占、非抢占均可会能区分紧急程度,适合实时系统静态优先级下,低优先级进程可能一直等作业调度、进程调度
HRRN 高响应比优先响应比 = (等待 + 要求服务) ÷ 要求服务,取最大非抢占不会兼顾长短作业:等待相同时短的先上;服务时间相同时等得久的先上;长作业等久了响应比会升高每次调度都要算一遍所有进程的响应比作业调度、进程调度
RR 时间片轮转到达顺序 + 时间片抢占(时间片到时由时钟中断剥夺)不会公平、响应快切换有开销;不区分紧急程度分时系统;只用于进程调度
多级队列按进程类型固定分到不同队列,队列间有固定优先级队列间通常抢占会不同类型进程用不同策略(如前台 RR、后台 FCFS)进程不能换队列,不灵活进程调度
多级反馈队列队列优先级 + 各队列内 FCFS / RR,进程可降级抢占(高级队列来新进程时)会兼顾公平、短作业优先、响应快,不必估计运行时间新进程不断到来时,低级队列饥饿通用,进程调度

RR 的时间片怎么定

  • 太大:每个进程在一个时间片内都能跑完,RR 就退化为 FCFS,响应时间变长。
  • 太小:进程切换太频繁,CPU 大量时间花在保存 / 恢复现场上,真正干活的比例下降。
  • 一般要让切换开销占比不超过 1%。选择依据:系统响应时间要求、就绪队列中的进程数、系统处理能力。

多级反馈队列的规则

  • 设置多级就绪队列,优先级从高到低,时间片从小到大(通常逐级加倍)。
  • 新进程到达,先进第 1 级队列的队尾,按 FCFS 等待。
  • 时间片用完还没结束,降到下一级队列的队尾;已在最低级的,放回最低级队尾(最低级按 RR)。
  • 只有第 1 ~ k−1 级都空了,才调度第 k 级的进程。
  • 第 k 级进程运行时,更高级队列来了新进程,就抢占它;被抢占的进程放回原队列末尾,不降级。
八 · 动手算 · 调度甘特图模拟器

同一组进程,七种算法各排出什么样的甘特图

默认数据是王道教材的经典例题(P1 0/7、P2 2/4、P3 4/1、P4 5/4),可以对着书核对。进程表可以直接改,改完立刻重算。单步看每一段为什么轮到它。

进程到达时间运行时间优先数
数字小 = 优先级高
    CPU 空闲(运行闲逛进程)↑P2 进程到达时刻平均值保留 2 位小数
    九 · 易错速记 & 速查

    选择题直接对表

    情境能否进行进程调度 / 切换一句话原因
    正在处理中断不能中断处理不属于任何进程,也不能被打断
    进程在内核程序临界区中不能内核数据(如就绪队列)改到一半,切走会乱
    原子操作 / 原语执行中不能原语要求不可分割,执行时关中断
    进程在普通临界区(如使用打印机)能不让调度 CPU 就会陪着慢速设备空等
    进程请求 I/O 而阻塞能(必须)主动放弃 CPU
    时间片用完 / 更高优先级进程到达(抢占式)能被动放弃 CPU
    系统调用进入核心态不一定模式切换(用户态 → 核心态)不等于进程切换
    周转时间 = 完成 − 到达不是“完成 − 开始”。“完成 − 开始”只在没被打断时等于运行时间。等待时间 = 周转 − 运行。
    带权周转 = 周转 ÷ 运行分母是实际运行时间,不是周转也不是等待。结果一定 ≥ 1。
    SJF 平均等待最短,有前提所有进程同时到达时,非抢占 SJF 的平均等待、平均周转最短。到达时间不同时,SRTF 的平均值通常更小。
    HRRN 不会饥饿长作业等得越久,响应比越高,总会被选中。它兼顾长短作业,本身是非抢占的。
    多级反馈队列会饥饿高级队列一直有新进程,低级队列的进程就一直轮不到。
    中断处理中不能切换进程内核临界区、原语执行中同样不能;普通临界区可以。
    调度次数 ≠ 时钟中断次数时钟中断每个时钟周期都来,一个时间片可能包含多个时钟周期;进程阻塞、结束引起的调度也不经过时钟中断。所以不能用一个去数另一个。
    阻塞 → 运行、就绪 → 阻塞都不存在唤醒后只能进就绪;只有运行中的进程才能主动请求 I/O 而阻塞。
    PCB 是进程存在的唯一标志程序段可被多个进程共享,所以程序段不是唯一标志。
    资源给进程,CPU 给线程同进程内线程切换不引起进程切换;ULT 下调度单位仍是进程,一个线程阻塞整个进程阻塞。
    线程私有:寄存器、栈、TCB全局变量、堆、打开的文件、代码段都是同进程线程共享的。
    RR 时间片太大退化为 FCFS太小则切换开销过大。RR 只能用于进程调度。