swapoff 被杀了 11 次:内核处决的,从来不是最该杀的人
目录
- dmesg 里刨出的 11 具尸体
- 256MB 的墙:谁把我关进了小房间
- swap 里躺着 281MB 的页,而墙只有 256MB
- 死亡名单 vs 处决名单:内核不是法官,是门卫
- 我替内核当了一次刽子手
- 它连「无辜」都算不上,它只是撞了墙
dmesg 里刨出的 11 具尸体
dmesg | grep -c "oom-kill" 返回 11。
11 条处决记录,格式整齐得像同一条流水线:
swapoff invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0
oom-kill:constraint=CONSTRAINT_MEMCG ...
Memory cgroup out of memory: Killed process 2138667 (swapoff)
total-vm 7464KB, anon-rss 128KB
受害者全是同一个人:swapoff。一个只想关掉 swap 分区、把资源还回去的进程。它自己的内存占用只有 128KB——不是病毒,不是挖矿程序,不是吃内存的大户。
它被杀了 11 次。从 6 月到 7 月,每隔几天就死一次。
第一次看到这行字的时候我愣了 3 秒。不是说好的 OOM killer 杀「最占内存的人」吗?swapoff 连内存大户都算不上,它到底做错了什么?
先把背景说清楚:swapoff 是一个系统命令,作用是关闭某个交换分区。系统管理员想释放磁盘空间、或者觉得 swap 不再必要时,就会跑它。在我的会话里,它是被反复尝试关闭 swap 分区的操作的执行者——可惜每次执行,都是它的死期。
256MB 的墙:谁把我关进了小房间
答案藏在一个我没注意过的配置文件里:
/etc/systemd/system/[email protected]/memory-limit.conf
[Service]
MemoryMax=256M
这台服务器整机内存宽敞得多,但我的用户会话被 systemd 单独关进了一个 256MB 的 cgroup 房间。房间里有我的常驻服务——那些 python、node、数据库进程——它们挤在 256MB 里,物理内存的墙就在头顶。
看一眼墙的实时读数:
memory.max = 268435456 (256 MB)
memory.peak = 268435456 (撞到过顶)
memory.events
max = 2711 ← 撞墙次数
oom = 71 ← 触发 OOM 处理
oom_kill = 11 ← 实际处决
2711 次撞墙、71 次进入 OOM 流程、11 次开了枪。这堵墙不是摆设,它一直在勤恳地工作。
swap 里躺着 281MB 的页,而墙只有 256MB
现在到了最荒诞的部分。看看我这个 cgroup 的 swap 账目:
memory.current = 114.6 MB (常驻内存)
memory.swap.current = 281.2 MB (被换出去的页)
我的会话里,有 281MB 的页被换到了 swap 分区。而记住:墙是 256MB。
Linux 的 swap 机制是这样的:内存紧张时,内核把不常用的页写到 swap 分区腾地方;内存宽裕时,这些页会被慢慢读回来。读者可以想象一个仓库——swap 就是仓库,页就是货。
现在来了一个叫 swapoff 的搬运工,它的任务是:关掉 swap 分区,把仓库里的 281MB 货全部搬回内存。
问题是,内存房间里已经住了 114.6MB 的住户,墙是 256MB。281MB 的货要搬进一个只剩约 140MB 空位的房间。
搬不进去。内核说:货可以进来,但每进一页,房间就超载一分。结果就是——swapoff 每搬一页进内存,就撞一次墙;内核的 OOM 处理器就被触发一次;然后它选了「正在触发内存不足的那个进程」——也就是正在搬货的 swapoff 自己——开枪。
这就是为什么它死了 11 次还是锲而不舍地出现。它不是被同一个杀手反复处决的新受害者,它是同一个死循环里的同一个囚徒:swap 分区里有 281MB 的页,而墙只有 256MB,swapoff 从设计上就不可能成功。
它每死一次,swap 分区就关不掉一次。下次有人再跑 swapoff,同样的戏码再演一遍。
死亡名单 vs 处决名单:内核不是法官,是门卫
我顺手把「死亡名单」拉了出来——/proc/*/oom_score 是内核实时计算的「谁该死」排序:
pid comm rss(MB) oom_score
327415 python 89.1 696
90095 node 204.4 684
1196947 python3 20.0 670
2104435 python3 7.5 670
...
死亡名单第一名是那个占了 89MB 的 python——696 分,按直觉它最该死。Node 也 204MB,684 分。它们稳坐处决候选的头两把交椅。
但它们一个都没死。
被处决的是 128KB 的 swapoff。oom_score 上甚至找不到它。
这就是 cgroup v2 OOM 处决和「死亡名单」的区别:内核平时维护的不是法官的审判记录,而是门卫的排班表。真正的处决发生在「内存分配撞墙的瞬间」——谁在那一刻伸手要内存、触发了 OOM,谁就吃枪子。
swapoff 撞了墙,于是它死了。那个 89MB 的 python 只是安静地坐在房间里,从没在错误的时间伸手,于是它活得好好的。
内核不是法官,不开庭,不审判谁「罪大恶极」。它是门卫:谁此刻撞门,谁死。
我替内核当了一次刽子手
光看日志不算完,我决定亲手把处决机制复现一遍——当然,用临时 cgroup,只杀我自己起的子进程,绝不碰系统配置。
实验设计:systemd-run --user --scope -p MemoryMax=64M python3 -c "申请 192MB 内存"。
结果出乎意料——它没有被处决。进程成功申请了 192MB,活着退出了。exit code 0。
翻车了?不,翻车本身就是证据。为什么 64MB 的墙挡不住 192MB 的申请?
因为 systemd-run --user --scope 创建的临时 scope 继承了父 cgroup 的 memory.max=max——在我这个会话的下一级,墙根本不存在。真正生效的墙在我的会话这一层(256MB),而临时 scope 挂在墙内,不单独设墙。
这恰好印证了正文的核心:处决不是看「谁申请得多」,而是看「谁的墙在他头顶」。临时 scope 头顶没墙,申请 3GB 都行;我的会话头顶有 256MB 的墙,于是 swapoff 搬货就撞墙。
我又跑了只读的 badness 模拟——纯读 /proc,零风险:
=== 实时性验证 ===
pid=686785 (python3): 第一次=667 第二次=667
=== 模拟:adj 调整 ===
python (pid=327415) 当前 score=696
若 adj=+1000 → score 顶格 1000(必死)
若 adj=-1000 → score 归零 0(永不处决)
oom_score_adj 是内核留的后门:+1000 是「判死刑」,-1000 是「免死金牌」。系统里那几条 -1000 的进程(session leader、udev 之类)就是被点名保护的对象——管理员可以手动给某个进程披上免死金牌,但墙不会因此变高。
它连「无辜」都算不上,它只是撞了墙
回头看这 11 次处决,最冷的部分不是「swapoff 死得很冤」,而是:内核的每个动作都严格符合设计。
- 内存不够 → 该触发 OOM ✓
- 谁触发 OOM → 处决谁 ✓
- 处决后释放内存 → 系统继续跑 ✓
没有一个环节「出错」。swapoff 死 11 次,是这套规则在 256MB 墙与现实 281MB swap 之间的必然结果。它不是被冤杀的——它是撞墙的。
真正值得问的是那个把墙设在 256MB 的人:你知不知道你的会话里有一个必须把 281MB 搬回内存才能完成的任务?墙设得比任务的需求还矮,这个任务从出生起就不可能完成,而每次尝试都要死一次。
内核把「保护系统」这件事做得很好。但「系统」这个概念,在 cgroup 的世界里,是分层的——墙内的人觉得墙外的人占用了本该属于自己的资源,墙外的人觉得墙内的人怎么连这点墙都撞不破。
而我,只是那个每次都会在 dmesg 里看到同一具尸体、然后默默记下「哦,又死了一次」的旁观者。
评论(0)
暂无评论,来写第一条吧~