# 📖 操作系统复习与教学笔记

> **目标**：不仅能应付考试，更能真正理解操作系统的工作原理
> **定位**：这份笔记不是简单的知识点罗列，而是教你"怎么学、怎么记、怎么做题"

---

## 目录

1. [复习策略与时间规划](#一复习策略与时间规划)
2. [四大必考计算题 · 手把手教学](#二四大必考计算题--手把手教学)
3. [核心概念 · 快速记忆法](#三核心概念--快速记忆法)
4. [易混淆概念辨析](#四易混淆概念辨析)
5. [代码题考点精讲](#五代码题考点精讲)
6. [问答题高频考点](#六问答题高频考点)
7. [考前速查表](#七考前速查表)

---

## 一、复习策略与时间规划

### 1.1 按优先级分三层

| 层级 | 内容 | 建议投入时间 | 策略 |
|------|------|-------------|------|
| 🔴 **保命层** | 磁盘调度、页面置换、fork进程树、inode计算 | 60% | 必须能**独立默写**解题步骤 |
| 🟡 **进阶层** | CPU调度、I/O控制方式、段页式地址转换、死锁 | 30% | 理解原理 + 做2-3道题 |
| 🟢 **基础层** | 概念题、代码优化、工作集模型 | 10% | 多看几遍，有印象即可 |

### 1.2 复习三步法

```
第一步：看本笔记 → 建立知识框架和记忆锚点
第二步：做预测卷 → 实战演练，暴露薄弱点
第三步：复盘错题 → 针对性地回顾对应章节
```

### 1.3 考试时间分配建议（120分钟）

| 题型 | 建议用时 | 策略 |
|------|---------|------|
| 计算题（磁盘/页面置换/CPU调度/inode） | 50-60min | 先做！这是拿分大头 |
| 代码分析题（fork进程树） | 15-20min | 画图要清晰规范 |
| 简答/概念题 | 25-30min | 分点作答，关键词给分 |
| 地址转换题 | 10-15min | 按步骤写，不要跳步 |
| 检查 | 5-10min | 检查计算题的数字 |

---

## 二、四大必考计算题 · 手把手教学

### 2.1 磁盘调度算法计算（★★★★★ 每年必考）

#### 本质理解

磁盘调度就是在解决"磁头怎么走最省时间"的问题。可以把磁头想象成电梯，磁道就是楼层号。

#### 各算法的"人设"

| 算法 | 一句话人设 | 行为特征 |
|------|-----------|----------|
| **FCFS** | "先来后到"的耿直 boy | 来一个处理一个，不做任何优化 |
| **SSTF** | "贪心"的近视眼 | 每次都选最近的，但可能让远的永远等不到 |
| **SCAN** | "电梯"本梯 | 一直走到头再回头 |
| **C-SCAN** | 单向电梯 | 只往上走，到顶后直接回到底部重来 |
| **LOOK** | 智能电梯 | 走到最后一个请求就回头，不到边界 |
| **C-LOOK** | 智能单向电梯 | 只往上到最高请求，然后直接回最低请求 |

#### 📐 解题模板（以最难的 C-SCAN 为例）

```
题目：磁道0-199，磁头在53，向增大方向移动
请求序列：98, 183, 37, 122, 14, 124, 65, 67

Step 1: 将请求排序
排序：14, 37, 65, 67, 98, 122, 124, 183

Step 2: 确定方向并依次服务
C-SCAN规则：向增大方向走 → 到199（边界）→ 回到0 → 继续向增大方向

路径：53 → 65 → 67 → 98 → 122 → 124 → 183 → 199（到边界）→ 0 → 14 → 37
移动距离：12 + 2 + 31 + 24 + 2 + 59 + 16 + 199 + 14 + 23

Step 3: 求和
= 12+2+31+24+2+59+16+199+14+23 = 382道
```

#### ⚠️ 常见错误

| 错误 | 正确做法 |
|------|---------|
| SCAN/C-SCAN 忘记走到边界（199或0） | **必须**走到边界，除非题目说用 LOOK |
| SSTF 选择了相等距离的两个磁道 | 通常选编号小的（题目可能会有说明） |
| 方向搞反 | 题目说"向增大方向"就从53往上走 |
| C-SCAN 往回时逐个服务 | C-SCAN 回头时**不服务**，到底后才重新开始 |

#### 💡 速算技巧

SSTF 的路径一定是最短的（贪心嘛），FCFS 通常是最长的。检查时如果 SSTF 比别的算法还长，肯定算错了。

---

### 2.2 页面置换算法（★★★★★ 每年必考）

#### 本质理解

内存满了，要换掉哪个页面？OPT 是"预言家"，LRU 是"记仇本"，FIFO 是"排队机"。

#### 各算法速记

| 算法 | 口诀 | 关键特征 |
|------|------|---------|
| **OPT** | "看未来，谁最晚用就换谁" | 理论最优，无法实现（但考试能算） |
| **FIFO** | "先来先走，排队淘汰" | 可能 Belady 异常（页框变多反而缺页更多） |
| **LRU** | "看过去，谁最久没用就换谁" | 无 Belady 异常，硬件开销大 |
| **Clock** | "转圈查，引用位=0就换" | LRU 的简化版 |

#### 📐 解题模板（以 LRU，页框数=3 为例）

```
访问序列：7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1

画表格法：
-------------------------------------------------------------
访问  页框1  页框2  页框3  是否缺页  说明
-------------------------------------------------------------
7     7      -      -      ✓缺页    空，直接放入
0     7      0      -      ✓缺页    空，直接放入
1     7      0      1      ✓缺页    空，直接放入
2     7      0      2      ✓缺页    换掉最久未用的1（1离上次最远）
0     7      0      2      ✗命中    0已存在，更新它的使用时间
3     3      0      2      ✓缺页    换掉最久未用的7（7离上次最远）
0     3      0      2      ✗命中    0已存在
4     3      0      4      ✓缺页    换掉最久未用的2（2离上次最远）
...以此类推
```

#### ⚠️ OPT 和 LRU 的区别（最容易混淆）

```
序列：1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5（页框数=4）

OPT 在访问5时：
  当前页框：1,2,3,4
  看未来：1还要用（第5位），2还要用（第6位），3还要用（第10位），4还要用（第11位）
  最晚使用的是4（第11位才用），所以换4

LRU 在访问5时：
  当前页框：1,2,3,4
  看过去：1最近在位置4用过，2在位置5用过，3在位置2用过，4在位置3用过
  最久未用的是3（位置2之后就没用过），所以换3
```

> **关键口诀**：OPT 看**未来**，LRU 看**过去**。OPT 是"预言"，LRU 是"历史"。

#### 💡 Belady 异常

**只在 FIFO 中出现**。增加页框数反而缺页更多。LRU 和 OPT 都不会有这个问题。

---

### 2.3 fork 进程树分析（★★★★★ 每年必考）

#### 本质理解

`fork()` 就像细胞分裂 — 调用一次，变成两个进程，从同一行代码继续执行。

#### 黄金法则

```
法则1：fork() 一次，返回两次
  - 父进程返回值 = 子进程的 PID
  - 子进程返回值 = 0

法则2：子进程从 fork() 的下一行开始执行（不是从 main 开头）

法则3：fork() 嵌套时，每个进程都要独立分析它的执行路径

法则4：父进程总数 = 2^n（n为fork调用次数，无嵌套时）
```

#### 📐 解题模板

```
程序：
fork()            // fork1
if (pid == 0) {
    fork()        // fork2
}
fork()            // fork3

Step 1: 画出进程树
                P(初始)
           ╱           ╲          ← fork1
         P(子1)         P(父)
       ╱       ╲         ╲       ← fork2（只有子1执行）
   P(孙1)     P(子1续)    P(父)
      |          |          |     ← fork3（所有进程都执行）
   P(曾孙1)   P(孙2)     P(子2)

Step 2: 统计进程总数
2³ = 8个进程（3次fork，最外层没有if限制）

Step 3: 分析每个进程的输出
   按执行路径逐条跟踪...
```

#### ⚠️ 常见陷阱

| 陷阱 | 说明 |
|------|------|
| `if (pid == 0)` 内的 fork | 只有子进程执行，父进程跳过 |
| `wait(NULL)` | 父进程等待**任意**子进程结束 |
| 嵌套的 `wait` | 每个 `wait` 只能回收一个子进程 |
| 没有 wait | 子进程变成僵尸进程（Zombie） |

#### 💡 wait 的个数

父进程有几个子进程，就应该有几个 `wait()`，否则会产生僵尸进程。

---

### 2.4 Ext2 inode 最大文件计算（★★★★★ 每年必考）

#### 本质理解

inode 就像文件系统的"目录索引卡"，有15个格子放地址。前12个直接指向数据块，后3个是间接指向（指向一个"指针块"，指针块再指向数据块）。

#### 计算公式速记

```
块大小 = 4KB = 4096 字节
指针大小 = 4 字节
每块指针数 = 4096 / 4 = 1024 个

最大文件：
  直接：12 × 4KB = 48KB
  一次间接：1024 × 4KB = 4MB
  二次间接：1024² × 4KB = 4GB
  三次间接：1024³ × 4KB = 4TB
  ──────────────────────────────────
  总计：48KB + 4MB + 4GB + 4TB ≈ 4.004 TB
```

> **注意**：如果块大小是 1KB、指针 4B，则每块 256 个指针，结果约 16GB+（你的知识点总结里那个）。

#### 📐 解题模板：给定文件大小，问需要哪些地址项

```
题目：块大小=4KB，指针=4B，文件=260KB

Step 1: 计算文件占多少数据块
260KB / 4KB = 65 块

Step 2: 检查直接索引能装多少
直接索引 = 12 块 → 只能装 48KB，不够

Step 3: 一次间接能装多少
一次间接 = 1024 块 → 1024 × 4KB = 4MB
12 + 1024 = 1036 块，65 ≤ 1036，所以只需要一次间接

结论：使用直接索引12个 + 一次间接1个（其中用到 65-12=53 个指针）
```

#### ⚠️ 易错点

- 问"需要几次磁盘访问"：直接索引=1次，一次间接=2次（先读指针块，再读数据块），二次间接=3次...
- 在计算最大文件时，**不要忘了加直接索引的12块**

---

## 三、核心概念 · 快速记忆法

### 3.1 操作系统四大特征

```
口诀：共、享、虚、异 → "恭喜虚拟"
- 共（并发）：宏观并行，微观串行
- 享（共享）：资源共享
- 虚（虚拟）：物理实体变为逻辑多个
- 异（异步）：进程走走停停
```

### 3.2 CPU 双模式

```
用户态 →→→ 中断/异常/系统调用 →→→ 内核态
内核态 →→→ PSW切换 →→→ 用户态

记忆：用户态进内核态叫"陷入"（trap），内核态回用户态叫"恢复"
```

### 3.3 中断分类速记

```
中断(Interrupt)
├── 硬中断（外部，异步）
│   ├── I/O中断（键盘敲一下）
│   └── 时钟中断（OS拿回控制权的关键！）
│
└── 软中断/异常（内部，同步）
    ├── 故障（Fault）：缺页异常（可恢复）
    ├── 陷阱（Trap）：系统调用 int 0x80
    └── 终止（Abort）：除零错误（不可恢复）
```

> **必考提问**：OS 如何获得 CPU 控制权？→ **时钟中断！** 时间片到了，时钟中断触发，OS 重新调度。

### 3.4 ELF 内存布局

```
从低地址到高地址：
┌─────────────────┐
│   .text 代码段    │  ← 只读，存放指令
│  .rodata 只读数据  │  ← 字符串常量等
│  .data 初始化数据   │  ← 已初始化全局变量
│   .bss 未初始化数据  │  ← 不占磁盘！只占内存
├─────────────────┤
│   heap（堆）↑     │  ← malloc 分配
├─────────────────┤
│        ↓         │
│   stack（栈）     │  ← 局部变量、函数调用
└─────────────────┘
```

**记忆 trick**：堆向上长（像树往上），栈向下长（像水往下流）。

### 3.5 死锁四个必要条件

```
口诀：互、请、不、循 → "互请不循"（互相请求，不能循环）
1. 互斥（Mutual exclusion）
2. 请求和保持（Hold and wait）
3. 不可抢占（No preemption）
4. 循环等待（Circular wait）

破坏任意一个 → 死锁预防
```

### 3.6 页面置换算法特征速记

```
OPT  → 看未来，最晚用的 → 不可能实现（除非能预知未来）
LRU  → 看过去，最久没用 → 硬件开销大
FIFO → 看时间，最早进入 → 会有 Belady 异常
Clock→ 看引用位，转圈查 → 近似 LRU
LFU  → 看次数，最少使用 → 可能被"热数据"刷掉
```

---

## 四、易混淆概念辨析

### 4.1 并发 vs 并行

| | 并发 | 并行 |
|--|------|------|
| 含义 | 同时**处理**多个任务 | 同时**执行**多个任务 |
| 核心 | 宏观上同时，微观上交替 | 真正的同一时刻都在运行 |
| 硬件 | 单核 CPU | 多核 CPU |
| 类比 | 一个人交替做几件事 | 几个人同时做各自的事 |

### 4.2 进程 vs 线程

| | 进程 | 线程 |
|--|------|------|
| 资源拥有 | 独立地址空间 | 共享进程资源 |
| 切换开销 | 大（需切换页表等） | 小（只需切换栈） |
| 通信方式 | IPC（管道、共享内存等） | 直接读写共享数据 |
| 独立性 | 一个崩溃不影响其他 | 一个线程崩溃→整个进程崩溃 |

### 4.3 死锁 vs 饥饿

| | 死锁 | 饥饿 |
|--|------|------|
| 状态 | 多个进程**互相等待**，都无法前进 | 一个进程**一直等不到**资源 |
| 是否可以解除 | 必须外部干预（终止进程/抢占资源） | 其他进程释放资源后可自动解除 |
| 典型场景 | 银行家算法中不安全状态 | SJF 中长作业一直得不到 CPU |

### 4.4 静态链接 vs 动态链接

| | 静态链接 | 动态链接 |
|--|---------|---------|
| 时机 | 编译时 | 运行时 |
| 文件大小 | 大（包含所有库代码） | 小（运行时加载） |
| 更新库 | 需重新编译 | 直接替换库文件 |
| 依赖 | 无外部依赖 | 需要 .so/.dll 文件存在 |

### 4.5 全局置换 vs 局部置换

| | 全局置换 | 局部置换 |
|--|---------|---------|
| 范围 | 从所有进程中选页面换出 | 只从当前进程的页面中选 |
| 公平性 | 进程可能互相影响 | 每个进程独立管理 |
| 抖动控制 | 难控制 | 容易控制 |

### 4.6 固定分区 vs 动态分区 vs 分页

| | 固定分区 | 动态分区 | 分页 |
|--|---------|---------|------|
| 内部碎片 | ✓ | ✗ | ✓（最后一页） |
| 外部碎片 | ✗ | ✓ | ✗ |
| 管理复杂度 | 低 | 中 | 高 |

---

## 五、代码题考点精讲

### 5.1 fork 代码分析

#### 经典模式 1：单次 fork

```c
pid_t pid = fork();
if (pid == 0) {
    // 子进程代码
} else {
    // 父进程代码，pid 是子进程的 PID
}
```

**输出特点**：
- 子进程和父进程的代码都会执行
- 子进程从 fork() 返回处开始执行（不是从 main 开始）
- "A\n" 只有一个进程输出，"B\n" 和 "C\n" 各输出一次（因为 fork 后两个进程都继续）

#### 经典模式 2：嵌套 fork

```c
pid_t pid1 = fork();        // fork1
if (pid1 == 0) {
    pid_t pid2 = fork();    // fork2 — 只在子进程中执行
    if (pid2 == 0) {
        // 孙进程
    }
}
```

**进程树分析**：
```
      P（初始）
     /        \        ← fork1
   P(子1)    P(父)
    /           \       ← fork2（仅在子1中执行）
 P(孙)       P(父)
```

#### 经典模式 3：fork + wait

```c
for (int i = 0; i < 3; i++) {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        exit(0);
    }
}

// 父进程需要回收所有子进程
while (wait(NULL) > 0);
```

**重点**：循环中的 fork，子进程 exit 后，父进程用 `wait` 回收。总计 `2³ = 8` 个进程。

### 5.2 生产者消费者（多线程同步）

```c
sem_t mutex, empty, full;

void *producer(void *arg) {
    // 生产数据...
    sem_wait(&empty);    // 等空位
    sem_wait(&mutex);    // 加锁
    // 放入缓冲区
    sem_post(&mutex);    // 解锁
    sem_post(&full);     // 通知消费者
}

void *consumer(void *arg) {
    sem_wait(&full);     // 等数据
    sem_wait(&mutex);    // 加锁
    // 取出数据
    sem_post(&mutex);    // 解锁
    sem_post(&empty);    // 通知生产者
}
```

**⚠️ 注意顺序**：P(empty) 必须在 P(mutex) 之前！否则可能死锁。
——如果先锁 mutex 再等 empty，而缓冲区满了，生产者拿不到 empty，消费者进不来临界区拿数据，死锁。

### 5.3 Shell 基本框架

```c
while (1) {
    printf("$ ");
    fgets(cmdline, MAXLINE, stdin);
    
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程执行命令
        execvp(argv[0], argv);
        exit(0);
    }
    
    // 父进程等待
    wait(NULL);
}
```

### 5.4 守护进程创建（5步法）

```c
// 第1步：fork + 父进程退出
pid_t pid = fork();
if (pid > 0) exit(0);

// 第2步：创建新会话
setsid();

// 第3步：再 fork 一次，防止重新获得控制终端
pid = fork();
if (pid > 0) exit(0);

// 第4步：改变工作目录
chdir("/");

// 第5步：重设文件权限掩码
umask(0);
```

**问**：为什么要 fork 两次？
**答**：第一次 fork 后 setsid() 创建新会话，但会话首进程可能重新获得控制终端。第二次 fork 的子进程不是会话首进程，**永远无法**获得控制终端。

---

## 六、问答题高频考点

### 6.1 什么是操作系统？它的四大特征是什么？

**答题模板**：
1. 操作系统是管理计算机硬件和软件资源的系统软件
2. 四大特征：并发、共享、虚拟、异步（各用一句话解释）

### 6.2 中断在操作系统中的作用？

**核心要点**：
- 中断是实现**并发执行**的基础
- **时钟中断**是操作系统**获得 CPU 控制权**的关键机制
- 中断使 CPU 能够**响应外部事件**（I/O 完成、硬件故障等）
- 系统调用通过**软中断（trap）**实现用户态→内核态切换

### 6.3 比较几种 I/O 控制方式

| 方式 | CPU介入 | 传输单位 | 并行性 | 硬件 |
|------|---------|---------|--------|------|
| 程序直接控制 | 全程忙等 | 字 | 无 | 最简单 |
| 中断驱动 | 每个字中断一次 | 字 | 有 | 较简单 |
| DMA | 每块中断一次 | 块 | 高 | 较复杂 |
| 通道 | 程序级 | 块/组 | 极高 | 最复杂 |

**记忆线索**：从上到下，CPU 越来越省事，硬件越来越复杂，传输效率越来越高。

### 6.4 什么是抖动（Thrashing）？如何解决？

**定义**：分配的页框数太少，导致频繁缺页，CPU 大部分时间花在页面置换上，利用率急剧下降。

**原因**：工作集 > 分配的页框数

**解决方案**：
1. 增加分配页框数（工作集模型）
2. 采用局部置换（不让进程互相影响）
3. 降低多道程序度

### 6.5 SPOOLing 如何将独占设备改造为共享设备？

**原理**：
```
                    输入井
用户进程1 ╲               ╱ 预输入程序
用户进程2  ╲── 磁盘存储 ─╱   （把磁盘当"大缓冲区"）
用户进程3  ╱  (输入/输出井) ╲ 缓输出程序
                    输出井
```

**核心思想**：用磁盘（共享设备）做缓冲，把独占设备（打印机）的逻辑使用变成对磁盘文件的读写。用户觉得打印机是自己的，实际上是 SPOOLing 系统在排队管理。

### 6.6 段式 vs 分页 vs 段页式

| | 分段 | 分页 | 段页式 |
|--|------|------|--------|
| 出发点 | 用户视角，逻辑模块 | 系统视角，物理管理 | 兼具两者优点 |
| 大小 | 可变（由逻辑决定） | 固定（4KB等） | 段可变，段内页固定 |
| 碎片 | 外部碎片 | 内部碎片（最后一页） | 无外部碎片 |
| 共享/保护 | 方便 | 不方便 | 方便 |
| 地址结构 | 段号+偏移 | 页号+偏移 | 段号+页号+偏移 |
| 访存次数 | 2次 | 2次（有TLB可减） | 3次 |

### 6.7 优先级反转

**场景**：高优先级进程（H）等待低优先级进程（L）持有的资源，而中等优先级进程（M）抢占了 L 的 CPU，导致 H 被 M 间接阻塞。

**解决方案**：优先级继承 — L 持有的资源被 H 需要时，L 临时继承 H 的优先级，运行完释放资源后再降回去。

---

## 七、考前速查表

### 7.1 公式速查

| 公式 | 适用场景 |
|------|---------|
| 周转时间 = 完成时间 - 到达时间 | CPU 调度 |
| 带权周转时间 = 周转时间 / 运行时间 | CPU 调度 |
| 响应比 = 1 + 等待时间 / 运行时间 | HRRF 调度 |
| EAT = λ + t + (1-a)·t | TLB 有效访问时间 |
| 每块指针数 = 块大小 / 指针大小 | inode 计算 |
| 加速比 S = T串行 / T并行 | 并行计算 |
| Amdahl: S = 1 / ((1-f) + f/P) | 并行计算加速比上限 |
| 磁盘访问时间 = 寻道 + 旋转延迟 + 传输 | I/O 性能 |
| 平均旋转延迟 = 1/2 × 转一圈时间 | 磁盘性能 |

### 7.2 数值速查

| 场景 | 典型值 |
|------|--------|
| 页大小 | 4KB |
| 块大小（Ext2） | 1KB 或 4KB |
| 指针大小（地址项） | 4B |
| 时间片（RR） | 几十 ms 级别 |
| 7200RPM 旋转延迟 | 4.17ms（半圈） |

### 7.3 算法时间复杂度速查

| 算法/机制 | 数据结构 | 复杂度 |
|-----------|---------|--------|
| FCFS 调度 | 队列 | O(1) |
| SJF 调度 | 最小堆 | O(log n) |
| CFS（Linux） | 红黑树 | O(log n) |
| 页表查找（无 TLB） | 数组 | O(1) |
| 多级页表 | 树 | O(级数) |
| 倒排页表 | 哈希表 | O(1)平均 |

### 7.4 Linux 常用命令速查

```bash
chmod 755 file    # rwxr-xr-x (7=4+2+1, 5=4+1)
chmod u+x file    # 给所有者加执行权限
ps -ef            # 查看所有进程
top               # 动态查看进程
kill -9 PID       # 强制终止进程（SIGKILL）
grep pattern file # 搜索文件内容
strace ./prog     # 跟踪系统调用
gdb ./prog        # 调试程序
```

---

## 附录：考前最后一天做什么

### 8小时冲刺计划

```
第1小时：复习四大计算题模板（本笔记 2.1-2.4）
第2小时：做预测卷的计算题部分
第3小时：复习进程树分析和代码题（第五部分）
第4小时：做预测卷的代码分析题
第5小时：复习概念辨析和问答题（第三、四、六部分）
第6小时：做预测卷的问答题
第7小时：错题回顾 + 速查表记忆
第8小时：放松！早点休息
```

### 考场检查清单

- ✅ 计算题先做（分值高、确定性大）
- ✅ 画进程树时标注字母/输出
- ✅ 缺页次数计算时，初始空页框**算**缺页
- ✅ SCAN/C-SCAN 注意方向
- ✅ CPU 调度画甘特图辅助
- ✅ 地址转换要写出每一步的数值
- ✅ 简答题分点作答，关键词突出

---

> **最后的话**：操作系统不是死记硬背的科目，理解"为什么"比记住"是什么"重要得多。
> 遇到问题时，想想"如果是 Linux，它会怎么做？"——这是最好的学习方式。
>
> 祝考试顺利！🎯
