Post

处理器管理

处理器管理

阅读指南:本章是操作系统的核心机制章节。知识链是线性的:硬件结构 → 中断机制 → 进程抽象 → 线程优化 → 调度算法。每一层都建立在上一层之上,建议严格按顺序阅读。

1. 处理器基础

1.1 CPU 的内部结构

理解 CPU 的结构,是理解”中断如何工作”和”进程切换如何发生”的前提。

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────┐
│                     CPU                             │
│                                                     │
│  ┌──────────┐    ┌──────────┐    ┌───────────────┐  │
│  │  控制单元 │    │   ALU    │    │  寄存器组      │  │
│  │  (CU)    │    │ 算术逻辑 │    │ (PC/IR/PSW...) │  │
│  └──────────┘    └──────────┘    └───────────────┘  │
│            ↕           ↕               ↕            │
│                  内部总线                            │
└─────────────────────────────────────────────────────┘
                       ↕
                  系统总线(连接内存、I/O 设备)

1.2 寄存器分类

CPU 的寄存器分为两大类,这一分类直接对应”用户模式”和”内核模式”的权限边界。

用户可见寄存器

所有程序(用户态和内核态)均可读写:

  • 通用数据寄存器:暂存运算操作数和结果(x86: EAX, EBX…)
  • 地址寄存器
    • 索引寄存器:支持数组/循环的地址计算
    • 栈指针(SP):指向当前栈顶
    • 段地址寄存器:存放段基址(段式管理中使用)

控制与状态寄存器

仅特权程序(OS 内核)可以修改

寄存器全称作用
PCProgram Counter下一条将执行的指令地址
IRInstruction Register当前正在执行的指令
CCCondition Code上次运算的结果标志(零/负/进位/溢出)
标志位中断允许/屏蔽位、处理器模式位、内存保护信息

1.3 程序状态字(PSW)

PSW(Program Status Word) 是操作系统概念层面的核心数据结构,代表进程在某一时刻的”运行快照”。

1
2
3
4
PSW 包含:
┌───────┬──────┬──────┬──────────┬──────────┬────────┐
│  PC   │  IR  │  CC  │ 中断允许 │ 处理器模式│ 内存保护│
└───────┴──────┴──────┴──────────┴──────────┴────────┘

关键认识:进程切换的本质就是保存旧进程的 PSW、恢复新进程的 PSW。PSW 是进程执行状态的完整摘要。

在某些硬件上,PSW 对应一个专用硬件寄存器(如 IBM System/390);在 x86 上,它被拆分到多个寄存器(EFLAGS, CS, EIP 等)。

1.4 指令执行流程

一条机器指令的执行遵循固定的三步循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────────────────┐
│                   指令执行周期                            │
│                                                         │
│  ①取指  →  ②解码  →  ③执行                              │
│  PC→IR     ID分析    ALU运算                             │
│  PC+1      确定操作数  写回结果                           │
│                       更新PC/CC                         │
│                         ↓                              │
│                   ④检查中断  ← 这是中断发生的时机!        │
│                    ↓          ↓                         │
│                  无中断      有中断                       │
│                  继续①      跳入中断处理                  │
└─────────────────────────────────────────────────────────┘

关键细节:”检查中断”这个微操作被插入在每条指令的执行周期末尾,而不是任意时刻。这意味着中断只在指令边界处被响应,保证了指令的原子性。

1.5 指令流水线

问题:取指、解码、执行三个阶段,每个阶段使用不同的硬件单元。若顺序执行,大部分时间硬件处于空闲状态。

解决:流水线——让多条指令的不同阶段在同一时刻并发执行:

1
2
3
4
5
时间轴 →
指令 1:[取指][解码][执行]
指令 2:       [取指][解码][执行]
指令 3:              [取指][解码][执行]
指令 4:                     [取指][解码][执行]

工程代价:流水线引入了”冒险”问题(数据冒险、控制冒险、结构冒险),中断发生时需要清空流水线——这是中断响应有一定延迟的原因之一。

1.6 处理器模式

核心问题:如何防止用户程序破坏 OS 内核或其他进程?

解决方案:处理器支持不同的执行模式,在不同模式下,指令的执行权限不同。

1
2
3
4
Ring 0(内核模式):可执行所有指令,包括特权指令
Ring 1
Ring 2
Ring 3(用户模式):只能执行非特权指令

实践中大多数 OS(Linux、Windows)只使用 Ring 0 和 Ring 3 两级。

特权指令的例子

  • 启动/停止 I/O 设备
  • 修改 PC(直接跳转到任意地址)
  • 修改内存保护寄存器
  • 关中断/开中断
  • 访问控制寄存器(CR0、CR3 等)

