11.操作系统原理总结
# 操作系统原理 · 全系列总结
10 篇操作系统核心原理的提炼。每篇一张速通卡、一段展开、一句话带走。
# 0.阅读导航
# 0.1 按场景读
| 你遇到了什么 | 先读哪篇 |
|---|---|
| 服务 P99 延迟飙升,但 CPU 不高 | → §01 进程线程(上下文切换风暴)→ §02 调度策略(CFS 抢占) |
| 多线程写共享变量结果不对 | → §04 同步互斥(锁/条件变量/死锁)→ §03 进程通信(IPC 选型) |
| 进程内存持续增长直到 OOM | → §05 内存管理(malloc 碎片)→ §06 虚拟内存(缺页/抖动) |
| 删了大文件磁盘空间没释放 | → §07 文件系统(inode + lsof) |
| 高并发连接处理不过来 | → §08 IO 模型(epoll vs select)→ §01 进程线程(协程方案) |
| Docker 容器莫名被 Kill | → §10 容器虚拟化(Namespace/Cgroups)→ §06 虚拟内存(Page Cache 误算) |
| 写内核模块/驱动 | → §09 设备驱动→ §04 同步互斥(原子/锁在内核中) |
| 面试被问操作系统核心原理 | → §01 进程线程 → §05 内存管理 → §06 虚拟内存 → §04 同步互斥 → §08 IO 模型 |
# 0.2 按层次读
第 1 层·进程调度 → §01 进程线程 → §02 处理器调度
第 2 层·并发同步 → §04 同步互斥 → §03 进程通信
第 3 层·内存管理 → §05 内存管理 → §06 虚拟内存
第 4 层·存储系统 → §07 文件系统 → §08 IO 模型
第 5 层·硬件交互 → §09 设备驱动
第 6 层·系统抽象 → §10 容器与虚拟化
# 1.第 01 篇·进程与线程原理
# 1.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | Go 微服务半夜崩溃——P99 延迟从 20ms 飙升到 3s,htop 显示 47 个进程开了 4813 个线程,Load average 96 |
| 暴露问题 | goroutine 不是"无成本"的——大量阻塞 goroutine 堆积触发上下文切换风暴 |
| 核心知识 | 进程五/七状态模型 → PCB(task_struct ≈ 1.7KB) → 线程(用户级/内核级/混合) → 上下文切换代价(线程 1-5μs, 进程 3-10μs) → 协程(栈初始 2KB vs 线程 8MB) |
| 架构主线 | 进程是资源分配单位,线程是 CPU 调度单位,协程是用户态轻量执行体——三者的抽象层级越来越轻 |
| 加深理解 | 你的服务是计算密集型还是 IO 密集型?前者用线程池,后者用协程——选反了性能差百倍 |
# 1.2 展开:从进程到协程的演进
进程五状态:新建→就绪→运行→阻塞→终止。七状态增加"就绪挂起"和"阻塞挂起",表示内存不足时进程被换出到磁盘。
PCB 里存了什么:PID、程序计数器(PC)、寄存器快照、内存管理信息(页表基址)、打开文件表、信号处理表、进程调度参数(优先级/nice 值/时间片)。
线程三种实现:用户级线程(内核不知情,无法利用多核)、内核级线程(创建需系统调用,代价高 20-100 倍)、混合实现(多对多,Go 的 M:N 模型)。
线程 vs 协程:线程栈默认 8MB,协程栈初始 2KB 可动态扩展;线程切换在内核态(≈1-5μs),协程切换在用户态(≈0.1μs);一台机器最多几千线程,但可以有百万协程。
# 1.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| Load average 远大于 CPU 核心数 | 检查线程数 → htop 按线程数排序 → 看是否有线程泄漏 |
| P99 延迟高但 CPU 不高 | vmstat 1 看 cs 列(context switch/秒)——超过 10 万可能是切换风暴 |
| 短时间创建大量线程 OOM | 线程栈 8MB × 1000 = 8GB——改用协程或线程池 |
# 1.4 一句话拎走
进程扛资源、线程抢 CPU、协程扛并发。选对并发模型,比优化代码有效十倍。
# 2.第 02 篇·处理器调度策略
# 2.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | Nginx 被投诉"接口偶尔卡 2-3 秒"——CPU 仅 23%,但 perf top 显示 __schedule 占 15% |
| 暴露问题 | CPU 不忙不代表没问题——cron 后台任务( mysqldump + gzip ) 跟 Nginx worker 抢 CPU,CFS 公平调度不分主次 |
| 核心知识 | FCFS(护航效应)→SJF(需预测)→RR(时间片 10-100ms)→MLFQ(多级反馈)→CFS(完全公平调度,nice 值权重)→实时调度(EDF/RMS) |
| 架构主线 | 调度器在周转时间、响应时间、公平性三者间永恒博弈——CFS 用 vruntime 把 CPU 时间精确到纳秒级分配 |
| 加深理解 | Linux CFS 的 sched_latency=6ms、sched_min_granularity=0.75ms 意味着什么?→ 在 8 核上每个进程至少运行 0.75ms,每 6ms 至少被调度一次 |
# 2.2 展开:CFS 的数学根基
vruntime 的公式:vruntime += 实际运行时间 × (1024 / 进程权重)。nice=0 权重 1024→vruntime=实际时间;nice=5 权重 335→vruntime 涨得快,CPU 分得少。
nice 值的权重差:nice=-20 权重 88761,nice=19 权重 15——极值差 5917 倍。这就是为什么 nice -n -20 能让一个进程吞掉几乎全部 CPU。
MLFQ 五条规则:①高优先级用短时间片 ②低优先级用长时间片 ③用完时间片降级 ④被 IO 唤醒升回高优先级 ⑤定期全部升回最高优先级(防饥饿)。
cgroup CPU 隔离:cpu.shares 按权重分配 CPU 时间。修复方案:给 Nginx 的 cgroup 分配 70% CPU shares,mysqldump 限制到 20%,P99 从 200ms 降到 5ms。
# 2.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| P99 延迟冒尖(CV 很大) | perf top 看 __schedule 占比 → 超过 5% 说明调度是瓶颈 |
| 后台任务拖垮在线服务 | cgroup 隔离 → cpu.shares 给在线服务 80%+ |
| RT 进程占满 CPU | sched_rt_runtime_us=950ms/秒→ 实时调度最多占用 95% CPU |
# 2.4 一句话拎走
CFS 的公平是对进程而言的,不是对用户而言的。在线服务的"公平"是低延迟,批处理的"公平"是高吞吐——cgroup 能在 CFS 之上实现"不公平的公平"。
# 3.第 03 篇·进程间通信机制
# 3.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 日志采集 Agent 内存飙升到 7.2GB,触发 OOM Killer——根因是管道通信阻塞 + Agent 开了巨大应用层缓冲 |
| 暴露问题 | 不是说"管道就够了"——管道默认缓冲区 64KB,高吞吐场景会饿死消费者 |
| 核心知识 | 管道(匿名/FIFO)→消息队列(POSIX)→共享内存(shmget/mmap)→信号量(System V/POSIX)→信号(Signal)→Socket(UDS/TCP)→mmap |
| 架构主线 | IPC 六种机制的取舍标准就两条:要不要跨网络、对速度的极致程度——共享内存快管道 60 倍,UDS 快 TCP 回环 2-3 倍 |
| 加深理解 | 你用过 Redis 的 UNIX domain socket 吗?——同一台机器上用 UDS 替代 TCP,延迟从 50-100μs 降到 ~30μs |
# 3.2 展开:IPC 选型决策树
| 机制 | 速度 | 消息边界 | 双向 | 跨网络 | 持久化 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|---|---|
| 无名管道 | 中 | 流式 | 半双工 | ❌ | ❌ | 低 | ps aux \| grep nginx |
| 命名管道 | 中 | 流式 | 半双工 | ❌ | ❌ | 低 | 无亲缘进程通信 |
| 消息队列 | 中 | ✅ | ✅ | ❌ | ✅ | 中 | 固定格式消息 |
| 共享内存 | 极快 | 需约定 | ✅ | ❌ | ❌ | 高 | ≤10MB/s 高吞吐 |
| 信号量 | 快 | N/A | N/A | ❌ | ❌ | 中 | 配合共享内存的锁 |
| UDS | 快 | 流式 | 全双工 | ❌ | ❌ | 中 | 同机进程间 |
| TCP Socket | 慢 | 流式 | 全双工 | ✅ | ❌ | 低 | 跨网络通信 |
共享内存为什么快 60 倍:管道和消息队列走系统调用→内核缓冲区→拷贝到用户空间(至少 2 次拷贝)。共享内存直接 mmap 同一块物理内存,读写就是普通内存操作,没有拷贝开销。
日志采集的实践方案:< 2MB/s 用管道(简单),< 10MB/s 用 UDS(平衡),≥ 500MB/s 用共享内存环形缓冲(极致)。最终方案用共享内存环形缓冲:内存从 7.2GB 降到 50MB,吞吐反升 10 倍。
# 3.3 一句话拎走
IPC 选型的本质是在速度、复杂度、跨网络三者间做取舍。同机用共享内存,跨机老实走 TCP。
# 4.第 04 篇·同步与互斥机制
# 4.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 演唱会售票系统超卖——同一座位被卖两次。代码是简单的"先查后改",单线程 100% 通过,并发下 ~0.1% 复现 |
| 暴露问题 | SELECT 和 UPDATE 之间不是原子的——竞态条件不靠"测试充分"发现,靠理解并发模型避免 |
| 核心知识 | 硬件原子(TAS/CAS)→内核(futex)→应用(Mutex/CondVar/Semaphore)→死锁四条件+银行家算法→内存序(acquire/release/seq_cst) |
| 架构主线 | 并发控制三层金字塔:底层硬件保证单条指令原子性,中间 futex 让内核仲裁等待,顶层条件变量让线程"等什么而不是等多久" |
| 加深理解 | mutex.lock() 在无竞争时只是一个原子操作(~25ns),有竞争时才陷入内核(futex等待);如果先无竞争后竞争也是 ~25ns,没有历史包袱 |
# 4.2 展开:从原子操作到死锁防线
Herlihy 共识数:read/write=1(TAS 级别)、CAS/LL/SC=∞(可实现任何并发原语)。CAS 是并发编程的"元操作"——lock-free 队列、无锁栈、乐观锁全部基于 CAS。
自旋锁的代价模型:临界区 < 1μs 选自旋锁(省去内核切换),> 10μs 选互斥锁(不浪费 CPU 空转),1-10μs 是最纠结的灰色地带。实际工程中自旋锁主要用于内核中断上下文(不能睡眠)。
死锁四条件 + 预防四法:互斥(不可剥夺)、持有并等待(一次性申请)、不可抢占(可剥夺)、循环等待(定义锁顺序)。预防死锁只需打破任一条件。
x86 vs ARM 内存一致性:x86 是 TSO(Total Store Order),store-store 有序;ARM 是弱内存序,几乎所有读写都可能乱序。CAS 在 x86 自带全屏障(lock cmpxchg),ARM 需要显式 dmb 屏障。
售票修复方案:UPDATE tickets SET status='sold' WHERE id=? AND status='available'——用受影响行数判断成功。500 QPS 超卖归零,吞吐 4000 QPS。
# 4.3 诊断信号灯
| 信号 | 行动 |
|---|---|
jstack 看到大量 BLOCKED 线程 | ThreadMXBean.findDeadlockedThreads() → 找到锁图 → 切大锁为小锁 |
| 进程突然 hang 住不出日志 | gdb attach → info threads + thread apply all bt 看阻塞点 |
| 吞吐量随核数增加反而下降 | 检查是否有全局热锁(如全局大锁保护小操作) |
| 偶尔超卖/少卖 ~0.1% | 检查"先查后改"——改成 CAS 乐观锁 |
# 4.4 一句话拎走
CAS 乐观锁解决"先查后改"的竞态条件,死锁预防解决"拿着锁等锁"的结构性风险。掌握这两点,并发编程通过一半。
# 5.第 05 篇·内存管理的原理
# 5.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | C++ 数据分析服务每 2 小时 RSS 从 200MB 涨到 8GB 被 OOM Kill——new/delete 已配对,但 ptmalloc 缓存了碎片 |
| 暴露问题 | free() 不意味着内存还给 OS——ptmalloc 的 tcache/fastbins 预缓存机制导致堆顶有长生命周期对象时 brk 无法收缩 |
| 核心知识 | 连续分配(First/Best/Worst Fit)→分页管理(四级页表)→TLB(64-1024 条目)→分段→ptmalloc Bins(tcache/fastbins/smallbins/largebins)→Buddy 系统→Slab 分配器 |
| 架构主线 | 内存管理的本质是解决离散物理内存→连续逻辑空间的映射——分页用 MMU 做翻译,分段给程序员一个视角,ptmalloc/Buddy/Slab 是不同层面的分配器 |
| 加深理解 | free(p) 后为什么不要用 p?——ptmalloc 可能合并或复用这块内存,此时 p 指向的内容已经不可预测 |
# 5.2 展开:物理内存的五层分配器
Buddy 系统(伙伴系统):内核管理整个物理页框(4KB)。分配 2^order 个连续页框——找不到就分裂大伙伴,释放时合并。order 0(4KB)到 order 10(4MB)。外部碎片几乎为零但不适合分配小块。
Slab 分配器:在 Buddy 之上,为内核对象(如 task_struct 1.7KB、inode)预缓存。三层结构:Cache→Slab→Object。释放对象不真正释放内存,放入 Slab 空闲链表下次复用。
ptmalloc 的 Bins 体系:
- tcache(每线程缓存):16-80B,无锁,最快
- fastbins(LIFO):16-80B,不合并,小块高频分配
- smallbins(FIFO):<512B,64 个 bin
- largebins:≥512B,63 个 bin,有序链表
- unsorted bin:释放后暂存,后续按大小分流
brk() 为什么泄漏:malloc(128KB 以下)→brk() 扩展堆顶,free() 后如果堆顶有长生命周期对象无法收缩。默认 >128KB 走 mmap()→free() 时 munmap() 归还。malloc_trim() 强制回收堆顶碎片。
修复方案:换 jemalloc(每线程 arena 减少锁竞争) + 定期 malloc_trim(),RSS 从 8GB 稳定到 500MB。
# 5.3 一句话拎走
malloc/free看起来简单,但它背后是 五层分配器(Bins/Buddy/Slab/Brk/mmap)的复杂协作。理解这五层,才能写出不泄漏的程序。
# 6.第 06 篇·虚拟内存机制
# 6.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | K8s 中 PG Pod 反复 OOM Kill(一小时 8 次),limit=8GB 但 top 显示仅用 6.2GB——Root Cause 是 CronJob 把 Page Cache 撑爆了 |
| 暴露问题 | free 命令的 free 列是没意义的——内核把空闲内存全用作 Page Cache,真正的可用内存是 available 列 |
| 核心知识 | 请求调页(按需加载)→缺页中断(Minor/Major/COW)→页替换算法(OPT/LRU/Clock/增强 Clock)→工作集模型→抖动(Thrashing)→Linux 水位线(high/low/min)+LRU 双链表 |
| 架构主线 | 虚拟内存做三件事:①让每个进程以为独占 128TB 空间 ②只加载用到的页(请求调页) ③物理内存满了驱逐冷页(页替换)——一个完美的"骗局" |
| 加深理解 | vmstat 1 里 bi(块读入)和 wa(IO等待百分比)——如果 wa > 10,说明磁盘正在被用作"慢速内存",系统已经进入抖动 |
# 6.2 展开:缺页中断的完整流程
缺页中断各步骤耗时:TLB miss(1-2μs)→查页表(10-200ns)→触发缺页中断→在物理内存分配页框→从磁盘读入数据(5-10ms,占 99.9%)→更新页表→返回到触发指令重执行。
缺页的三种类型:
- Minor(发生最多):页在物理内存中但未映射(如 COW 复制),无磁盘 IO
- Major(最慢):页已被换出到磁盘,需要磁盘 IO → 一次 5-10ms
- COW(写时复制):fork 后父子共享页标记只读,任一写入触发复制
为什么 Belady 异常只发生在 FIFO:Page Fault 随帧数增加反而更多——因为 FIFO 的替换决策"只按年龄不看使用频率"。LRU/Clock 无此问题。
增强 Clock 的二维决策:(访问位, 修改位)四档优先级:(0,0)最优先替换→(0,1)→(1,0)→(1,1)最优先保留。被修改过的页(脏页)替换需要先写回磁盘,代价是干净页的 2 倍。
工作集模型(Denning 1968):进程正常运行时需要的页框数叫"工作集"。当系统总工作集 > 物理页框总数 → 抖动。解决方案:减少并发进程或升级物理内存。
Linux 水位线:内存可用量低于 low→启动 kswapd 后台回收→降至 min→阻塞用户进程强制回收→低于 high 停止。
修复 PG 的抖动:设置 PG 容器 memory.limit=8GB + swappiness=10(减少 swap)、CronJob 容器 memory.limit=1GB。P99 从 500ms 降到 8ms,OOM 归零。
# 6.3 诊断信号灯
| 信号 | 行动 |
|---|---|
wa(IO等待)>10% + bi(块读入)持续很高 | 系统正在抖动 → 先确认哪个进程缺页最多( pidstat -r -p PID 1 ) |
free 显示 free 很低但 available 很高 | 正常——free 被内核用做 Page Cache。看 available 才是有效可用内存 |
| K8s Pod 被驱逐但 limit 没用满 | Page Cache 误算——container_memory_working_set_bytes 不包含 Inactive File Cache |
# 6.4 一句话拎走
虚拟内存是操作系统最伟大的"骗局"——每个进程以为自己独占 128TB,实际上共享着有限的物理页框。理解缺页中断和 LRU 双链表,你就理解了性能和 OOM 的根源。
# 7.第 07 篇·文件系统原理
# 7.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 生产服务器磁盘 99% 满,删了 30GB 旧日志后 df 显示空间没变——lsof \| grep deleted 才发现 Nginx 还持有已删除文件的 fd |
| 暴露问题 | rm 只删了目录项(dentry),只要还有进程持有 fd,inode 和数据块就不释放——"删文件"是删引用,不是删数据 |
| 核心知识 | 文件分配(连续/链接/索引)→inode(12 直接块+间接块→extent tree)→目录结构(硬链接/软链接)→VFS(统一接口)→日志(ordered)→EXT4 vs XFS |
| 架构主线 | 文件系统的职责就两个:名字→数据的映射(目录+inode) 和 空间管理(分配/回收)。VFS 让所有文件系统对上层看起来一样 |
| 加深理解 | 硬链接和软链接到底什么区别?——硬链接共享 inode(同一文件多名字),软链接是独立的快捷方式文件(存的是目标路径,目标没了就断) |
# 7.2 展开:从 inode 到 VFS
inode 的寻址能力:EXT4 每个 inode 有 12 个直接块指针(12×4KB=48KB) + 1 个间接块(1024 个 4KB 块=4MB) + 1 个二级间接块(1024²×4KB=4GB) + 1 个三级间接块(1024³×4KB=4TB)。但现代 EXT4 默认用 extent tree(B+树),每个 extent 描述连续块范围(最多 128MB),对大文件效率远超间接块。
硬链接 vs 软链接:硬链接让两个名字指向同一个 inode(ln file hard),删除任一名字 inode 仍在(引用计数未归零)。软链接是独立的 inode 文件,内容是目标路径(ln -s file soft),目标没了 link 就断了(dangling)。
日志三种模式:data=journal(元数据+数据双写,最慢最安全)→data=ordered(先写数据后写元数据,默认,平衡)→data=writeback(仅元数据日志,最快,断电可能损坏)。EXT4 默认 ordered。
VFS 的桥梁作用:open("/home/file") → VFS 根据 /home 找到挂载的 EXT4 超级块 → EXT4 的 ext4_file_operations.open() → 返回 inode → 创建 struct file → 填充 f_op(针对 EXT4 的 read/write 实现) → 分配 fd 返回用户态。上层代码不关心底层是 EXT4/XFS/NFS——这就是 VFS 的魔法。
EXT4 vs XFS 选型:小文件多用 EXT4(更紧凑),大文件/大目录用 XFS(extent B+树 + 分配组并行)。数据库场景普遍选 XFS。
# 7.3 诊断信号灯
| 信号 | 行动 |
|---|---|
df -h 满但 du -sh / 加起来远小于总容量 | lsof +L1 \| grep deleted——有进程持有已删除文件的 fd |
df -i IUse% 满但空间充足 | inode 耗尽——大量小文件(如 session 文件、PHP session),加大 inode 数或清理 |
touch 报 No space left on device 但空间充足 | inode 满或 fs 错误 → df -i 确认 |
# 7.4 一句话拎走
文件系统的核心抽象就一个:名字→inode→数据块。
rm删名字不删数据,只要 fd 还在进程手里。
# 8.第 08 篇·输入输出模型
# 8.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | Python WebSocket 推送服务 1200 个连接时延迟从 50ms 飙升到 3s——strace 显示 select 消耗 78% CPU,max 1024 个 fd |
| 暴露问题 | select 三个致命缺陷:1024 上限 + 每次全量 fd 拷贝 + O(n) 遍历——连接数从 100 到 10000 不是线性退化,是指数级崩塌 |
| 核心知识 | IO 两阶段(等数据→拷贝数据)→五种模型(阻塞/非阻塞/多路复用/信号驱动/异步IO)→select→poll→epoll(LT/ET)→io_uring→零拷贝(sendfile) |
| 架构主线 | IO 模型的进化主线只有一条:让 CPU 在"等数据"的阶段不去空等——从阻塞浪费一个线程到 epoll 单线程管万级连接 |
| 加深理解 | epoll_wait 返回的不是"哪些事件发生了"?——不,它返回的是"哪些 fd 就绪了",事件触发是"就绪"不是"完成" |
# 8.2 展开:epoll 为什么完胜 select/poll
IO 两阶段模型:阶段一(等数据准备好,~1-100ms)→阶段二(内核拷贝到用户空间,~几μs)。阻塞 IO 两个阶段都阻塞当前线程,非阻塞 IO 阶段一不阻塞但需轮询,IO 多路复用让一个线程等待多个 fd 的阶段一。
select/poll/epoll 量化对比(10000 连接,每秒 100 次事件循环):
| 指标 | select | poll | epoll |
|---|---|---|---|
| 单次拷贝量 | 10000 fd × 最大 fd | 10000 fd | 仅 100 就绪 fd |
| 遍历复杂度 | O(max_fd) | O(n) | O(就绪数)——O(100) |
| 内核维护 | 每次重建 | 每次重建 | 红黑树+就绪链表(事件驱动) |
| 10000 连接耗时 | ~100ms(全部拷贝+轮询) | ~50ms | ~0.1ms(仅处理 100 个就绪) |
LT vs ET:LT(水平触发)只要 fd 就绪就通知(默认,安全)。ET(边缘触发)仅在 fd 状态变化时通知一次(高效,必须循环读到 EAGAIN)。ET 少一次内核态切换但容易漏数据——非阻塞 fd + 循环读是 ET 的必要条件。
io_uring——下一代异步 IO:两大共享环形缓冲区(SQ/CQ),完全 mmap,提交和完成都不进内核。BIO 模拟 AIO→内核 AIO(限制多)→io_uring(统一框架,网络+文件)。适合极高 IOPS 场景(> 10 万 IOPS 时比 epoll/spdk 更优)。
零拷贝 sendfile:sendfile(out_fd, in_fd, &offset, count)——数据从磁盘 DMA→内核 Page Cache→DMA→网卡。2 次 DMA、0 次 CPU 拷贝(相比传统 read+write 的 4 次 DMA+2 次 CPU 拷贝)。
改为 epoll 的效果:不到 20 行改动,延迟从 3s 降到 10ms,CPU 从 80% 降到 5%。
# 8.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| 连接数 > 1000 时 CPU 突然飙高 | 检查是否用了 select/poll → 换 epoll |
| epoll + ET 丢数据 | 检查 fd 是否 NONBLOCK + 循环读到 EAGAIN |
| 磁盘 IO 慢但 CPU 不高 | 大概率是 HDD 随机读写 → 换 SSD(NVMe 20-30μs vs HDD 7-14ms) |
# 8.4 一句话拎走
epoll 的伟大不在于"更快",而在于 O(就绪数)——连接数一万还是十万,只跟活跃连接数有关。
# 9.第 09 篇·设备驱动基础
# 9.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 内核旁路驱动上生产 30 秒后服务器直接死机——硬编码的 DMA 物理地址在生产机器上属于 ACPI 区域,写入破坏了固件表触发 NMI |
| 暴露问题 | 用户态程序崩溃最多 core dump,内核态代码崩溃整台机器陪葬——没有"恢复"选项 |
| 核心知识 | 内核模块(insmod/rmmod)→字符设备(file_operations)→块设备(request_queue)→网络设备(net_device_ops)→Linux 设备模型(bus/driver/device)→内核态专有 API(GFP/Kmalloc/vmalloc/ioremapped) |
| 架构主线 | 驱动 = 把硬件操作用文件操作接口包装:open = 打开设备、read = 读寄存器、write = 写寄存器、ioctl = 发控制命令、mmap = 映射 DMA 缓冲区 |
| 加深理解 | 为什么驱动不能 printf?——驱动跑在内核态,内核栈只有 8KB(线程栈的 1/1000),不能递归、不能大局部变量、不能用浮点 |
# 9.2 展开:驱动的三层模型
字符设备:按字节流访问(串口、键盘、/dev/null)。file_operations 结构体定义 open/read/write/ioctl/mmap 等回调——每个回调对应一个系统调用。
块设备:按块访问(磁盘、SSD),有缓存层(page cache)。随机访问、大小固定(典型 512B/4KB)。请求通过 request_queue + IO 调度器(mq-deadline/kyber/bfq)排序合并。
网络设备:无设备文件,通过 ifconfig/ip 管理。net_device_ops 定义 ndo_open/ndo_start_xmit/ndo_change_mtu。收发通过 sk_buff(Socket Buffer)结构。NAPI 机制在中断密集时切到轮询模式减少 CPU 开销。
Linux 设备模型三要素:bus(总线类型,PCI/USB/I2C)、driver(驱动代码)、device(具体设备)。bus 维护两张链表(drivers+devices)→match 函数遍历两张链表配对→probe 成功后初始化设备。插 USB 设备时系统自动走 match→probe,无需手动加载。
内核开发的五项安全准则:
- 不信任用户态数据:每次
copy_from_user后检查返回值 - 不硬编码物理地址:用设备树(Device Tree)或 ACPI 获取,不用
remap_pfn_range - 中断处理要快:top half 记下数据,bottom half(tasklet/workqueue)慢处理
- 不用浮点数:内核不自动保存 FPU 寄存器,手动保存代价极高
- 锁的嵌套顺序固定:死锁在内核比用户态更糟——没有 kill -9 来救
# 9.3 一句话拎走
用户态死了最多弹个报错,内核态死了整台机器重启。写驱动的第一课是"怕"——怕破坏内存、怕死锁、怕硬编码。
# 10.第 10 篇·容器与虚拟化
# 10.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | K8s 集群 Node 所有 Pod 被驱逐——top 显示内存仅用 60%,但 mmap 的 Page Cache 占用没被释放且被 kubelet 计为"占用" |
| 暴露问题 | 容器 limit 是 Cgroups 管,Node 可用内存是内核算——两个"剩余内存"是不同维度的,Cgroups limit 用完 Pod 被 Kill,Node 内存低 Pod 被驱逐 |
| 核心知识 | VM vs 容器(隔离级别/启动时间 0.5s vs 30s/密度 100:1)→Namespace×7(PID/NET/MNT/UTS/IPC/USER/Cgroup)→Cgroups v1/v2→OverlayFS(Copy-on-Write)→Docker 架构→K8s 编排 |
| 架构主线 | 容器 = chroot + Namespace×7 + Cgroups + OverlayFS + Seccomp/Capability——这五件都不是新技术,Docker 的创新在于把它们打包成一个好用的界面 |
| 加深理解 | docker run 那条命令背后经历了什么?——docker CLI→dockerd API→containerd→containerd-shim→runc→fork 子进程 → clone(NEWNS |
# 10.2 展开:容器的五块积木
Namespace 七把锁:
| Namespace | 隔离什么 | 丢了会怎样 |
|---|---|---|
| PID | 进程号(子进程只看到自己的 PID 1) | 容器里能看到宿主所有进程 |
| NET | 网络栈(独立 IP/路由/iptables) | 容器和宿主共用端口空间 |
| MNT | 挂载点(独立 /proc) | 容器里的 ps 看到宿主进程 |
| UTS | 主机名和域名 | hostname 改容器会改宿主 |
| IPC | 共享内存/信号量/消息队列 | 容器间 IPC 互相干扰 |
| USER | UID/GID(容器 root ≠ 宿主 root) | 容器 root 逃逸直接获得宿主 root |
| Cgroup | cgroup 文件系统视图 | 容器里能看到所有 cgroup 限制 |
Cgroups v2 统一模型:v1 每个子系统独立目录子树(不统一让 limit 复杂),v2 单一树状层级 + /sys/fs/cgroup/cgroup.controllers 列举启用的子系统。memory.high 软限制(节流)、memory.max 硬限制(OOM Kill)。
VM 的纯开销:每个 VM 约 150MB(QEMU 进程 + Guest 内核 + EPT 二级页表) + 500-2000 CPU 周期/次 VM Exit。EPT(Extended Page Table)把 Guest 物理地址再翻译一次到 Host 物理地址——每个内存访问走两次 MMU(TLB miss ×2)。容器 CPU 损失:0%。
安全三层防线:①Capability(剥离 CAP_SYS_ADMIN/CAP_NET_RAW 等)→②Seccomp(限制系统调用白名单,阻止 reboot/clone/mount)→③AppArmor/SELinux(文件/网络访问控制)。
修复 Pod 驱逐:合理设 requests=limits(Guaranteed QoS)避免超分;驱逐信号改为 evictionHard 留足 Buffer——memory.available < 500Mi 就驱逐而不是等 0。
# 10.3 一句话拎走
容器不是轻量级虚拟机——它是 Linux 五个老功能的精美包装。理解这五块积木,Docker 和 K8s 的每一次错都是可解释的。
# 11.全系列·贯通四句话
# 11.1 虚拟化——操作系统的核心魔法
进程以为自己独占 CPU → 调度器分时共享
进程以为自己独占 128TB 内存 → MMU + 页表翻译
程序以为自己直接读写文件 → VFS 桥梁 + 具体文件系统驱动
Docker 以为自己是独立机器 → Namespace×7 + Cgroups
诊断第一问:这个"我以为"在哪个层面被操作系统骗了? 答案通常就是瓶颈所在。
# 11.2 调度无处不在
| 资源 | 调度器 | 核心算法 | 目标 |
|---|---|---|---|
| CPU | CFS | vruntime 红黑树 | 公平分配 CPU 时间 |
| 页框 | LRU/Clock | 双链表 Active/Inactive | 驱逐最不常用的页 |
| IO | 块层调度器 | mq-deadline/bfq | 合并排序减少寻道 |
| 物理内存 | Buddy + Slab | 伙伴分裂/合并 | 最小化外部碎片 |
所有调度器都在追求同一个东西:在有限资源下最大化"有效工作"。
# 11.3 分层隔离——计算机科学的终极方法论
| 层 | 向上隐藏的复杂性 | 抽象出的接口 |
|---|---|---|
| VFS | EXT4/XFS/NFS 的实现差异 | open/read/write/close |
| 块层调度 | HDD/SSD/NVMe 的寻道/块差异 | submit_bio |
| 驱动模型(bus/driver/device) | PCI/USB/I2C 总线差异 | probe/remove |
| 容器 | 内核 Namespace/Cgroups/seccomp 细节 | docker run |
每一层独立演化——EXT4 替换 XFS 时上层代码不改,NVMe 替换 HDD 时文件系统不改。
# 11.4 隔离与共享的永恒张力
操作系统所有设计都在这个天平上摇摆:
| 隔离需求 | 共享代价 |
|---|---|
| 进程独立地址空间(MMU) | TLB miss + 上下文切换(3-10μs) |
| 线程共享地址空间 | 需要锁/条件变量/原子操作保护共享数据 |
| 容器 Namespace 隔离 | 跨容器通信必须走网络(unix socket/TCP) |
| 内核态/用户态分离 | 系统调用开销(~0.1-0.3μs) + 数据拷贝(pipe) |
零隔离(裸机)= 最快但无保护;完全隔离(VM)= 最安全但 150MB+500CPU 周期/VmExit。容器的创新在于找到了两者的甜蜜点。
# 12.全系列数据一页纸
| 篇 | 核心数字 | 含义 |
|---|---|---|
| 01 | 20-100× + 1.7KB | 线程创建快进程 20-100 倍;PCB(task_struct)大小 1.7KB |
| 02 | 5917× + 6ms | nice=-20 和 nice=19 的 CPU 权重差;CFS 调度周期 |
| 03 | 60× + 64KB | 共享内存快管道 60 倍;管道默认缓冲区大小 |
| 04 | 0.1%→5% + ∞ | 竞态条件从偶现到规律复现;CAS 的 Herlihy 共识数 |
| 05 | 5 层 + 128KB | 物理内存五层分配器;brk/mmap 阈值分界 |
| 06 | 5-10ms + 3 种 | 一次 Major Page Fault 的磁盘代价;缺页三类型(Minor/Major/COW) |
| 07 | 4TB + 3 种 | EXT4 间接块寻址上限;日志三种模式(data=journal/ordered/writeback) |
| 08 | 1000× + 0→2 次 | epoll 比 select 快千倍;sendfile 零拷贝(DMA×2, CPU×0) |
| 09 | 8KB vs 8MB | 内核栈 8KB(线程栈的 1/1000)——驱动不能递归不能大局部变量 |
| 10 | 0% + 7× | 容器 CPU 损失 0%(vs VM ~5-20%);Namespace 七种隔离(编号都是 0x02000000 的掩码位) |
# 13.全系列症状诊断表
对照症状,直接跳到对应篇章。
| 症状 | 第一反应 | 第二篇 |
|---|---|---|
| 服务 CPU 不高但 P99 延迟很高 | §01 进程线程(切换风暴) | §02 调度策略(CFS 抢占) |
| 多线程数据偶尔不对 | §04 同步互斥(锁/原子) | §03 进程通信 |
| 进程 RSS 持续增长 | §05 内存管理(malloc 碎片) | §06 虚拟内存(Page Cache) |
| 删了文件空间没释放 | §07 文件系统(lsof + deleted) | |
| 连接数上千后 CPU 飙高 | §08 IO 模型(epoll) | §01 进程线程(协程) |
| Docker 容器 OOM Kill | §10 容器虚拟化(Cgroups) | §06 虚拟内存(缺页/抖动) |
| 服务器 CPU 不高但 load 很高 | §01 进程状态(不可中断睡眠态 D) | §08 IO 模型(阻塞) |
htop 显示内存用满但进程列表正常 | §06 虚拟内存(Page Cache 误判) | §10 容器(Cgroups limit) |
| 线程池开了 500 个线程吞吐反降 | §01 进程线程(切换代价) | §04 同步互斥(热锁争抢) |
用 top 看内存使用率出错 | §06 虚拟内存(free vs available) | §05 内存管理(RSS vs VSS) |
| 内核模块加载后系统硬死 | §09 设备驱动(硬编码/死锁) | |
| 面试问操作系统核心原理 | §01 → §06 → §04 → §08 | §05 → §03 → §07 |
# 14.学习路径建议
# 14.1 半天速通
读 §0 导航 → 每篇只看 速通卡 + 一句话拎走(每篇 2 分钟,10 篇共 20 分钟)。遇到具体场景再展开。
# 14.2 系统建立操作系统直觉(推荐)
§01 进程线程(并发三层) → §02 调度策略(CFS)
→ §04 同步互斥(原子→锁→死锁→CAS) → §03 进程通信(IPC 选型)
→ §05 内存管理(malloc 真相) → §06 虚拟内存(缺页与抖动)
→ §07 文件系统(inode+VFS) → §08 IO 模型(epoll+零拷贝)
→ §09 设备驱动(内核态危险) → §10 容器虚拟化(Namespace+Cgroups)
# 14.3 排查问题时
看 §13 诊断表 定位篇章 → 看速通卡确认方向 → 看信号灯验证假设 → 看展开加深理解。
# 14.4 自测清单
读完 10 篇后能回答以下问题即算掌握:
- 进程和线程的本质区别?协程比线程快在哪?
- CFS 的 vruntime 怎么计算?nice=-20 和 nice=19 的 CPU 差距多大?
- 共享内存为什么快管道 60 倍?UDS 为什么快 TCP 回环 2-3 倍?
- 死锁四条件是什么?怎么预防?CAS 为什么能实现无锁编程?
free()调用后内存为什么不还给 OS?ptmalloc 的 Bins 体系怎么设计的?- Page Fault 的三种类型分别是什么?系统抖动的本质是什么?
rm删大文件为什么df不降?lsof +L1 | grep deleted怎么用?- epoll 为什么完胜 select?ET 和 LT 各适合什么场景?
- 内核态和用户态的核心区别?为什么写驱动不能用
printf? - Docker 的五块积木(最低要求记住 Namespace×7 + Cgroups)分别是什么?