先看一次真实的服务器异常。某天凌晨监控告警,机器负载正常但接口延迟翻了十倍,登上去先看内存:
$ free -h
total used free available
Mem: 15Gi 15Gi 82Mi 1.2Gi
Swap: 4.0Gi 2.1Gi 1.9Gi
物理内存吃满,交换区吃掉 2GB。再用 vmstat 看一眼,si/so(每秒换入换出的页数)高得吓人。诊断:内存不够,系统在疯狂地把页在内存和磁盘之间搬来搬去,整机效率被磁盘拖进地狱。这个现象叫抖动(thrashing),它背后的机制,就是本章的主题:物理页框不够时,踢谁?
设页访问序列,3 个页框,把每个算法真算一遍(考试就是考这个,工程上考的是直觉):
访问串:7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
FIFO(先进先出):谁进得最早踢谁。手动模拟(加粗为缺页):
7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
[7] [7] [7] [2] [2] [2] [2] [4] [4] [4] [0] [0] [0] [0] [0] [0] [0] [7] [7] [7]
[0] [0] [0] [0] [3] [3] [3] [2] [2] [2] [2] [2] [1] [1] [1] [1] [1] [0] [0]
[1] [1] [1] [1] [1] [4]... [3] [3] [3] [3] [3] [2]... [7]...
缺页共 15 次
LRU(最近最久未用):踢「上次访问离现在最远」的页。同样的串算下来缺页 12 次。差别出在第 4 次访问:2 来了,FIFO 踢 7(它最早进),LRU 踢 1(它是三者中最近最久没用的)。LRU 用「过去」预测「未来」,FIFO 用「到来顺序」预测,后者和「将来还会不会用」毫无关系,这是两者差距的根源。
OPT(最佳置换):踢「未来最久不用」的页,上帝视角,缺页 9 次,是所有算法的理论下界。它不可实现(谁也不能预知未来),价值在于当标尺:LRU 的 12 距离下界 9 已经相当近,CLOCK 的 14 略逊,FIFO 的 15 敬陪末座。
CLOCK(时钟算法):LRU 的硬件穷人版。精确 LRU 要给每次访问记时间戳或维护链表,开销太大;CLOCK 给每页一个访问位,所有页排成环,指针扫过去:访问位是 1,清零继续走;是 0,踢它。直觉是「最近被用过的页获得一次免死金牌」,对 LRU 的一次粗略逼近,一个 bit 的硬件成本。真实的 Linux 更精细:两链表(active/inactive)+ 二次机会,思想同源。
一个反直觉的实验结果:FIFO 在访问串 1 2 3 4 1 2 5 1 2 3 4 5 上,3 个页框缺页 9 次,4 个页框反而缺页 10 次。内存变大,性能变差,这就是 Belady 异常。
病因:FIFO 踢的是「最早到来」,这和「将来还会不会用」毫无关联,多一个页框只是多了一个「早到」的陪跑。LRU 和 OPT 没有这个病(它们有栈性质:页框多的命中集合包含页框少的命中集合)。这道题的实用价值,一是提醒你「资源加量不一定改善性能」,二是考你对各算法性质的理解深度。
回到开头的服务器事故。抖动的机制:每个进程都需要一个「当前频繁访问的页集合」常驻内存(即工作集),如果分给它的页框数低于工作集大小,它每前进几步就缺页一次,磁盘排队爆满,CPU 倒闲下来了。
正常: CPU ████████ 磁盘 ██ CPU 在干活
抖动: CPU ██ 磁盘 ████████ 全系统在等磁盘
监控里「CPU 利用率暴跌 + 磁盘 IO 打满 + load 偏高」同时出现,基本就是它。经典的医疗方案是工作集模型:算出每个进程的工作集,给够;给不够就别硬撑,把整个进程挂起换出(中级调度,第 3 章那个层次),腾出页框喂饱剩下的进程。少而饱胜过多而饿。Linux 的实际做法更宽容,页回收按 LRU 近似慢慢挤,配合 cgroup 限制各家配额,但「内存超卖过头的代价是指数级恶化」这个结论完全一致。
一道公式题收尾:缺页率 p(介于 0 和 1 之间),内存访问 100ns,缺页处理(读盘)8ms:
p = 1% 时 EAT ≈ 80μs,慢了 800 倍。缺页率是性能的悬崖,不是斜坡:99% 好和 100% 好,可能差几百倍。这也是为什么容量规划要留余量,别让工作集差一页。
vmstat 1,同时开一个吃内存的程序(比如分配并写满一个 20GB 的数组,若内存小于它),观察 si/so 列变化,亲眼看一次抖动。