为什么用户程序不能执行这些? 若用户程序可以随意修改内存保护寄存器,它就能访问任意进程的内存;若能关中断,它就能阻止 OS 夺回 CPU 控制权。特权机制是 OS 安全的硬件基础。

1.7 模式切换

处理器模式的切换是受控的,不能由程序随意触发。

用户模式 → 内核模式(只有三种触发方式):

触发方式例子
中断(外部)I/O 完成、时钟中断
异常(内部)除零、缺页、越界访问
系统调用(主动陷入)read(), write(), fork()

内核模式 → 用户模式

  • 只能通过执行特殊的”中断返回指令”(x86: iret)完成
  • 该指令同时恢复 PC 和 PSW,保证原子性切换

工程意义:这种单向受控的切换机制,使得”进入内核”的路径是完全可控的——OS 知道用户程序只能通过特定入口(系统调用表、中断向量表)进入内核,而无法跳到任意内核地址。


2. 中断管理

2.1 中断的地位

“操作系统是中断驱动的。” ——中断是激活 OS 的唯一方式。

OS 并不是一个持续运行的程序,它更像一个”被动响应者”:

1
2
3
4
5
平时:用户程序运行,OS 静止
        ↓ 发生中断(I/O完成 / 时钟 / 系统调用 ...)
OS:被激活,处理事件,再选择下一个程序运行
        ↓ 执行中断返回
用户程序:恢复运行

没有中断,OS 就无法在用户程序运行时夺回 CPU 控制权,多道程序设计也就无从实现。

2.2 中断的三大类型

广义中断(中断 = 一切需要 CPU 暂停当前任务去处理的事件)在具体实现中分为三类:

类型来源与当前指令的关系典型例子
中断(狭义)处理器外部硬件无关(异步)I/O 中断、时钟中断、键盘输入
异常当前指令执行本身引发有关(同步)除零、缺页、非法指令、越界
系统调用程序主动执行陷入指令有关(同步,主动)read(), fork(), exit()

同步 vs 异步

  • 同步:中断与当前执行的指令直接相关,可重现(同样的程序序列必然在同一位置触发)
  • 异步:中断由外部事件引发,与当前指令无关,发生时间不可预测

2.3 各类中断的处理原则

处理器硬件故障中断

  • 触发:CPU、内存、总线等硬件物理故障
  • 处理:保护现场 → 停机 → 向操作员报告
  • 注意:这类中断优先级最高,因为硬件故障可能损坏数据

程序性中断(异常)

异常类型触发条件处理方式
算术异常除零、浮点溢出通知用户程序,允许用户自定义处理
指令异常非法指令、越界访问、权限违反终止进程(不可恢复)
虚拟地址异常访问不在主存的页(缺页)调入页面后重新执行该指令

为什么缺页后要重新执行,而不是继续执行? 因为引发缺页的那条指令没有完成——它的内存访问失败了。只有调入页面、让访问成功,这条指令才算执行完毕。

系统调用(自愿性中断)

1
2
3
4
5
6
7
8
9
用户程序执行 INT n / SYSCALL 指令
        ↓
CPU 模式切换到内核态
        ↓
OS 读取调用号(功能号)
        ↓
查系统调用表,跳转对应处理函数
        ↓
处理完毕,返回结果,切换回用户态

系统调用是用户程序主动请求 OS 服务的标准接口。它与普通函数调用的核心区别:系统调用会触发模式切换,进入内核执行。

I/O 中断

1
2
3
4
5
I/O 设备完成操作 → 向 CPU 发送中断信号
        ↓
OS 检查 I/O 状态
  ├── 成功 → 唤醒等待该 I/O 的进程(进入就绪队列)
  └── 失败 → 重试 or 向用户报告错误 or 人工干预

外部中断

  • 时钟中断:实现时间片轮转的基础,OS 借此定期夺回 CPU 控制权
  • 键盘/鼠标:将输入事件通知 OS
  • 关机/重启信号:触发系统清理流程

2.4 中断系统的实现:硬件与软件协同

中断系统分为两个子系统,严格分工:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──────────────────────────────────────────────────┐
│              中断系统                             │
│                                                  │
│  硬件子系统(中断响应)                            │
│  ─────────────────────────                       │
│  · 检测中断信号                                   │
│  · 判断是否屏蔽                                   │
│  · 选出最高优先级中断                              │
│  · 保存 PC/PSW 到核心栈                           │
│  · 跳转到中断处理程序入口                          │
│                                                  │
│  软件子系统(中断处理)                            │
│  ─────────────────────────                       │
│  · 保存其余寄存器状态                              │
│  · 识别中断源(分析中断码)                        │
│  · 查中断向量表,调用具体处理子程序                 │
│  · 恢复现场,决定返回哪个进程                      │
└──────────────────────────────────────────────────┘

