一、设备与磁盘(第 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_uringmmap 连拷贝也省掉,才能进一步压。)

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)。

  1. inode
  2. 二级间接块(拿到一级间接块的块号)
  3. 一级间接块(拿到数据块的块号)
  4. 数据块

共 4 次。 加上路径查找(第 26 篇:每深一层两次),一个 /home/u/big.dat 还要再加 6 次,总共 10 次

⭐ 在第 1 题那块 5400 转的盘上,10 次随机读 = 176 毫秒——读一个字节要将近 0.2 秒。这就是为什么 dentry cacheinode 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%。列出至少三种可能。

  1. 有被删掉但还开着的文件(第 25 篇实测过)。du 走目录树,看不到没有名字的 inode;df 看的是块的账。lsof +L1 能列出来。 最常见的场景:日志被 rm 了但服务还开着。
  2. 有目录被挂载盖住了。 /data 下原本有 30 GB 文件,后来在 /data 上挂了一块新盘——那 30 GB 还在,但 du 走不到。mount --bind / /mnt 之后去 /mnt 下看。
  3. 稀疏文件反过来的情况——不,稀疏文件是 du 小于 ls -l,和这里方向相反。真正相关的是文件系统预留:ext4 默认给 root 留 5%,df 算进"已用"。
  4. inode 用光df -i)——这不会造成上面的数字差,但会造成另一个困惑:“有空间却建不了文件”。
  5. 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 核的配额。后果:

  1. 大量上下文切换——16 个线程抢 2 核的配额(第 4、7 篇)。
  2. 配额被更快地耗尽 → 更早被冻住(第 7 篇:cpu.max 靠冻住你来执行)。16 个线程一起跑,25 毫秒的配额几个毫秒就用完了,然后全体冻 90 多毫秒。 延迟尖刺比单线程时更严重。

修法:

  • 应用层:显式设置线程数(-XX:ActiveProcessorCount)。现代 JVM(10+)能自己读 cgroup,所以升级 JVM 也算一种修法。
  • 平台层:装 lxcfs,让容器里的 /proc/cpuinfonproc 反映 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.statnr_throttledthrottled_usec
缺页 / 换出(一次缺页几毫秒) 第 15 篇 /proc/<pid>/stat 的主缺页数、vmstat 的 si/so
内存回收(分配时被迫同步回收) 第 15 篇 memory.statpgscan/pgstealmemory.eventshigh
锁竞争 / 优先级反转 第 18、21 篇 卡住时抓栈;锁等待时间的分布
fsync 等落盘 第 27 篇 strace -T -e fsynciostat 的 await
SSD 垃圾回收 第 24 篇 ⚠️ 从主机侧几乎看不见——只能靠"盘越满越严重"这个特征反推
TLB / 缓存变冷(进程被迁到别的核) 第 4、13 篇 perf statcontext-switchescpu-migrationsdTLB-load-misses
GC 停顿(如果是托管语言) 第 9 篇提过 运行时自己的 GC 日志
KPTI 让系统调用变贵 第 30 篇 这个是稳定开销,不会造成长尾,但会抬高基线

排查顺序的经验:先看 cgroup 的计数器cpu.statmemory.events)——它们是内核明确记下来的事实,不用猜;⭐ 然后才是需要推断的那些

这道题其实是整门课的索引:一个 p99 长尾,可能来自虚拟化 CPU、虚拟化内存、并发、持久化、隔离——五个部分全都能造成它