OOM Killer 设计说明
1. 设计背景
OOM killer 的目标不是“让一次分配成功”,而是在系统已经无法满足内存请求时,选择一个最可能释放足够内存、且相对合适的用户进程终止它,避免内核在同一个失败路径上无意义重试、死循环或系统级死锁。
DragonOS 当前已经有基础页帧分配器、页缓存回收线程、用户态缺页处理和进程退出释放地址空间的能力,但还没有 Linux 6.6 那种完整的页分配慢速路径、reclaim、memcg、NUMA/cpuset OOM 域、OOM reaper 与内存水位联动。因此当前 OOM killer 的第一阶段定位是:
- 解决用户态合法缺页返回
VM_FAULT_OOM后可能在同一 RIP 上反复缺页的问题; - 在 x86_64 用户态 page fault 尾部主动进入
mm::oom; - 选择一个用户地址空间对应的 victim,向相关线程组投递
SIGKILL; - 等待 victim 的地址空间释放出现可观察进度,再让触发缺页的任务重试 fault;
- 保留与 Linux OOM 语义一致的评分、不可杀条件、共享
mm处理方向,为后续接入页分配慢速路径打基础。
当前实现重点在 kernel/src/mm/oom.rs,并由 kernel/src/mm/mod.rs 导出。
2. 当前总体路径
用户态缺页 OOM 的主路径如下:
x86_64 #PF
-> arch/x86_64/mm/fault.rs
-> PageFaultHandler::handle_mm_fault()
-> 返回 VM_FAULT_OOM
-> drop(space_guard)
-> mm::oom::pagefault_out_of_memory(OomContext)
-> 串行化 OOM 选择
-> 扫描进程并选择 victim
-> 对 victim mm 的所有相关线程组发送 SIGKILL
-> 等待 mm 释放进度或当前任务被 kill
-> Retry: 重新执行 fault
-> CurrentTaskKilled: 处理 pending signal 并返回用户态退出
-> NoVictim: fatal OOM panicarch/x86_64/mm/fault.rs 在处理 VM_FAULT_OOM 前会释放 space_guard。这是重要的并发边界:OOM 路径会扫描全局进程、投递信号、等待 wait queue,不能持有地址空间写锁、VMA 锁、页表编辑锁或分配器锁进入。
3. OOM 可能从哪里来
当前 VM_FAULT_OOM 主要来自用户缺页处理中的实际内存分配失败:
PageFaultHandler::handle_normal_fault()分配中间页表失败;do_anonymous_page()为私有匿名页调用PageMapper::map()失败;- 共享匿名映射
shared.get_or_create_page()或map_phys()失败; - 文件私有映射 COW 时
copy_page_as_normal()失败; - 写保护 fault 中匿名页或私有文件页复制失败;
do_fault_around()预分配 PTE 页表失败;- 测试专用
mm::oom::should_inject_fault_oom()命中。
底层页帧分配由 LockedFrameAllocator 包装 buddy allocator。LockedFrameAllocator::allocate() 当前只尝试直接从 buddy 分配并返回 Option,还没有 Linux __alloc_pages_slowpath() 那样同步 reclaim、压缩、重试和最终 out_of_memory() 的慢路径。kernel/src/mm/page.rs 中的 page_reclaim 线程会在空闲页低于阈值时回收页缓存中的可回收文件页,但它不是当前分配失败路径上的同步 OOM 判定器。
因此,当前 DragonOS OOM killer 的触发点是“用户态合法 page fault 泄漏出的 VM_FAULT_OOM”,而不是完整的全局页分配 OOM。
4. mm::oom 的关键数据结构
OomContext 描述一次 OOM 触发现场:
trigger_pid:触发 fault 的线程 PID;trigger_tgid:触发 fault 的线程组 ID;fault_address:缺页地址;fault_ip:触发 fault 的指令地址;order:本次请求阶数,当前用户 fault 路径固定为1页。
OomOutcome 是返回给 arch fault 路径的裁决:
Retry:已经提交 victim,且观察到释放进度,触发者可以重试缺页;CurrentTaskKilled:当前任务已经收到 fatal signal 或正在退出;NoVictim:没有可杀 victim 或 OOM 核心无法推进,进入 fatal OOM。
全局状态由 OOM_STATE: SpinLock<OomState> 保护:
generation:每次开始选择递增,用来关联一次 victim 提交与等待;selecting:表示当前有 CPU 正在选择 victim;inflight:表示已有 victim 正在释放内存,其他 OOM 触发者应等待而不是继续杀更多任务。
OomVictimState 记录正在等待的 victim 地址空间:
mm_id:AddressSpace的全局唯一 ID;mm: Weak<AddressSpace>:弱引用,避免 OOM 状态持有mm导致释放不了;initial_reclaim_generation:提交 victim 时的释放进度版本;generation:对应 OOM 轮次。
等待者使用 OOM_WAITQ。测试注入使用 OOM_FAULT_INJECT,对应 /proc/sys/vm/oom_fault_inject,这是 DragonOS-only 的测试接口,不是 Linux ABI,并且受 CAP_SYS_ADMIN 限制。
5. victim 选择策略
select_victim() 扫描 ProcessManager::get_all_processes(),对每个任务:
- 找到任务的
user_vm(),没有用户地址空间的任务不参与; - 归并到线程组 leader,按 TGID 去重;
- 读取 leader 的
oom_score_adj; - 应用不可杀过滤;
- 计算 badness 分数;
- 选择分数最高者。
当前不可杀条件包括:
- PID 0;
- 全局 PID 1;
KTHREAD;- 已带
EXITING标志; - 处于 active vfork;
oom_score_adj == -1000。
当前评分公式是:
score = resident_user_pages + oom_score_adj * total_system_pages / 1000其中:
resident_user_pages来自AddressSpace::resident_pages(),表示当前mm中 present 用户 PTE 的页数;total_system_pages来自LockedFrameAllocator.usage().total();oom_score_adj范围是[-1000, 1000]。
如果分数相同,当前实现优先选择 resident_pages 更大的候选;仍相同则选择 TGID 更大的候选。这是 DragonOS 当前的确定性 tie-break,不是 Linux ABI。
与 Linux 6.6 对照:Linux oom_badness() 的基线是 get_mm_rss(mm) + swapents + page-table pages,再加上 oom_score_adj * totalpages / 1000。DragonOS 当前没有 swap,也没有把页表页计入评分,因此评分更接近“RSS + oom_score_adj”的阶段性实现。
6. oom_score_adj
DragonOS 当前实现 /proc/[pid]/oom_score_adj,位置在 kernel/src/filesystem/procfs/pid/oom_score_adj.rs。
语义要点:
- 可读写范围是
[-1000, 1000]; -1000表示 OOM 不可杀;- 非特权写入不能把分数调低到
oom_score_adj_min以下; - 拥有
CAP_SYS_RESOURCE的写入会同步更新oom_score_adj_min; - 对共享同一
mm的其他线程组,写入会传播oom_score_adj,但 active vfork 场景会跳过传播; ProcessManager::lock_oom_score_adj()用于序列化相关更新。
这与 Linux /proc/[pid]/oom_score_adj 的核心方向一致:oom_score_adj_min 用来防止非特权进程绕过管理员设置的最低保护线;共享同一 mm 的进程也应保持一致的 OOM 评分倾向。
当前状态:DragonOS 已有 oom_score_adj,但 /proc/[pid]/oom_score 只读评分展示还不是当前 OOM killer 的核心接口。
7. SIGKILL 提交与共享地址空间
选中 victim 后,send_oom_sigkill() 不只杀选中 TGID。它会调用 kill_targets_for_mm() 扫描所有进程,找出共享 candidate AddressSpace 的线程组 leader,并对每个目标以 PidType::TGID 投递 SIGKILL。
这么做是为了匹配 Linux 的重要语义方向:如果多个线程组共享同一个 mm,只杀其中一个线程组可能无法释放该地址空间,甚至会让被 OOM kill 的任务卡在退出路径,等待另一个仍在运行并持有同一 mm 的任务。Linux __oom_kill_process() 也会向共享 victim mm 的其他用户进程发送 SIGKILL,但跳过 global init 和 kthread。
DragonOS 当前同样跳过 PID 0、PID 1 和 kthread。提交成功后会打印摘要:
oom-kill: trigger_pid=... trigger_tgid=... victim_tgid=...
score=... adj=... rss=... order=... addr=... ip=...如果当前触发 OOM 的任务本身就是 victim,或者它共享 victim mm 且不是 init/kthread,pagefault_out_of_memory() 直接返回 CurrentTaskKilled,由 fault 路径处理 pending signal。
8. 等待释放进度
DragonOS 不把 resident_user_pages 下降本身视作 OOM 释放完成。真正用于唤醒 OOM 等待者的是 AddressSpace::oom_reclaim_generation()。
原因是:清 PTE 和实际释放物理页之间隔着 TLB shootdown。只要其他 CPU 还可能通过 stale TLB 访问旧物理页,内核就不能释放该页。kernel/src/mm/mmu_gather.rs 维护了顺序:
- 页表项先被清除;
- 待释放的
Arc<Page>暂存在MmuGather::pending_pages; flush_mmu_tlbonly()完成本地和远端 TLB shootdown;flush_mmu_free()清空pending_pages,触发页引用释放;- 如果确实释放了页,调用
mm.advance_oom_reclaim_generation(); - 调用
mm::oom::notify_mm_reclaim_progress(mm)唤醒 OOM 等待者。
victim_has_progress() 判断:
Weak<AddressSpace>已无法升级,认为有进展;mm_id不匹配,认为旧mm生命周期已结束;oom_reclaim_generation相比提交 victim 时发生变化,认为有进展。
如果 InnerAddressSpace::drop() 发生,会先 unmap_all(),随后调用 mm::oom::notify_mm_drop(mm_id) 清除 inflight 状态并唤醒等待者。
9. 与进程退出的关系
OOM killer 通过 SIGKILL 促使 victim 走普通退出路径,而不是直接在 OOM 上下文释放别的进程地址空间。
进程退出关键步骤在 ProcessManager::exit():
- 先
mark_exiting(); - 处理
clear_child_tid、robust list、vfork completion 等仍可能访问用户地址空间的退出工作; - 之后执行类似 Linux
exit_mm()的逻辑:- 将当前 CPU 切到 idle 地址空间;
- 从 PCB 中
replace_user_vm(None); - 从旧
mm.active_cpus中清除当前 CPU; - 更新 per-CPU TLB 状态;
- 如果没有其他用户任务仍引用旧
mm,调用old_vm.write().unmap_all(); - drop 旧
Arc<AddressSpace>,最终触发InnerAddressSpace::drop()中的notify_mm_drop()。
这里有两个不变量:
- OOM 路径不能直接释放 victim 的
mm,必须让 victim 自己经过退出路径,避免破坏 clear tid、robust futex、文件关闭等语义; user_vm=None之后,任务不应再作为 OOM victim 候选,因为它已经没有可通过 OOM kill 释放的用户地址空间。
10. 与信号系统的关系
OOM 路径使用 Signal::oom_fatal_signal_pending() 判断当前任务是否已经注定退出。该 helper 不只检查线程私有 pending SIGKILL,还检查:
- 线程组是否已有 group exit code;
sighand.shared_pending中是否有SIGKILL;- 普通
fatal_signal_pending()。
这是 DragonOS 当前信号模型下的必要补充:OOM victim 投递使用 PidType::TGID,信号会进入线程组共享 pending。如果 OOM 等待者只检查线程级 pending,就可能在已经收到进程级 SIGKILL 时继续选择新 victim 或重试 fault,造成过度 kill 或 livelock。
11. 并发与生命周期注意点
OOM killer 的关键并发规则如下:
OOM_STATE只保护 OOM 选择和 inflight 状态,不保护进程生命周期本身;- 选择 victim 时只保存
Arc<AddressSpace>到候选,提交到 inflight 后降级为Weak<AddressSpace>; - 等待 OOM slot 或等待释放进度时,不能持有
OOM_STATE、AddressSpace写锁、VMA 锁、页表编辑锁或分配器锁; selecting防止多个 CPU 同时选择并提交 victim;inflight防止在已有 victim 正在释放时过度杀进程;- 观察到 victim 释放进度后,应清除
inflight并唤醒等待者; notify_mm_reclaim_progress()和notify_mm_drop()都必须能在 victim 生命周期结束边界安全调用;resident_user_pages是评分统计,不是释放完成判据;oom_score_adj更新需要全局序列化,避免共享mm传播时出现明显不一致;- vfork 期间跳过 victim 或跳过共享
mm分数传播,是为了避免父子共享地址空间但生命周期关系特殊时误伤或破坏 Linux 兼容语义。
12. fatal OOM
当 select_victim() 找不到候选,或者 SIGKILL 提交失败且不是简单的 ESRCH 竞态,pagefault_out_of_memory() 返回 NoVictim。x86_64 fault 路径随后触发 panic:
fatal user page-fault OOM: pid=... tgid=... addr=... rip=...这是当前阶段的明确失败策略。Linux 在全局 OOM 且没有 killable process 时也会认为系统可能已经无法前进,并可能 panic。DragonOS 当前还没有 memcg OOM、sysrq OOM、panic_on_oom 等分支,因此 fatal OOM 只表示当前用户 fault OOM 闭环无法推进。
13. Linux 6.6 语义对照
Linux 6.6 的 OOM 设计有几个核心点:
out_of_memory()由全局oom_lock串行化,避免多个上下文过度 kill;- 页分配失败通常在
__alloc_pages_slowpath()中完成 reclaim、重试和__alloc_pages_may_oom(); pagefault_out_of_memory()主要处理 memcg OOM 和已有 fatal signal;全局 OOM 通常应由分配上下文负责;oom_badness()基于 RSS、swap、页表页和oom_score_adj;oom_score_adj == -1000保护任务不可被 OOM kill;- 如果任务正在退出或已有 fatal signal,Linux 倾向让它尽快释放内存,而不是再杀其他任务;
__oom_kill_process()会处理共享 victimmm的其他用户进程;- OOM victim 会被
mark_oom_victim()标记,并可能交给 OOM reaper 异步回收; panic_on_oom、oom_kill_allocating_task、memcg、cpuset、mempolicy、NUMA 都影响最终策略。
DragonOS 当前已经对齐的方向:
- OOM 选择全局串行化;
oom_score_adj范围和-1000不可杀语义;- 以 resident pages 作为主要 badness 基线;
- 处理共享 victim
mm的线程组; - 避免在已有 fatal signal 或退出中继续 kill;
- 等待真实释放进度后再重试 fault;
- 对 no victim 走明确 fatal OOM。
当前尚未完整对齐的部分:
- OOM 触发点还未前移到页分配慢速路径;
- 没有 memcg OOM 域;
- 没有 NUMA、cpuset、mempolicy 约束;
- 没有 swap,评分不包含 swapents;
- 评分尚未包含页表页;
- 没有 Linux 风格
TIF_MEMDIE、MMF_OOM_SKIP、oom_mm标记; - 没有独立 OOM reaper;
- 没有
panic_on_oom、oom_kill_allocating_task、oom_dump_tasks等 sysctl; /proc/[pid]/oom_score仍需后续补齐。
14. 当前实现边界
需要特别说明的当前状态:
- OOM killer 只覆盖 x86_64 用户态 page fault 的
VM_FAULT_OOM路径; - 内核态访问用户地址失败优先走异常表修复或 panic,不进入用户 OOM kill;
- 普通内核分配、slab 分配、DMA 分配、页缓存分配失败不统一触发
mm::oom; - page reclaim 线程是后台回收机制,不是页分配失败路径上的同步 reclaim;
oom_fault_inject仅用于测试 OOM fault 闭环,不应被用户态程序依赖;do_swap_page()、do_numa_page()当前仍未实现,对应 Linux 能力也不应在本文中假设存在;- fatal OOM 当前直接 panic,是第一阶段比静默重试更安全的失败方式。
15. 未来演进方向
后续演进建议按以下顺序推进:
将 OOM 触发点前移到页分配慢速路径
在
LockedFrameAllocator/ buddy allocator 上层建立类似 Linux__alloc_pages_slowpath()的同步 reclaim、重试和 OOM 判定流程。用户 page fault 尾部应逐步回到 Linux 风格:只处理 memcg OOM、fatal signal 和“泄漏出的VM_FAULT_OOM”告警/重试。建立更完整的 reclaim 与 OOM 闭环
将页缓存 reclaim、dirty writeback、不可回收页判断、分配请求上下文整合到统一慢路径中,而不是只依赖后台线程。
引入 OOM victim 标记
增加类似 Linux
TIF_MEMDIE、oom_mm、MMF_OOM_SKIP的状态,用于区分“已被 OOM kill、应尽快释放”的任务和普通任务,减少重复 kill,并为 OOM reaper 做准备。实现 OOM reaper 或等价机制
在确保不破坏 robust futex、clear_child_tid、vfork 和用户态退出语义的前提下,异步回收 victim 的可回收匿名页,缩短 OOM stall 时间。
完善 badness 统计
在
resident_user_pages基础上加入页表页、swap、shmem、file/anon 分类统计,并补齐/proc/[pid]/oom_score。接入 cgroup v2 memory OOM
未来容器场景需要 memory.max、memory.events、oom_kill、oom_group_kill 等语义。memcg OOM 应有自己的 OOM 域,而不是总是全局杀进程。
支持策略 sysctl
根据 Linux 语义补齐
panic_on_oom、oom_kill_allocating_task、oom_dump_tasks等策略项,并明确哪些是兼容 ABI,哪些是 DragonOS 内部调试接口。补齐 NUMA/cpuset/mempolicy 约束
当 DragonOS 支持相关资源域后,victim 选择必须限制在真正能为本次失败分配释放内存的候选集合中。
增强诊断输出
OOM 日志应能输出当前内存状态、候选任务表、victim 的 VM/RSS/页表/oom_score_adj 等信息,便于定位真实内存压力来源。