硬件响应过程(自动完成,无需软件介入)

1
2
3
4
5
6
7
8
9
① 在指令执行周期末尾检测中断信号
        ↓
② 判断该中断是否被屏蔽(屏蔽位 = 1 → 忽略)
        ↓
③ 选择优先级最高的待处理中断
        ↓
④ 将当前 PC 和 PSW 压入核心栈(保护断点)
        ↓
⑤ 从中断向量表取出处理程序入口地址,跳转

中断向量表:一个固定地址的数组,每个中断号对应一个处理程序的入口地址。硬件直接用中断号作为索引查表,无需软件参与寻址。

软件处理过程(中断处理程序)

1
2
3
4
5
6
7
8
9
① 保存硬件未自动保存的寄存器(通用寄存器等)
        ↓
② 分析 PSW 中的中断码,确定中断来源
        ↓
③ 调用对应的中断处理子程序(查中断向量表)
        ↓
④ 处理完毕,决定恢复方向:
   ├── 直接返回被中断进程(无状态转换)
   └── 调整进程队列 + 触发进程调度

2.5 多中断处理

中断屏蔽

通过设置屏蔽位(mask bit),OS 可以在处理某个中断时,选择性地忽略其他中断。

1
2
3
中断屏蔽寄存器:每位对应一类中断
  bit = 1 → 该类中断被屏蔽(忽略)
  bit = 0 → 该类中断可被响应

用途:在执行原子操作(如修改进程队列)时,临时屏蔽所有中断,防止被打断导致数据结构不一致。

中断优先级

当多个中断同时到来时,按优先级决定响应顺序:

1
2
优先级(从高到低):
  硬件故障 > 系统调用 > 程序异常 > 时钟/外部 > I/O > 关机

直觉:越是”不处理就会丢失数据或系统崩溃”的中断,优先级越高。

中断嵌套

处理优先级较低的中断 X 的过程中,更高优先级的中断 Y 到来,可以打断 X 的处理:

1
2
3
4
5
6
7
8
9
正常执行
    ↓ 中断 X(优先级低)
处理 X ──────────────────────────────→
    ↓ 中断 Y(优先级高)
    处理 Y ────────→
                   ↓ Y 处理完
    继续处理 X ────────────────────────→
                                      ↓ X 处理完
继续正常执行

后响应的 Y,比先响应的 X 先处理完——这是嵌套与顺序的关键区别。

嵌套层数需有上限(通常 ≤ 3 层),防止核心栈溢出。

两个经典场景

1
2
3
4
5
场景一:X 和 Y 同时到来,Y 被屏蔽
  → 先响应并完整处理 X,再响应处理 Y(顺序执行)

场景二:X 和 Y 同时到来,Y 未被屏蔽且优先级更高
  → 先响应 X,Y 立即抢占 X,Y 先做完,再回来做 X(嵌套执行)

3. 进程管理

3.1 为什么需要进程?

问题:多道程序设计中,多个程序同时驻留内存并轮流使用 CPU。OS 如何追踪和管理每个”正在运行的程序”?

回答:程序本身是静态的(一段代码),而”运行”是动态的。同一份程序可以被多个用户同时使用(如 shell),每次运行都有独立的状态。

OS 需要一个管理实体来表示”程序的一次运行”——这就是进程(Process)

进程的形式化定义:程序关于某数据集合的一次运行活动,是操作系统资源分配和处理器调度的独立单位。

3.2 进程的五元组描述

一个进程在任意时刻,可以用五元组完整描述:

1
2
3
4
5
6
7
进程 = (P, C, D, R, PSW)

P   — 进程控制块(OS 管理数据结构)
C   — 内存中的代码(程序文本段)
D   — 内存中的数据(数据段 + 堆 + 栈)
R   — 通用寄存器的当前值
PSW — 程序状态字(PC/IR/CC/标志位)

R 和 PSW 是进程”当前执行位置”的快照,进程切换时需要完整保存和恢复。

3.3 进程状态模型

三态模型(基础)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                    调度选中
        ┌─────────────────────────────────┐
        ↓                                 │
  ┌──────────┐    时间片到 / 被抢占    ┌──────────┐
  │  运行态  │ ─────────────────────→ │  就绪态  │
  │ Running  │                        │  Ready   │
  └──────────┘                        └──────────┘
        │                                 ↑
        │ 等待资源/I/O/信号                │ 资源满足/I/O完成
        ↓                                 │
  ┌──────────┐                            │
  │  等待态  │ ───────────────────────────┘
  │ Waiting  │
  └──────────┘

