一、设备与磁盘(第 23–24 篇)
1. 一块 5400 转的机械硬盘,平均寻道 12 毫秒,持续传输率 100 MB/s。算它的随机 4 KB 读 IOPS 和顺序 4 KB 读 IOPS。
旋转等待: 转一圈 60/5400 = 11.1 毫秒,平均等半圈 = 5.56 毫秒。 传输 4 KB: 4096/100e6 ≈ 0.04 毫秒。
随机读: 12 + 5.56 + 0.04 = 17.6 毫秒 → 57 IOPS 顺序读: 只有传输时间 0.04 毫秒 → 25000 IOPS
相差约 440 倍。
⭐ 对比第 24 篇算的 7200 转盘(320 倍):转速慢、寻道慢的盘,这个比例更极端。 这就是老式笔记本硬盘上"随便干点什么都卡"的物理原因。
2. 第 23 篇实测每次系统调用约 260 纳秒。一个程序每秒做 100 万次 4 KB 的读。如果改成每次读 64 KB(次数降到 1/16),能省多少 CPU 时间?
改之前: 100 万次 × 260 纳秒 = 0.26 秒/秒——⭐ 26% 的 CPU 花在过门上(还没算数据拷贝)。
改之后: 62500 次 × 2887 纳秒(第 23 篇实测 64 KB 时每次调用的成本)= 0.18 秒/秒。
省了约 0.08 秒/秒,即 8% 的 CPU。
⚠️ 但注意:省得没有"次数降到 1/16"听起来那么多。因为大块读时,每次调用的成本里数据拷贝占了主要部分(2887 纳秒里只有约 260 是固定开销)。⭐ 固定开销只能省掉固定的那部分——这是批量优化的天花板。
(如果用 io_uring 或 mmap 连拷贝也省掉,才能进一步压。)
3. 一块 SSD,标称总写入量 300 TB。你的负载是每秒 1000 次 4 KB 随机写,写放大是 8。这块盘能撑多久?改成每秒 4 次 1 MB 顺序写(同样的数据量),写放大降到 1.1,能撑多久?
随机写: 每秒请求 1000 × 4 KB = 4 MB/s,实际写 4 × 8 = 32 MB/s。 300 TB ÷ 32 MB/s = 9.83e6 秒 ≈ 114 天。
顺序写: 每秒请求 4 MB/s,实际写 4 × 1.1 = 4.4 MB/s。 300 TB ÷ 4.4 MB/s = 7.15e7 秒 ≈ 2.3 年。
同样的数据量,寿命差 7.3 倍。
⭐ 第 24 篇那句"顺序覆盖写的写放大接近 1,小块随机写可以到几十",在这里变成了"你的盘是活 4 个月还是活 2 年"。这也是所有 LSM-tree 存储引擎存在的经济理由。
二、文件系统(第 25–26 篇)
4. 一个文件系统块大小 4 KB,inode 有 12 个直接指针、1 个一级间接、1 个二级间接、1 个三级间接,块指针 4 字节。它能表示的最大文件多大?
一个块能装的指针数: 4096 / 4 = 1024 个。
- 直接:12 × 4 KB = 48 KB
- 一级间接:1024 × 4 KB = 4 MB
- 二级间接:1024 × 1024 × 4 KB = 4 GB
- 三级间接:1024³ × 4 KB = 4 TB
合计约 4 TB + 4 GB + 4 MB + 48 KB ≈ 4.004 TB。
⚠️ 但实际上限往往更小——文件大小字段本身的位数、以及文件系统对总块数的限制,都可能先撞上。
5. 上题的文件系统,读一个 3 GB 文件的最后一个字节,最少要读几次盘(假设什么缓存都没有)?
3 GB 落在二级间接的范围里(超过 48 KB + 4 MB,不到 4 GB)。
- 读 inode
- 读 二级间接块(拿到一级间接块的块号)
- 读 一级间接块(拿到数据块的块号)
- 读 数据块
共 4 次。 加上路径查找(第 26 篇:每深一层两次),一个 /home/u/big.dat 还要再加 6 次,总共 10 次。
⭐ 在第 1 题那块 5400 转的盘上,10 次随机读 = 176 毫秒——读一个字节要将近 0.2 秒。这就是为什么 dentry cache 和 inode cache 不是优化,是必需品。
6. 一个目录 d 里有三个子目录。stat d 显示的链接数是多少?为什么?
5。
- 1 来自父目录里那条
d的记录。 - 1 来自
d自己里面的.。 - 3 来自三个子目录各自的
..。
⭐ 所以有一条经验:一个目录的链接数 = 2 + 它的子目录个数。find . -type d -links 2 能找出所有没有子目录的目录——老式的 find 用这个技巧做剪枝。
(这也是第 74 号实验里"mkdir 三个子目录后父目录链接数应该是 5"那道自查题。)
7. df 说磁盘用了 90%,du -sh / 加起来只有 40%。列出至少三种可能。
- ⭐ 有被删掉但还开着的文件(第 25 篇实测过)。
du走目录树,看不到没有名字的 inode;df看的是块的账。lsof +L1能列出来。 最常见的场景:日志被rm了但服务还开着。 - 有目录被挂载盖住了。
/data下原本有 30 GB 文件,后来在/data上挂了一块新盘——那 30 GB 还在,但du走不到。mount --bind / /mnt之后去/mnt下看。 - 稀疏文件反过来的情况——不,稀疏文件是
du小于ls -l,和这里方向相反。真正相关的是文件系统预留:ext4 默认给 root 留 5%,df算进"已用"。 - inode 用光(
df -i)——这不会造成上面的数字差,但会造成另一个困惑:“有空间却建不了文件”。 du没有权限进入某些目录,它会跳过并(可能)静默。用sudo du。
三、崩溃一致性(第 27–28 篇)
8. 往文件追加一个块要写三个地方(数据位图、inode、数据块)。列出所有 2³ = 8 种落盘组合,说明每种的后果。
记 B=位图、I=inode、D=数据块。
| 落盘的 | 后果 | 严重度 |
|---|---|---|
| 无 | 什么都没发生 | 无 |
| D | 一个块被写了但没人指向、没人占用 | 无(下次会被正常分配) |
| B | 块泄漏——标着已用但没人用 | 轻(浪费空间,fsck 能修) |
| I | ⚠️ inode 指向一个"空闲"的块 → 该块会被分配给别人 → 两个文件共用一块 | 灾难 |
| B+D | 块泄漏 | 轻 |
| B+I | ⚠️ 文件末尾是一段旧垃圾(可能是别人删掉的数据) | 灾难(信息泄露) |
| I+D | ⚠️ 同 I:块会被重复分配 | 灾难 |
| B+I+D | 完整成功 | 无 |
⭐ 8 种里有 3 种是灾难,而且它们全都通不过"这是不是一次原子的更新"这个检验。这就是第 27 篇为什么必须引入日志。
⚠️ 注意 B+I 那一行是 writeback 模式会出的问题,而 ordered 模式强制 D 先于 I/B 落盘,正好堵死它。
9. 为什么日志的提交记录(TxEnd)必须单独写一次,而且要在前面的内容确认落盘之后?如果不这样会怎样?
因为磁盘可以重排写入顺序。
如果你一次性把 TxBegin | 内容 | TxEnd 全交给磁盘,磁盘可能先写了 TxEnd,中间的内容还没落。这时候断电:
重启后你看到一条"有头有尾、看起来完整"的日志,照着它去重做——而它的内容是垃圾。 于是你把原本还好好的文件系统改坏了。
⭐ 一条不完整但被标成完整的日志,比没有日志更危险——没有日志时你至少知道要跑 fsck;有了假日志,你会满怀信心地执行一次破坏。
正确顺序: 写内容 → barrier(等它们真落盘) → 写 TxEnd → barrier → checkpoint。
(第 75 号实验的任务三就是把这条注释掉,让你亲眼看见损坏重新出现。)
10. 一个应用要原子地更新配置文件。写出完整的调用序列,并说明每一步不做的后果。
1. fd = open("conf.tmp", O_WRONLY|O_CREAT|O_TRUNC)
2. write(fd, 新内容)
3. fsync(fd) ← 确保内容真的落盘
4. close(fd)
5. rename("conf.tmp", "conf") ← 原子替换
6. dirfd = open(".", O_RDONLY); fsync(dirfd) ← 确保「目录里这条记录变了」落盘
各步不做的后果:
- 省掉第 3 步: 进程崩溃没事(内容已经在页缓存里,内核会写下去);⚠️ 但机器断电就完了——
rename可能先落盘,于是你得到一个"名字是新的、内容是空的或残缺的"文件。这比原来的旧文件还糟。 - 省掉第 5 步、直接原地覆盖: 第 27 篇实测——92% 的并发读会读到残缺的文件。
- 省掉第 6 步: 断电后
rename本身可能丢失,你回到旧文件(这个结果是安全的),但也可能出现"新名字指向的 inode 已经被回收"这种更糟的情况,取决于文件系统。
⭐ 注意第 3 步和第 5 步防的是两种不同的故障:fsync 防断电,rename 防并发读者。两个都要。
四、隔离(第 29–30 篇)
11. 容器里 nproc 返回 16(宿主机的核数),而 cgroup 只给了 cpu.max = "200000 100000"(2 核)。一个默认按核数开线程池的 JVM 会怎样?该怎么修?
JVM 会开 16 个 GC 线程、16 个 ForkJoinPool 线程,而它实际只有 2 核的配额。后果:
- 大量上下文切换——16 个线程抢 2 核的配额(第 4、7 篇)。
- ⭐ 配额被更快地耗尽 → 更早被冻住(第 7 篇:
cpu.max靠冻住你来执行)。16 个线程一起跑,25 毫秒的配额几个毫秒就用完了,然后全体冻 90 多毫秒。 延迟尖刺比单线程时更严重。
修法:
- 应用层:显式设置线程数(
-XX:ActiveProcessorCount)。现代 JVM(10+)能自己读 cgroup,所以升级 JVM 也算一种修法。 - 平台层:装
lxcfs,让容器里的/proc/cpuinfo和nproc反映 cgroup 的限额。
⭐ 根本原因是第 29 篇那句话:cgroup 限制了你能用多少,但没有改变你看到的数字——限额和视图是两套机制,它们没有对齐。
12. 攻击者在你的容器里拿到了 root。分别在"开了 user namespace"和"没开"两种情况下,说明他能做什么。
没开 user namespace: 容器里的 uid 0 就是宿主机的 uid 0。他被 capabilities 和 seccomp 拦着,但:
- 只要内核里有任何一个提权漏洞能绕过这些检查,他立刻就是宿主机 root。
- 如果有 volume 挂进来,他对那些文件就是以 root 身份操作的。
- ⚠️ 如果容器是
--privileged起的,他现在就已经是宿主机 root 了,一个漏洞都不用。
开了 user namespace: 容器里的 uid 0 映射成宿主机上某个高位普通 uid(比如 165536)。
- 他在容器里是 root,能装包、改
/etc。 - ⭐ 但即使他逃出容器,他在宿主机上也只是一个没有任何权限的普通用户。
- 挂进来的 volume,他只能以那个映射后的 uid 访问。
⭐ 第 30 篇那句话的具体形态:你没法保证内核没有漏洞,那就让"够到之后的收益"变小。 user namespace 就是把收益从"整台机器"降到"一个 nobody"。
五、综合
13. 把这门课里"加一层中间人,用一张映射表"的机制全部列出来,并指出它们各自的映射表存在哪、由谁维护、失效时能不能重建。
| 机制 | 映射什么 | 表在哪 | 谁维护 | 失效能重建吗 |
|---|---|---|---|---|
| 页表(12–14) | 虚拟地址 → 物理帧 | 内存 | 内核 | ⭐ 不能(进程状态就没了),所以断电即失,本来也不需要持久化 |
| TLB(13) | 同上,缓存 | CPU 内 | 硬件 | ⭐ 能(页表还在,重新走一遍) |
| FTL(24) | 逻辑块 → 闪存页 | SSD 里的 DRAM + 闪存 | 盘的固件 | ⚠️ 必须能——所以它要持久化,而且要有自己的崩溃恢复。丢了整盘数据就没了 |
| inode 的块地图(26) | 文件偏移 → 磁盘块 | 磁盘上 | 文件系统 | ⚠️ 不能重建,只能修复(fsck 靠冗余信息猜) |
| LFS 的 imap(28) | inode 号 → 当前位置 | 磁盘上(追加写) | 文件系统 | 能(扫日志重建,但慢) |
| PID namespace(29) | 内层 pid → 外层 pid | 内存 | 内核 | 不能,也不需要 |
⭐ 关键的区分是"这张表本身是不是必须持久化"。 内存里的表可以随时重建或丢弃;磁盘上的表一旦坏了,它指向的数据就等于没了——所以第 27、28 篇那些日志、校验和、写时复制,绝大部分是在保护这张表,而不是保护数据本身。
14. 一个服务的 p99 延迟偶尔飙到 500 毫秒,平均只有 3 毫秒。列出这门课里学过的、所有可能造成这种"长尾"的机制,并给出各自的排查手段。
| 可能原因 | 出自 | 怎么查 |
|---|---|---|
| cgroup CPU 限流(跑 25 毫秒冻 75 毫秒) | 第 7、29 篇 | ⭐ cat /sys/fs/cgroup/cpu.stat 看 nr_throttled 和 throttled_usec |
| 缺页 / 换出(一次缺页几毫秒) | 第 15 篇 | /proc/<pid>/stat 的主缺页数、vmstat 的 si/so |
| 内存回收(分配时被迫同步回收) | 第 15 篇 | memory.stat 的 pgscan/pgsteal,memory.events 的 high |
| 锁竞争 / 优先级反转 | 第 18、21 篇 | 卡住时抓栈;锁等待时间的分布 |
fsync 等落盘 |
第 27 篇 | strace -T -e fsync;iostat 的 await |
| SSD 垃圾回收 | 第 24 篇 | ⚠️ 从主机侧几乎看不见——只能靠"盘越满越严重"这个特征反推 |
| TLB / 缓存变冷(进程被迁到别的核) | 第 4、13 篇 | perf stat 看 context-switches、cpu-migrations、dTLB-load-misses |
| GC 停顿(如果是托管语言) | 第 9 篇提过 | 运行时自己的 GC 日志 |
| KPTI 让系统调用变贵 | 第 30 篇 | 这个是稳定开销,不会造成长尾,但会抬高基线 |
⭐ 排查顺序的经验:先看 cgroup 的计数器(cpu.stat、memory.events)——它们是内核明确记下来的事实,不用猜;⭐ 然后才是需要推断的那些。
这道题其实是整门课的索引:一个 p99 长尾,可能来自虚拟化 CPU、虚拟化内存、并发、持久化、隔离——五个部分全都能造成它。