Linux 内核 OOM Killer 决策机制深度剖析从 badness 函数打分逻辑到 cgroup 内存限制的连锁反应一、当__alloc_pages_slowpath走投无路OOM 触发的精确条件与嵌入式场景的特殊风险在嵌入式 Linux 系统上OOM Killer 一旦触发往往意味着系统级故障——不同于服务器的杀进程保系统嵌入式设备的内存总量固定且无交换分区被杀的进程通常是核心业务进程导致设备进入不可恢复状态。从内核视角看OOM 的触发路径如下kmalloc/vmalloc→__alloc_pages_nodemask→get_page_from_freelist→ 快速路径失败 →__alloc_pages_slowpath→ 尝试内存回收kswapd 唤醒 direct reclaim→ 回收无果 →__alloc_pages_may_oom→out_of_memory。每一步都有精确的条件判断而非简单的内存不够就杀进程。关键点在于__alloc_pages_may_oom中的 gfp_mask 检查如果分配请求标记了__GFP_NORETRY即便系统内存枯竭也不会触发 OOM而是直接返回 NULL。这意味着上层调用者必须做好分配失败的兜底处理——而大量内核驱动代码恰恰忽略了这一点在嵌入式场景中埋下隐患。二、oom_badness的量化模型一套将进程危害度映射为 0-1000 的函数设计OOM Killer 的核心判断逻辑集中在mm/oom_kill.c的oom_badness函数中。该函数输入进程的task_struct输出一个unsigned long类型的恶性分数分数最高的进程被选中牺牲。flowchart TD A[out_of_memory 回调] -- B{存在正在退出的进程?} B --|是| C[延迟等待: oom_reaper 回收中] B --|否| D[遍历所有进程br/调用 oom_badness] D -- E{oom_score_adj -1000?} E --|是| F[返回 LONG_MINbr/该进程豁免杀] E --|否| G[计算 rss swap_usagebr/ pagetable 占用] G -- H[乘以 oom_score_adjbr/调整因子得到 raw_score] H -- I{进程有 CAP_SYS_ADMIN?} I --|是| J[raw_score / 4] I --|否| K[保持 raw_score] J -- L{进程是 OOM 触发者?} K -- L L --|是| M[raw_score * 优先级权重] L --|否| N[final_score clamp raw_score 到 0..1000] M -- N N -- O[选出最高分进程br/send SIGKILL] style G fill:#ff9,stroke:#333,stroke-width:2px style H fill:#bbf,stroke:#333,stroke-width:2pxoom_badness的核心计算公式为points get_mm_rss(mm) get_mm_counter(mm, MM_SWAPENTS) mm_pgtables_bytes(mm) / PAGE_SIZE; points points * 1000 / total_ram_pages; adj_points points * oom_score_adj / 1000;该公式的设计有几个意图。用 RSS常驻内存集而非 VSS虚拟内存尺寸作为基准因为虚拟内存中大量未实际分配的映射区域并不消耗物理页不应计入恶性分数。将页表自身的内存消耗也计入在 ARM32 平台上每个 PTE 占用 4 字节一个进程若映射了 1GB 地址空间借由 2 级页表仅页表开销就高达 2MB——这在 256MB 小内存设备上不可忽视。oom_score_adj是用户态控制权重。取值范围 [-1000, 1000]默认 0。/proc/PID/oom_score_adj可写。设为 -1000 表示永不杀设为 1000 表示优先杀。该值在 cgroup 内的计算逻辑有所不同——当 memcg 触发 OOM 时oom_badness会额外乘以oom_group因子。三、从/proc/PID/oom_score到自定义守护进程监控 OOM 风险的生产级实现以下代码实现了一个 OOM 风险监控守护进程周期性扫描各进程的oom_score当检测到风险进程时提前预警。/* * oom_monitor.c — 嵌入式 OOM 风险监控守护进程 * 编译: arm-linux-gnueabihf-gcc -o oom_monitor oom_monitor.c -Wall -Wextra * 设计意图在 OOM Killer 下手前通过监控 oom_score 变化趋势提前告警 * 给上层业务进程一个优雅降级的机会 */ #include stdio.h #include stdlib.h #include string.h #include dirent.h #include unistd.h #include fcntl.h #include sys/stat.h #include sys/resource.h #include signal.h #include time.h #include errno.h #define OOM_SCORE_THRESHOLD 800 /* 超过此阈值触发告警 */ #define SCAN_INTERVAL_SEC 5 /* 扫描间隔 */ #define MAX_PROCS 256 #define PATH_BUF 256 typedef struct { int pid; char comm[64]; unsigned long oom_score; unsigned long oom_score_adj; unsigned long vm_rss_kb; } ProcOomInfo; /* * 读取 /proc/PID/oom_score 的值 * 该文件内容为进程当前的 OOM 得分由内核在线计算 */ static int read_oom_score(int pid, unsigned long* score) { char path[PATH_BUF]; char buf[32]; int fd, n; snprintf(path, sizeof(path), /proc/%d/oom_score, pid); fd open(path, O_RDONLY); if (fd 0) return -1; n read(fd, buf, sizeof(buf) - 1); close(fd); if (n 0) return -1; buf[n] \0; *score strtoul(buf, NULL, 10); return 0; } /* * 扫描 /proc 目录收集所有进程的 OOM 风险信息 * 跳过内核线程通过检查 /proc/PID/cmdline 是否为空判断 */ static int scan_processes(ProcOomInfo* procs, int max_procs) { DIR* dir; struct dirent* entry; int count 0; dir opendir(/proc); if (!dir) { fprintf(stderr, [OOM_MON] opendir /proc 失败: %s\n, strerror(errno)); return -1; } while ((entry readdir(dir)) ! NULL count max_procs) { /* 只处理数字目录名PID */ if (entry-d_type ! DT_DIR) continue; char* endptr; long pid strtol(entry-d_name, endptr, 10); if (*endptr ! \0 || pid 0) continue; ProcOomInfo* info procs[count]; info-pid (int)pid; /* 读取进程名: 跳过内核线程cmdline 为空或不可读 */ char cmdfile[PATH_BUF]; snprintf(cmdfile, sizeof(cmdfile), /proc/%d/stat, (int)pid); FILE* fp fopen(cmdfile, r); if (!fp) continue; if (fscanf(fp, %d %s, (int){0}, info-comm) 2) { fclose(fp); continue; } fclose(fp); /* 读取 OOM 相关指标 */ if (read_oom_score((int)pid, info-oom_score) ! 0) continue; count; } closedir(dir); return count; } /* * 主循环周期性扫描并上报 OOM 风险项 * 当检测到高风险进程时调用注册的回调进行业务层告警 */ int main(int argc, char* argv[]) { ProcOomInfo procs[MAX_PROCS]; fprintf(stderr, [OOM_MON] OOM 监控守护进程启动 阈值%d, 间隔%ds\n, OOM_SCORE_THRESHOLD, SCAN_INTERVAL_SEC); while (1) { int n scan_processes(procs, MAX_PROCS); if (n 0) { sleep(SCAN_INTERVAL_SEC); continue; } int warnings 0; for (int i 0; i n; i) { if (procs[i].oom_score OOM_SCORE_THRESHOLD) { /* * 告警格式PID:进程名 OOM分数 * 上层可通过 syslog 或 socket 收集此信息 * 在生产环境中应改为 syslog(LOG_WARNING, ...) */ fprintf(stderr, [OOM_MON] WARN: PID%d name%s oom_score%lu\n, procs[i].pid, procs[i].comm, procs[i].oom_score); warnings; } } if (warnings 0) { fprintf(stderr, [OOM_MON] 扫描完成进程数%d无风险项\n, n); } sleep(SCAN_INTERVAL_SEC); } return 0; }四、OOM 机制的工程陷阱cgroup 嵌套时的行为分裂与内核版本差异cgroup v1 和 v2 的 OOM 控制行为存在根本性差异。在 cgroup v1 中内存限制绑定在memory.oom_control中默认行为是当 cgroup 内内存超限时杀死 cgroup 内oom_score最高的进程。但若父 cgroup 也设了限制且先于子 cgroup 触发 OOM被杀死的可能是跨 cgroup 的进程——这导致我的 cgroup 设了 128MB 上限但杀死的是另一个 cgroup 的进程的诡异现象。cgroup v2 引入了memory.oom.group控制项。设为 1 时整个 cgroup 作为一个原子被杀死——内核发送 SIGKILL 给组内所有进程。这解决了 v1 中杀了一个进程其余进程继续分配导致连环 OOM的问题但粒度变得更粗——一个内存泄漏的子进程会拖垮整个 cgroup。在系统 OOM 和 cgroup OOM 同时发生时的优先级问题内核版本 4.19 之后采用了不同的处理逻辑先判断是否属于某个 memcg 超限若是则仅在该 memcg 范围内选牺牲者否则回退全系统扫描。在嵌入式 Linux常使用 LTS 4.19/5.4/5.10 内核中oom_kill_allocating_task默认值为 0关闭允许选择非触发者这在多进程守护场景下更安全。另外需要注意 Android 系统的 Low Memory KillerLMK和标准 OOM Killer 的关系。LMK 在__alloc_pages_slowpath更早阶段拦截使用独立的打分体系基于oom_score_adj预设的六个等级与内核 OOM Killer 并行存在。如果嵌入式系统使用了 AOSP 构建需确认 LMK 驱动是否被加载它可能在你不知情的情况下杀掉了后台服务。五、总结Linux OOM Killer 的决策链路是内存分配慢路径 → 回收失败 → oom_badness 打分 → 最高分进程被 SIGKILL。打分体系以 RSS 为核心基准受oom_score_adj调节权重。cgroup 的引入使 OOM 行为变得与层级结构耦合在 v1 和 v2 之间存在显著差异。对于嵌入式系统的启示关闭 swap无磁盘交换介质、合理设置oom_score_adj保护核心进程、监控/proc/PID/oom_score趋势作为早期预警、在 cgroup v2 环境中使用memory.oom.group控制原子性。如果系统内存在多个内存敏感进程推荐为每个敏感进程单独创建 memcg设定渐进式限制并注册memory.pressure通知在真正触发 OOM 之前给业务层创造自我降级的时间窗口。
Linux 内核 OOM Killer 决策机制深度剖析:从 badness 函数打分逻辑到 cgroup 内存限制的连锁反应
发布时间:2026/7/14 14:06:52
Linux 内核 OOM Killer 决策机制深度剖析从 badness 函数打分逻辑到 cgroup 内存限制的连锁反应一、当__alloc_pages_slowpath走投无路OOM 触发的精确条件与嵌入式场景的特殊风险在嵌入式 Linux 系统上OOM Killer 一旦触发往往意味着系统级故障——不同于服务器的杀进程保系统嵌入式设备的内存总量固定且无交换分区被杀的进程通常是核心业务进程导致设备进入不可恢复状态。从内核视角看OOM 的触发路径如下kmalloc/vmalloc→__alloc_pages_nodemask→get_page_from_freelist→ 快速路径失败 →__alloc_pages_slowpath→ 尝试内存回收kswapd 唤醒 direct reclaim→ 回收无果 →__alloc_pages_may_oom→out_of_memory。每一步都有精确的条件判断而非简单的内存不够就杀进程。关键点在于__alloc_pages_may_oom中的 gfp_mask 检查如果分配请求标记了__GFP_NORETRY即便系统内存枯竭也不会触发 OOM而是直接返回 NULL。这意味着上层调用者必须做好分配失败的兜底处理——而大量内核驱动代码恰恰忽略了这一点在嵌入式场景中埋下隐患。二、oom_badness的量化模型一套将进程危害度映射为 0-1000 的函数设计OOM Killer 的核心判断逻辑集中在mm/oom_kill.c的oom_badness函数中。该函数输入进程的task_struct输出一个unsigned long类型的恶性分数分数最高的进程被选中牺牲。flowchart TD A[out_of_memory 回调] -- B{存在正在退出的进程?} B --|是| C[延迟等待: oom_reaper 回收中] B --|否| D[遍历所有进程br/调用 oom_badness] D -- E{oom_score_adj -1000?} E --|是| F[返回 LONG_MINbr/该进程豁免杀] E --|否| G[计算 rss swap_usagebr/ pagetable 占用] G -- H[乘以 oom_score_adjbr/调整因子得到 raw_score] H -- I{进程有 CAP_SYS_ADMIN?} I --|是| J[raw_score / 4] I --|否| K[保持 raw_score] J -- L{进程是 OOM 触发者?} K -- L L --|是| M[raw_score * 优先级权重] L --|否| N[final_score clamp raw_score 到 0..1000] M -- N N -- O[选出最高分进程br/send SIGKILL] style G fill:#ff9,stroke:#333,stroke-width:2px style H fill:#bbf,stroke:#333,stroke-width:2pxoom_badness的核心计算公式为points get_mm_rss(mm) get_mm_counter(mm, MM_SWAPENTS) mm_pgtables_bytes(mm) / PAGE_SIZE; points points * 1000 / total_ram_pages; adj_points points * oom_score_adj / 1000;该公式的设计有几个意图。用 RSS常驻内存集而非 VSS虚拟内存尺寸作为基准因为虚拟内存中大量未实际分配的映射区域并不消耗物理页不应计入恶性分数。将页表自身的内存消耗也计入在 ARM32 平台上每个 PTE 占用 4 字节一个进程若映射了 1GB 地址空间借由 2 级页表仅页表开销就高达 2MB——这在 256MB 小内存设备上不可忽视。oom_score_adj是用户态控制权重。取值范围 [-1000, 1000]默认 0。/proc/PID/oom_score_adj可写。设为 -1000 表示永不杀设为 1000 表示优先杀。该值在 cgroup 内的计算逻辑有所不同——当 memcg 触发 OOM 时oom_badness会额外乘以oom_group因子。三、从/proc/PID/oom_score到自定义守护进程监控 OOM 风险的生产级实现以下代码实现了一个 OOM 风险监控守护进程周期性扫描各进程的oom_score当检测到风险进程时提前预警。/* * oom_monitor.c — 嵌入式 OOM 风险监控守护进程 * 编译: arm-linux-gnueabihf-gcc -o oom_monitor oom_monitor.c -Wall -Wextra * 设计意图在 OOM Killer 下手前通过监控 oom_score 变化趋势提前告警 * 给上层业务进程一个优雅降级的机会 */ #include stdio.h #include stdlib.h #include string.h #include dirent.h #include unistd.h #include fcntl.h #include sys/stat.h #include sys/resource.h #include signal.h #include time.h #include errno.h #define OOM_SCORE_THRESHOLD 800 /* 超过此阈值触发告警 */ #define SCAN_INTERVAL_SEC 5 /* 扫描间隔 */ #define MAX_PROCS 256 #define PATH_BUF 256 typedef struct { int pid; char comm[64]; unsigned long oom_score; unsigned long oom_score_adj; unsigned long vm_rss_kb; } ProcOomInfo; /* * 读取 /proc/PID/oom_score 的值 * 该文件内容为进程当前的 OOM 得分由内核在线计算 */ static int read_oom_score(int pid, unsigned long* score) { char path[PATH_BUF]; char buf[32]; int fd, n; snprintf(path, sizeof(path), /proc/%d/oom_score, pid); fd open(path, O_RDONLY); if (fd 0) return -1; n read(fd, buf, sizeof(buf) - 1); close(fd); if (n 0) return -1; buf[n] \0; *score strtoul(buf, NULL, 10); return 0; } /* * 扫描 /proc 目录收集所有进程的 OOM 风险信息 * 跳过内核线程通过检查 /proc/PID/cmdline 是否为空判断 */ static int scan_processes(ProcOomInfo* procs, int max_procs) { DIR* dir; struct dirent* entry; int count 0; dir opendir(/proc); if (!dir) { fprintf(stderr, [OOM_MON] opendir /proc 失败: %s\n, strerror(errno)); return -1; } while ((entry readdir(dir)) ! NULL count max_procs) { /* 只处理数字目录名PID */ if (entry-d_type ! DT_DIR) continue; char* endptr; long pid strtol(entry-d_name, endptr, 10); if (*endptr ! \0 || pid 0) continue; ProcOomInfo* info procs[count]; info-pid (int)pid; /* 读取进程名: 跳过内核线程cmdline 为空或不可读 */ char cmdfile[PATH_BUF]; snprintf(cmdfile, sizeof(cmdfile), /proc/%d/stat, (int)pid); FILE* fp fopen(cmdfile, r); if (!fp) continue; if (fscanf(fp, %d %s, (int){0}, info-comm) 2) { fclose(fp); continue; } fclose(fp); /* 读取 OOM 相关指标 */ if (read_oom_score((int)pid, info-oom_score) ! 0) continue; count; } closedir(dir); return count; } /* * 主循环周期性扫描并上报 OOM 风险项 * 当检测到高风险进程时调用注册的回调进行业务层告警 */ int main(int argc, char* argv[]) { ProcOomInfo procs[MAX_PROCS]; fprintf(stderr, [OOM_MON] OOM 监控守护进程启动 阈值%d, 间隔%ds\n, OOM_SCORE_THRESHOLD, SCAN_INTERVAL_SEC); while (1) { int n scan_processes(procs, MAX_PROCS); if (n 0) { sleep(SCAN_INTERVAL_SEC); continue; } int warnings 0; for (int i 0; i n; i) { if (procs[i].oom_score OOM_SCORE_THRESHOLD) { /* * 告警格式PID:进程名 OOM分数 * 上层可通过 syslog 或 socket 收集此信息 * 在生产环境中应改为 syslog(LOG_WARNING, ...) */ fprintf(stderr, [OOM_MON] WARN: PID%d name%s oom_score%lu\n, procs[i].pid, procs[i].comm, procs[i].oom_score); warnings; } } if (warnings 0) { fprintf(stderr, [OOM_MON] 扫描完成进程数%d无风险项\n, n); } sleep(SCAN_INTERVAL_SEC); } return 0; }四、OOM 机制的工程陷阱cgroup 嵌套时的行为分裂与内核版本差异cgroup v1 和 v2 的 OOM 控制行为存在根本性差异。在 cgroup v1 中内存限制绑定在memory.oom_control中默认行为是当 cgroup 内内存超限时杀死 cgroup 内oom_score最高的进程。但若父 cgroup 也设了限制且先于子 cgroup 触发 OOM被杀死的可能是跨 cgroup 的进程——这导致我的 cgroup 设了 128MB 上限但杀死的是另一个 cgroup 的进程的诡异现象。cgroup v2 引入了memory.oom.group控制项。设为 1 时整个 cgroup 作为一个原子被杀死——内核发送 SIGKILL 给组内所有进程。这解决了 v1 中杀了一个进程其余进程继续分配导致连环 OOM的问题但粒度变得更粗——一个内存泄漏的子进程会拖垮整个 cgroup。在系统 OOM 和 cgroup OOM 同时发生时的优先级问题内核版本 4.19 之后采用了不同的处理逻辑先判断是否属于某个 memcg 超限若是则仅在该 memcg 范围内选牺牲者否则回退全系统扫描。在嵌入式 Linux常使用 LTS 4.19/5.4/5.10 内核中oom_kill_allocating_task默认值为 0关闭允许选择非触发者这在多进程守护场景下更安全。另外需要注意 Android 系统的 Low Memory KillerLMK和标准 OOM Killer 的关系。LMK 在__alloc_pages_slowpath更早阶段拦截使用独立的打分体系基于oom_score_adj预设的六个等级与内核 OOM Killer 并行存在。如果嵌入式系统使用了 AOSP 构建需确认 LMK 驱动是否被加载它可能在你不知情的情况下杀掉了后台服务。五、总结Linux OOM Killer 的决策链路是内存分配慢路径 → 回收失败 → oom_badness 打分 → 最高分进程被 SIGKILL。打分体系以 RSS 为核心基准受oom_score_adj调节权重。cgroup 的引入使 OOM 行为变得与层级结构耦合在 v1 和 v2 之间存在显著差异。对于嵌入式系统的启示关闭 swap无磁盘交换介质、合理设置oom_score_adj保护核心进程、监控/proc/PID/oom_score趋势作为早期预警、在 cgroup v2 环境中使用memory.oom.group控制原子性。如果系统内存在多个内存敏感进程推荐为每个敏感进程单独创建 memcg设定渐进式限制并注册memory.pressure通知在真正触发 OOM 之前给业务层创造自我降级的时间窗口。