关键约束(必须记住):

  • 不存在:等待态 → 运行态(等待满足后必须先进就绪队列排队,再被调度)
  • 不存在:就绪态 → 等待态(就绪进程不会主动等待资源)
  • ✅ 运行 → 等待:进程主动请求资源或 I/O,得不到立即满足
  • ✅ 等待 → 就绪:I/O 完成或资源可用,被 OS 唤醒
  • ✅ 就绪 → 运行:调度程序选中该进程
  • ✅ 运行 → 就绪:时间片用完,或被更高优先级进程抢占

为什么不能等待态 → 运行态? 调度的公平性要求——多个进程都在等待,等待结束时应该进入就绪队列,与其他就绪进程公平竞争 CPU,而不是某个进程等待结束就直接独占 CPU。

五态模型(加入挂起)

动机:系统中进程数过多,内存不足,需要暂时将某些进程”冻结”并换出到磁盘。

1
2
3
4
5
6
7
┌──────────┐   挂起   ┌──────────────┐
│  就绪态  │ ────────→│  挂起就绪态  │
└──────────┘          └──────────────┘
      ↑                      ↓ 激活
┌──────────┐   挂起   ┌──────────────┐
│  等待态  │ ────────→│  挂起等待态  │
└──────────┘          └──────────────┘

挂起 vs 等待的本质区别

 等待态挂起态
内存进程仍在内存中进程被换出到磁盘
已占资源保留(等待的是新资源)全部归还
调度不参与,但随时可被唤醒入就绪完全不参与调度
恢复条件等待事件发生需被 OS 显式激活

3.4 进程控制块(PCB)

PCB 是 OS 管理进程的核心数据结构,记录进程的一切必要信息。

PCB 包含的三类信息

标识信息(区分进程):

  • 系统分配的进程 ID(PID)
  • 进程组 ID
  • 用户定义的进程名
  • 父进程 ID

现场信息(进程切换时保存/恢复):

  • 通用寄存器值(R0, R1, …)
  • PC(下一条指令地址)
  • PSW(处理器状态字)
  • 核心栈指针 / 用户栈指针

控制信息(OS 决策依据):

  • 调度状态(运行/就绪/等待)和优先级
  • 代码段和数据段的地址
  • 队列链接指针(父子兄弟进程的连接)
  • 进程间通信信息(消息队列、信号量)
  • 资源清单(已分配的内存、文件描述符等)
  • 处理器使用时间(用于调度和计费)

进程映像 vs PCB

常见误区:进程映像 ≠ PCB。

1
2
3
4
5
6
7
8
9
10
进程映像(某时刻进程在内存中的完整实体):
┌─────────────┐
│     PCB     │ ← PCB 只是其中一部分
├─────────────┤
│   程序块     │ ← 代码段
├─────────────┤
│   数据块     │ ← 数据段 + 堆
├─────────────┤
│   核心栈    │ ← 进程在内核态时使用的栈
└─────────────┘

进程上下文

进程上下文(Process Context):进程执行所需的完整环境,分三层:

1
2
3
4
5
用户级上下文:用户程序块、数据区、用户栈、共享内存区
     ↕ 系统调用/中断跨越
寄存器上下文:PSW、栈指针、通用寄存器(切换时保存/恢复的核心)
     ↕
系统级上下文:PCB、内存区描述表、核心栈(OS 在内核态为该进程维护的数据)

3.5 进程队列管理

OS 维护多个队列组织进程:

1
2
3
4
5
6
7
就绪队列(通常按优先级排序):
  PCB_A → PCB_C → PCB_F → ...

等待队列(每类资源一个队列):
  磁盘I/O等待队列:PCB_B → PCB_D → ...
  网络等待队列:   PCB_E → ...
  信号量等待队列: PCB_G → ...

PCB 之间通过链表指针连接(PCB 中的”队列指引元”字段),O(1) 插入/删除。

3.6 进程控制原语

原语(Primitive):不可分割的操作,执行过程中不允许被中断。通过关中断实现原子性。

创建进程

1
2
3
4
5
① 分配 PCB,初始化标识信息
② 建立进程映像(分配内存,加载代码和数据)
③ 初始化 PCB:设置初始 PC(程序入口)、优先级等
④ 分配必要资源(文件描述符等)
⑤ 将 PCB 插入就绪队列

撤销进程

1
2
3
4
① 将进程从所在队列中摘除
② 递归撤销所有子进程
③ 释放占用的全部资源(内存、文件、设备)
④ 回收 PCB

阻塞进程

1
2
3
4
5
(进程自己调用,等待某事件)
① 保存当前处理器现场到 PCB
② 修改 PCB 状态:运行态 → 等待态
③ 将 PCB 插入对应等待队列
④ 调用进程调度程序,选择新进程运行

