408 · OPERATING SYSTEM · 汤小丹 / 王道 完整版
28 节课,完整覆盖王道 6 章内容:从概念到进程、调度、同步、内存、虚拟内存、文件、IO,每个考点都有代码 / 算例 / 陷阱。
// agenda
01 · 概念
| 结构 | 核心 / 特点 |
|---|---|
| 单体 | 整个 OS 在内核态;速度快,难维护(早期 Linux) |
| 分层 | 下层为上层服务;调试方便,但调用链长(THE) |
| 微内核 | 仅调度 / IPC / 基础内存在内核态;其余以服务进程实现(Mach、Minix) |
| 外核 | 把硬件抽象做薄给应用更大自由度 |
| 模块化 | 可加载内核模块(现代 Linux) |
02 · 模式
| 用户态 | 核心态 | |
|---|---|---|
| 权限 | 仅普通指令 | 全部指令(含特权) |
| 地址空间 | 用户空间 | 系统空间 |
| 程序 | 应用程序 | OS 内核 |
| 切换方式 | → 核心:中断 / 异常 / 系统调用 | ← 用户:iret / sysret |
用户程序请求 OS 服务的唯一合法入口。
request / release / read / write
open / close / create / unlink
fork / exec / wait / exit
pipe / msgget / shmget
mmap / brk / sbrk
02 · 控制转移
| 类型 | 来源 | 典型 |
|---|---|---|
| 外中断 | 外设 / 时钟 | 键盘、磁盘 ready、定时器 |
| 内中断 | CPU 内部 | 除零、缺页、保护错 |
| 陷阱 | 主动 trap | 系统调用 |
中断 / 异常都涉及"保护现场 → 跳服务程序 → 恢复"。
在服务中允许更高优先级中断。每级有中断屏蔽字规定能屏蔽哪些级别。同级总是屏蔽,且需自屏。
03 · 进程
// 进程在系统中存在的唯一标识 struct PCB { PID, UID, GID; // 标识符 PSW, PC, GPR[]; // 处理机状态 state, priority, queue; // 调度信息 PageTableBase, ASID; // 内存 OpenFileTable; // 文件 IPC channels; // 通信 CPU/IO usage; // 记账 };
新建(创建中)→ 就绪(等 CPU)→ 运行(在 CPU 上)→ 阻塞(等事件)→ 终止(资源回收中)。
+ 挂起状态(被换出内存):就绪挂起 / 阻塞挂起。
引入"挂起"是为了腾出内存给其他进程。挂起 ≠ 阻塞:挂起是用户/OS 主动换出。
03 · 状态机 + 原语
| 原语 | 执行内容 |
|---|---|
| 创建 | 申请 PCB → 分配资源 → 初始化 → 入就绪队列 |
| 撤销 | 终止子孙 → 归还资源 → 释放 PCB |
| 阻塞 | 保存运行现场 → 状态改阻塞 → 入对应等待队列 |
| 唤醒 | 从阻塞队列移出 → 状态改就绪 → 入就绪队列 |
原语:用关 / 开中断保证执行不可中断;必在核心态执行。
03 · 线程
| 进程 | 线程 | |
|---|---|---|
| 地址空间 | 独立 | 共享所属进程的 |
| 切换开销 | 大 | 小 |
| 通信 | IPC | 共享变量(需同步) |
| 调度单位 | — | 是 |
| 资源单位 | 是 | 不是 |
PC、寄存器组、栈、线程局部存储 TLS。
代码段、堆、全局变量、文件描述符、信号处理器。
| 用户级 ULT | 内核级 KLT | |
|---|---|---|
| 切换 | 用户库,快 | 陷核,慢 |
| 阻塞 | 整进程阻塞 | 仅当前线程 |
| 多核 | ❌ | ✅ |
| 调度 | 用户控制 | OS 调度 |
04 · 调度概念
| 层级 | 对象 | 动作 | 频率 |
|---|---|---|---|
| 高级(作业) | 外存 → 内存 | 挑选哪个作业进入内存 | 低 |
| 中级(交换) | 内存 ↔ 外存 | 挂起 / 激活 | 中 |
| 低级(进程) | 就绪 → 运行 | 从就绪队列选一个上 CPU | 高 |
周转时间 = 完成 - 提交
带权周转 = 周转 / 服务
等待时间 = 周转 - 服务
响应时间 = 首次响应 - 请求
CPU 利用率 = 忙 / 总
吞吐率 = 完成数 / 单位时间
低优先级 / 长任务一直不被调度。解决:老化 aging — 等待时间越长优先级越高。
04 · 三个基础算法
进程到达:A(0,7) B(2,4) C(4,1) D(5,4)
| FCFS | A 0-7 | B 7-11 | C 11-12 | D 12-16 |
|---|---|
| SJF 非抢占 | A 0-7 | C 7-8 | B 8-12 | D 12-16 |
| SRTN | A 0-2 | B 2-4 | C 4-5 | B 5-7 | D 7-11 | A 11-16 |
SJF 在选 B 时 C 还没到;SRTN 一旦更短的来,立即抢占。
| FCFS | SJF | SRTN | |
|---|---|---|---|
| 抢占 | 否 | 否 | 是 |
| 平均等待 | 大 | 小 | 最小 |
| 护航效应 | 有 | 无 | 无 |
| 饥饿 | 无 | 长任务 | 长任务 |
| 实现 | 简单 | 需 burst 预测 | 需 burst + 抢占 |
SJF 在 burst 已知或可预测(指数平均 τ_{n+1} = α·t_n + (1-α)τ_n)。
SRTN 在新进程到达时再判断一次。
04 · 交互式算法
Unix/Linux 早期调度器使用类似 MLFQ。
05 · 同步
| 互斥 | 同步 | |
|---|---|---|
| 含义 | 共享资源不能同时被多个进程访问 | 多进程按特定顺序协作 |
| 类比 | 厕所一人一格 | 接力赛跑 |
| 信号量初值 | 1(互斥锁) | 0(事件计数) |
| 方法 | 说明 |
|---|---|
| 软件 | Peterson 算法 / Dekker,纯算法实现,不需特殊指令 |
| 硬件 | 关中断(单核)/ TestAndSet / Swap 原子指令 |
| 信号量 | OS 提供的高层抽象,PV 操作 |
| 管程 | 编译器封装互斥与同步,条件变量 wait/signal |
bool TS(bool *lock) { bool old = *lock; *lock = true; return old; // 原子读改写 } // 自旋锁 while(TS(&lock)) ; 进入临界区; ... lock = false;
05 · 信号量
// 整型: 忙等 P(S): while(S <= 0) ; // 自旋 S--; V(S): S++; // 记录型: 让权 (考试用) typedef { int value; queue L; } sem; P(sem &S): S.value--; if(S.value < 0) block(S.L, self); V(sem &S): S.value++; if(S.value <= 0) wakeup(S.L);
"P 资源 → P 互斥 → 临界区 → V 互斥 → V 资源"。
顺序错(先 P 互斥再 P 资源)易死锁。
PV 由 OS 实现,内部用关中断 / 硬件原语保证原子;用户层只看到不可分割的操作。
06 · 经典问题
mutex = 1 // 缓冲互斥
empty = N // 空槽位数
full = 0 // 已填槽数
producer(): while(true) { produce(item); P(empty); // 等空槽 P(mutex); // 互斥 put(item); V(mutex); V(full); // 多了一份产品 } consumer(): while(true) { P(full); P(mutex); item = get(); V(mutex); V(empty); consume(item); }
⚠️ 互换 P 顺序:
P(mutex); P(empty); 若 empty=0 → 持锁阻塞 → 消费者拿不到 mutex → 死锁。
06 · 经典问题 (续)
sem mutex=1, w=1; int rc=0; reader(): P(mutex); rc++; if(rc==1) P(w); // 第一个读阻塞写 V(mutex); read(); ... P(mutex); rc--; if(rc==0) V(w); // 最后一个读放写 V(mutex); writer(): P(w); write(); V(w);
读者优先:只要有读,写就一直等 → 写饥饿。
写优先需另加 mutex2 阻塞后续读。
本质:哲学家是死锁的"循环等待"原型。任意打破 4 条件之一都能解决。
07 · 死锁
| 核心 | 代价 | |
|---|---|---|
| 预防 | 破坏 4 条件之一 | 资源利用率降低 |
| 避免 | 运行时安全性检查 | 需预先声明最大需求 |
| 检测+解除 | 不限制,定期检测 | 检测开销 + 回滚损失 |
把进程节点的"未被阻塞"的边都消除:所有边都能消则无死锁。残留环 = 死锁进程。
07 · 死锁避免
Available[m] // 各类资源现有数 Max[n][m] // 进程最大需求 Allocation[n][m] // 已分配 Need = Max - Allocation Request[i] // 进程 i 当前请求 // 1. 请求合法性 if(Request > Need || Request > Available) reject 或 阻塞; // 2. 试探分配 Available -= Request; Allocation += Request; Need -= Request; // 3. 安全性检查 若安全 → 真分配 否则 → 回退试探
Work = Available; Finish[i] = false; repeat: 找 i : Finish[i]=false && Need[i] <= Work if(找到): Work += Allocation[i] Finish[i] = true continue break; if(forall Finish[i]=true) → 安全 (Work 序列即安全序列) else → 不安全
5 进程 3 类资源,Total=(10,5,7),Allocation/Need 给定 → 找 Need[i] ≤ Available 的进程逐步释放,直到所有进程 Finish。常见题:给一个新 Request,判断是否安全。
08 · IPC
异步通知(SIGINT、SIGTERM);处理函数注册 / 默认动作 / 忽略。
网络 IPC,支持本机和跨机;TCP / UDP / Unix Domain。
System V 三件套:sem / msg / shm。
09 · 内存基础
| 方式 | 说明 |
|---|---|
| 绝对装入 | 编译时确定物理地址;适合单道 |
| 静态重定位 | 装入时修改地址;运行中不能再移 |
| 动态重定位 | 运行时通过重定位寄存器 + 偏移;可在内存中移动(现代 OS) |
| 方法 | 规则 | 问题 |
|---|---|---|
| 单一连续 | 整个用户区给一个程序 | 低利用率 |
| 固定分区 | 分若干固定大小区 | 内部碎片 |
| 动态分区 | 按需切割 | 外部碎片 |
"碎片紧凑"compaction 可以整理外部碎片 → 但需要动态重定位支持。
09 · 分页
基本分页:2 次访存(取页表项 + 取数据)。
有 TLB 时:命中只需 1 次(TLB 内含目标页表项)。
每个进程最后一页可能不满 → 内部碎片,平均损失半页。页越大碎片越多,但页表越小。
典型页大小:4 KB、8 KB(Linux x86 默认 4 KB)。
V 有效位 / D 修改位(dirty)/ A 访问位 / U 用户权限 / R/W 读写位 / G 全局位。
09 · 大地址空间
32 位虚拟地址,4 KB 页:页表项数 = 2³²/2¹² = 2²⁰ = 1M 项。每项 4 字节 → 4 MB 页表 / 进程。100 个进程就要 400MB。
每个物理页框一项,全 OS 共一张表(不再按进程)。
表项 = <PID, 页号 P, 控制位>
地址变换:(PID, P) → hash → 表项 i → 物理帧 i
x86_64:48 位地址,4 KB 页 → 4 级页表(9+9+9+9+12 位)。Intel 5 级页表支持 56/57 位虚拟地址。
09 · 段页
| 分页 | 分段 | |
|---|---|---|
| 大小 | 固定 | 可变 |
| 地址 | 一维 | 二维 |
| 视角 | 物理 | 逻辑 |
| 碎片 | 内部 | 外部 |
既保留分段的逻辑独立 / 共享 / 保护,又用分页解决碎片。
每段单独分页,段内的页表起址在段表中。
10 · 虚拟内存
利用程序的局部性原理:只调入当前真用到的页;缺页时再从磁盘换入。
物理块号 | V 有效 | D 修改 | A 访问 | U/S 权限
V=0 表示该页不在内存中。
10 · 置换
| 算法 | 规则 | 问题 |
|---|---|---|
| OPT | 淘汰未来最久不用 | 无法实现(无未来信息) |
| FIFO | 淘汰最早进入的页 | Belady 异常 |
| LRU | 淘汰最近最久未用 | 实现开销大 |
| Clock (简单) | 循环扫,用访问位 A=0 替换 | 近似 LRU |
| Clock 改进 | (A,D) → (0,0) < (0,1) < (1,0) < (1,1) | 考虑修改位减少写回 |
FIFO 算法下,物理块数增加反而缺页率上升的反常现象。LRU、OPT 不会出现。
页面引用串:7,0,1,2,0,3,0,4,2,3
3 个物理块:
页框组成环形链表,扫描指针顺序前进;访问位 A:1→清零并跳过;0→替换并新页 A=1。
10 · 内存调优
物理块过少 → 缺页频繁 → CPU 大量时间在换页而非计算 → 系统吞吐反而下降。
原因:进程数太多、驻留集太小。
同样的逻辑,按行 vs 按列访问 NxM 数组缺页次数可相差 N 倍(行优先存储时按行访问最佳)。
// 假设页大小 4KB, int 4B, 1页=1024个int // 二维数组 1024×1024 行优先 // 好: 按行访问 for(i=0; i<1024; i++) for(j=0; j<1024; j++) a[i][j] = 0; // 缺页 1024 次 // 差: 按列访问 for(j=0; j<1024; j++) for(i=0; i<1024; i++) a[i][j] = 0; // 缺页 1024×1024 次!
11 · 文件
每个文件对应一个 FCB(也叫 inode),存元信息。
inode 内容:
类型 / 大小 / 时间戳
权限 (r/w/x × user/group/other)
链接计数
数据块指针(12 直接 + 1/2/3 级间接)
| 硬链接 | 软链接 | |
|---|---|---|
| 本质 | 新目录项指向同一 inode | 新文件,内容为路径字符串 |
| 跨分区 | ❌ | ✅ |
| 目录 | ❌ | ✅ |
| 源删除 | 另一份还能用 | 失效(悬空) |
| 引用计数 | +1 | 不变 |
11 · 物理结构
| 方式 | 优点 | 缺点 |
|---|---|---|
| 连续 | 顺序快,random O(1) | 外部碎片,难扩展 |
| 链接 | 无碎片,扩展容易 | random 慢,1 块坏全坏 |
| 索引 | random 快,扩展灵活 | 额外索引块 |
| 多级索引 | 支持超大文件 | 访问深度大 |
字号 i, 位号 j, 每字 32 bit
块号 b = 32·i + j
反过来 i = b / 32, j = b % 32
12 · IO
T = T_寻道 + T_旋转 + T_传输
// 寻道占主导,调度优化的就是这一项
| 算法 | 规则 |
|---|---|
| FCFS | 到达顺序,公平 |
| SSTF | 最短寻道优先,可能远端饥饿 |
| SCAN(电梯) | 一个方向扫到底再回头 |
| C-SCAN | 扫到尽头直接跳回起点 |
| LOOK / C-LOOK | 不到尽头,到最远请求就回头 |
| 方式 | CPU 介入度 | 每次单位 |
|---|---|---|
| 查询 | 持续轮询 | 1 字 |
| 中断 | 就绪通知 | 1 字 / 中断 |
| DMA | 启动 + 结束 | 1 块 / 中断 |
| 通道 | 仅启动通道程序 | 1 组数据 / 中断 |