唤醒进程

1
2
3
4
5
(OS 在事件发生时调用,如 I/O 完成)
① 将 PCB 从等待队列中摘除
② 修改 PCB 状态:等待态 → 就绪态
③ 将 PCB 插入就绪队列
④ 若该进程优先级高于当前运行进程 → 触发抢占

注意:唤醒和阻塞必须成对使用——有阻塞必有对应的唤醒事件,否则进程将永久阻塞(死锁或饥饿的一种形式)。

3.7 进程切换的完整过程

前提:进程切换必须在内核模式下完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
触发条件(以时钟中断为例):
  CPU 在用户模式执行进程 A
        ↓ 时钟中断
① 正向模式切换(硬件自动)
   · 保存 A 的 PC/PSW 到 A 的核心栈
   · 切换到内核模式
   · 跳转到时钟中断处理程序

② 保存被中断进程 A 的其余现场
   · 保存通用寄存器到 A 的 PCB

③ 处理时钟中断
   · 更新时钟计数、时间片统计

④ 判断是否需要进程切换
   · A 的时间片用完 → 需要切换
   · 保存 A 的核心栈指针到 A 的 PCB
   · 修改 A 的 PCB 状态:运行态 → 就绪态
   · 将 A 的 PCB 插入就绪队列

⑤ 进程调度
   · 从就绪队列选择下一个进程 B

⑥ 恢复进程 B
   · 修改 B 的 PCB 状态:就绪态 → 运行态
   · 恢复 B 的地址空间(切换页表)
   · 从 B 的 PCB 恢复核心栈指针 SP
   · 从 B 的核心栈恢复通用寄存器

⑦ 逆向模式切换(中断返回指令,如 iret)
   · 从 B 的核心栈弹出 PSW/PC
   · 切换到用户模式
   · 跳转到 B 上次被中断的地址继续执行

哪些中断不触发进程切换? 若中断处理完毕后,没有进程状态发生转换(即被中断进程仍然是最高优先级就绪进程),则直接返回被中断进程,不进行进程切换。

3.8 fork 系统调用(理解进程创建)

fork() 创建一个与父进程几乎完全相同的子进程(写时复制)。

关键行为fork() 被调用一次,返回两次

  • 在父进程中返回子进程的 PID(> 0)
  • 在子进程中返回 0
  • 出错返回 -1

经典题型:以下程序最多创建几个进程?

1
2
3
4
5
6
int main() {
    fork();   // ← ①
    fork();   // ← ②
    fork();   // ← ③
    return 0;
}
1
2
3
4
main
 ├─ fork①后:main 和 child1
 ├─ fork②后:main、child1、child2(main的子进程)、child3(child1的子进程)
 └─ fork③后:上面4个进程各自再fork,共 4×2 = 8 个进程

规律:n 次无条件 fork(),最终产生 2^n 个进程。


4. 多线程技术

4.1 为什么需要线程?

进程的问题

  • 切换开销大:切换进程需要切换地址空间(换页表、刷 TLB),代价高
  • 通信代价高:进程间通信需要通过内核(管道、消息队列、共享内存),复杂且慢
  • 进程内无法并发:一个进程内只有一条执行路径,无法同时处理多个任务

典型场景:Web 服务器需要同时处理数千个连接请求。用进程实现:每个请求一个进程,切换开销不可接受。

解决思路:将进程的两项核心功能分离

1
2
3
4
进程(原来)= 资源分配单位 + 调度执行单位
                    ↓ 分离
进程(新) = 资源分配和保护的独立单位(虚拟地址空间 + 文件句柄 + ...)
线程      = 调度和分派的基本单位(执行路径,轻装运行)

4.2 线程的特征

同一进程内的多个线程:

共享(属于进程,不需要复制):

  • 虚拟地址空间(代码段、数据段、堆)
  • 文件描述符
  • 进程 ID 和权限

独立拥有(每个线程自己的):

  • 程序计数器(PC)——各自的执行位置
  • 执行栈——函数调用链、局部变量
  • 线程状态(运行/就绪/睡眠)
  • 寄存器上下文

线程切换 vs 进程切换

1
2
进程切换:需要切换地址空间(刷 TLB,换页表)→ 开销大(约 1-10 µs)
线程切换:同进程内,地址空间不变,只切换栈和寄存器 → 开销小(约 0.1-1 µs)

4.3 线程状态

线程只有三态(没有挂起——挂起是进程层面的操作):

1
2
3
4
5
睡眠态 ←────────────── 运行态
  │    等待事件/资源      │
  │                      │ 时间片到/被抢占
  │ 事件/资源满足         ↓
  └─────────────────→ 就绪态

4.4 内核级线程(KLT)vs 用户级线程(ULT)

用户级线程(ULT, User-Level Thread)

线程的创建、调度、同步完全在用户空间由线程库实现,内核不感知线程的存在。

1
2
3
4
5
6
7
8
9
10
┌────────────────────────────────────┐
│            用户空间                 │
│  线程A  线程B  线程C                │
│    ↕ 线程库管理(用户态调度器)       │
└────────────────────────────────────┘
         ↕ 系统调用(进程级)
┌────────────────────────────────────┐
│            内核空间                 │
│       进程(内核只看到进程)         │
└────────────────────────────────────┘

优点

  • 线程切换无需陷入内核,速度极快
  • 可在任何 OS 上实现(无需内核支持)

致命缺点

  • 一个线程阻塞 → 整个进程阻塞(内核不知道进程内部还有其他线程可以运行)
  • 无法利用多核处理器(内核调度的是进程,多个 ULT 无法在多个 CPU 核上真正并行)

Jacketing 技术(缓解 ULT 阻塞问题):

1
2
3
4
5
6
7
原来:线程 A 调用 read()(阻塞系统调用)
      → 内核阻塞整个进程 → 线程 B/C 也被阻塞

Jacketing:将 read() 包装成非阻塞版本
      → 先调用 select() 检查 I/O 是否就绪
      → 不就绪 → 切换到其他线程,稍后再试
      → 就绪 → 执行真正的 read()

内核级线程(KLT, Kernel-Level Thread)

内核直接管理线程,每个线程在内核中有对应的描述结构。

1
2
3
4
5
6
7
8
9
10
┌────────────────────────────────────┐
│            用户空间                 │
│  线程A  线程B  线程C                │
└────────────────────────────────────┘
         ↕ 每个线程独立的系统调用
┌────────────────────────────────────┐
│            内核空间                 │
│   KLT_A   KLT_B   KLT_C          │
│       内核调度器直接调度线程          │
└────────────────────────────────────┘

优点

  • 一个线程阻塞,其余线程继续运行
  • 支持多核真正并行

缺点

  • 线程切换需要陷入内核,比 ULT 切换慢
  • 线程创建/销毁需要系统调用

对比总结

 ULT(用户级)KLT(内核级)
管理者用户态线程库OS 内核
线程阻塞影响整个进程阻塞只影响该线程
多核并行❌ 不支持✅ 支持
切换开销小(无需陷入内核)大(需系统调用)
适用场景逻辑并发(单核)物理并行(多核)

4.5 混合策略(M:N 模型)

结合 ULT 和 KLT 的优点:

1
2
3
用户空间:M 个 ULT
              ↕ 多对多映射
内核空间:N 个 KLT(N ≤ M)

工作方式

  • ULT 的创建/调度/同步在用户空间完成(快)
  • KLT 的数量由程序员根据硬件和需求调节
  • 有 KLT 空闲时,将 ULT 绑定到 KLT 上(活跃态),实现并行
  • 阻塞的 ULT 解绑 KLT,让 KLT 去运行其他 ULT

工程实现:Solaris 的轻量级进程(LWP)是这种混合策略的经典案例。现代 Go 语言的 goroutine 调度器也是 M:N 模型的一个实现。


5. 处理器调度

5.1 调度的三个层次

OS 中的”调度”不是单一问题,而是分三层:

层次别称决策问题时间尺度
高级调度长程调度 / 作业调度允许哪些新进程进入系统(后备队列 → 就绪队列)分钟级
中级调度中程调度 / 负载平衡哪些进程驻留内存(控制挂起/激活)秒级
低级调度短程调度 / 进程调度就绪队列中谁获得 CPU毫秒级

最重要的是低级调度——它直接决定每一次 CPU 分配,对用户体验影响最直接。

高级调度的作用:在批处理系统中,控制系统中活跃进程的总数(多道程序度),防止内存过载和 CPU 饱和。

中级调度的作用:在负载高峰时挂起部分进程(换出到磁盘),缓解内存和 CPU 压力,之后再激活。

5.2 调度算法的评估指标

设计调度算法时需要权衡多个指标,这些指标往往相互冲突:

指标含义谁关心
CPU 利用率CPU 忙碌的时间比例系统管理员
吞吐量单位时间完成的进程数批处理用户
周转时间进程从提交到完成的总时间批处理用户
等待时间进程在就绪队列中等待的总时间所有用户
响应时间从请求提交到首次响应的时间交互式用户
公平性每个进程获得公平的 CPU 份额系统设计者

矛盾举例:最短进程优先(SPN)能最小化平均周转时间,但会导致长进程”饥饿”——公平性极差。

5.3 非抢占式调度算法

FCFS(先来先服务,First Come First Served)

策略:按进入就绪队列的先后顺序分配 CPU,一旦获得 CPU 就运行到结束(或主动放弃)。

示例(到达时间均为 0,服务时间如下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
进程    服务时间
A         3
B         5
C         1
D         2

FCFS 顺序:A → B → C → D

时间轴:
0    3         8    9   11
|─A──|────B────|─C─|─D─|

等待时间:A=0, B=3, C=8, D=9
平均等待时间 = (0+3+8+9)/4 = 5

缺点:”护航效应”——一个长进程后面的所有短进程都要等待。对 I/O 密集型进程(短 CPU 时间片、频繁等待)特别不利。

SPN(最短进程优先,Shortest Process Next)

策略:选择估计剩余 CPU 时间最短的就绪进程。

优点:在所有非抢占算法中,SPN 给出最小的平均等待时间(可以数学证明)。

缺点

  • 需要预知进程的服务时间——实践中通常用历史执行时间做估计(指数加权平均)
  • 长进程饥饿:只要不断有短进程到来,长进程可能永远等不到 CPU
1
2
3
4
5
6
7
8
9
平均等待时间(同上例):
SPN 顺序:C(1) → D(2) → A(3) → B(5)

时间轴:
0  1   3      6          11
|C|─D─|──A───|─────B─────|

等待时间:C=0, D=1, A=3, B=6
平均等待时间 = (0+1+3+6)/4 = 2.5   ← 比 FCFS 的 5 少了一半

HRRN(最高响应比优先,Highest Response Ratio Next)

动机:SPN 对短进程偏袒,HRRN 通过引入”等待时间”因素来照顾长期等待的进程。

响应比公式

1
2
响应比 = (等待时间 + 估计服务时间) / 估计服务时间
       = 1 + (等待时间 / 估计服务时间)

性质

  • 进程刚到时,响应比 = 1(等待时间 = 0)
  • 等待越久,响应比越大,越容易被选中——自动解决饥饿问题
  • 服务时间短的进程,响应比增长更快——仍偏袒短进程

代价:每次调度需要计算所有就绪进程的响应比,时间复杂度 O(n);仍需估计服务时间。

5.4 抢占式调度算法

RR(时间片轮转,Round Robin)

策略:按 FCFS 顺序,每个进程最多运行一个时间片(quantum),时间片用完则强制剥夺 CPU,排到就绪队列末尾。

时间片选择的权衡

1
2
3
时间片过短 → 切换开销占比过大(假设切换需 1ms,时间片 1ms → 50% 时间浪费在切换)
时间片过长 → 响应时间变差,退化为 FCFS
经验法则:时间片 = 10-100ms,切换时间 ~1ms(切换开销约占 1%)

优点

  • 不需要预知进程的服务时间(只需时钟中断)
  • 对所有进程公平,响应时间有保证
  • 适合交互式系统

示例(时间片 = 1,进程 A:3, B:5, C:1, D:2):

1
2
3
时间轴:
0 1 2 3 4 5 6 7 8 9 10 11
A B C D A B D A B   B    B

SRT(最短剩余时间优先,Shortest Remaining Time)

SPN 的抢占版本:新进程到来时,如果其服务时间比当前运行进程的剩余时间更短,立即抢占。

优点:平均等待时间理论上比 SPN 更好(因为更早执行短进程)

缺点

  • 需要实时跟踪所有进程的剩余时间
  • 长进程饥饿问题更严重(不断被新到的短进程抢占)

多级反馈队列(Feedback)

设计思想:不需要预先知道进程类型(交互式/批处理),让进程自己通过行为”分类”。

结构

1
2
3
4
5
6
队列 0(最高优先级,时间片最短):  [新进程优先入这里]
队列 1(次高优先级,时间片较短):
队列 2(中等优先级,时间片中等):
队列 3(低优先级,时间片较长):
...
队列 N(最低优先级,时间片最长/FCFS):  [长期运行的批处理进程沉到这里]

规则

  • 新进程进入最高优先级队列
  • 每次被时钟抢占(时间片用完),降一级队列(表明进程需要较长 CPU 时间)
  • 主动放弃 CPU(如等待 I/O),不降级(保持在当前队列)
  • 只有高优先级队列为空,才调度低优先级队列的进程

效果

1
2
短进程(交互式):很快执行完,在高优先级队列里解决,响应时间短
长进程(批处理):逐渐沉到低优先级队列,获得更长时间片(减少切换)但优先级低

关键洞察:Feedback 对短进程友好的程度超过 RR,因为短进程在高优先级队列竞争者少,等待时间更短。

现代实现:Linux 的 CFS(Completely Fair Scheduler)、Windows 的优先级队列都借鉴了这一思想,实际可有 32-128 个队列(包含实时队列)。

彩票调度(Lottery Scheduling)

思想:给每个进程分配若干”彩票”(表示资源份额),每次调度随机抽一张,持票进程获得 CPU。

1
2
3
进程 A:50 张票(50% 的 CPU 期望)
进程 B:30 张票(30% 的 CPU 期望)
进程 C:20 张票(20% 的 CPU 期望)

优点

  • 支持任意比例的资源分配(增减彩票即可)
  • 合作进程可以互相”赠票”(提高彼此的优先级)
  • 实现简单,无饥饿(每个进程都有票,总有机会被选中)

缺点:随机性导致短期内可能不公平(虽然长期期望是准确的)。

5.5 调度算法综合对比

算法抢占需要预知服务时间饥饿风险适用场景
FCFS无(但短进程等待久)批处理
SPN长进程饥饿批处理(已知服务时间)
SRT长进程严重饥饿批处理(可实时估计)
HRRN无(响应比自动升高)批处理
RR交互式系统
Feedback低优先级可能通用(混合负载)
Lottery可选理论上无需要比例控制的场景

5.6 调度算法演示示例

以下用统一示例展示各算法的差异。

假设(到达时间和服务时间如下):

1
2
3
4
5
进程    到达时间   服务时间
A          0          3
B          2          5
C          4          1
D          6          2

FCFS(非抢占):

1
2
3
4
时间:0        3         8    9
      [──A──] [────B────] [C] [──D──]
                                    11
等待时间:A=0, B=1, C=4, D=3   平均=2

SPN(非抢占,在每次有进程完成时选最短的):

1
2
3
时间:0    3  4       9  10      
      [─A─] [B先等] ... (需逐步分析)
(实际计算需结合到达时间,细节略)

RR(时间片=1)

1
2
3
4
A到达时进队,B在t=2到达插队(RR末尾),以此类推
时间:0 1 2 3 4 5 6 7 8 9 10 11
      A A B A B C B D B  B
(C=1片执行完,A=3片,D=2片,B=5片)

Feedback(时间片=1,沉降规则)

与 RR 的差异:长进程 B 在用完多个时间片后沉入低优先级队列,短进程 C、D 插到 B 前面,响应时间更短。


6. 附录:概念速查与常见误区

常见误区

误区正确理解
“进程映像 = PCB”PCB 是进程映像的一部分,进程映像还包括程序块、数据块、核心栈
“等待态可以直接变运行态”不可以。必须先变就绪态,再被调度变运行态
“线程有挂起状态”没有。挂起是进程层面的操作(涉及换出内存),线程只有运行/就绪/睡眠三态
“ULT 可以利用多核”不能。内核只看到进程,ULT 绑定在单个进程上,无法分配到多个 CPU 核
“中断后一定发生进程切换”不一定。若无进程状态转换,中断处理完直接返回被中断进程
“SPN 平均等待时间最短所以最好”SPN 会导致长进程饥饿,实际系统需要权衡公平性
“时间片越短越好”不对。时间片过短导致切换开销占比过大,系统效率下降

状态转换合法性速查

1
2
3
4
5
6
7
运行 → 就绪:✅ 时间片到/被抢占
运行 → 等待:✅ 请求资源/I/O
等待 → 就绪:✅ 资源满足/I/O 完成
就绪 → 运行:✅ 被调度选中

等待 → 运行:❌ 非法(必须经过就绪态)
就绪 → 等待:❌ 非法(就绪进程不会主动等待)

三层调度的触发时机

调度层次触发时机
高级调度新作业提交时;当前系统进程数低于阈值时
中级调度内存不足时(挂起);内存空闲且挂起进程等待结束时(激活)
低级调度进程阻塞时;时间片用完时;进程终止时;新进程创建时(抢占型)

本章知识依赖图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CPU 硬件结构(寄存器、指令周期)
            ↓
处理器模式(特权 vs 非特权,Ring 0 vs Ring 3)
            ↓
模式切换机制(中断/异常/系统调用 → 内核)
            ↓
中断系统(响应 + 处理,中断向量表,优先级,嵌套)
            ↓
进程抽象(PCB,三态/五态,创建/撤销/阻塞/唤醒原语)
            ↓
进程切换(保存/恢复上下文的具体步骤)
            ↓
线程(分离调度与资源,ULT/KLT/混合)
            ↓
调度算法(三层调度,FCFS/SPN/RR/Feedback/...)

每一层都依赖上一层的机制。理解了中断,才能理解进程切换是如何被触发的;理解了进程切换,才能理解调度算法为什么需要关注切换开销。

This post is licensed under CC BY 4.0 by the author.