在 Linux 系统中,内存分配失败是内核和应用开发中绕不开的场景。无论是内核态驱动、内核线程申请内存,还是用户态应用通过 malloc、mmap 分配内存,当系统内存耗尽时,内核会启动一套分级救场机制——从内存回收、Swap 换出到最终的 OOM Killer,每一步都有明确的源码逻辑和行为规则。
一、前置基础:内存分配核心逻辑
Linux 内存分配的核心是“按需分配+分级救场”,无论内核态还是用户态,分配内存时都会先尝试从空闲页中分配,当空闲页低于阈值(low watermark)时,依次触发 内存回收 → Swap 换出 → OOM Killer,且整个流程均由内核统一调度,核心依赖 mm/page_alloc.c、mm/vmscan.c、mm/oom_kill.c 等源码文件。
关键前提:
- • 内存回收分为 异步回收(kswapd 内核线程后台执行)和 同步回收(分配进程阻塞,主动参与回收);
- • kswapd仅针对匿名页(进程堆、栈、数据段)Swap到磁盘空间,文件页(page cache)可直接回收或写回磁盘;
- • OOM Killer 是最后手段,核心逻辑是“杀进程、释内存、保系统”,但并非固定杀死触发 OOM 的进程。
二、内核态内存分配不足:处理流程与细节
内核态内存分配(如驱动、内核线程通过 alloc_pages、__get_free_pages、kmalloc 等 API 申请内存)的特点是:分配上下文复杂(可能在原子上下文)、对延迟敏感,且分配失败可能直接导致内核异常,因此其处理流程更严谨,优先级更高。
2.1 内核态内存分配入口与触发条件
内核态内存分配的核心入口是 __alloc_pages_nodemask,该函数会先尝试从伙伴系统中分配空闲页,当空闲页数量低于 zone 的 low 水位时,进入慢速路径(__alloc_pages_slowpath),启动救场机制。
关键源码片段(mm/page_alloc.c):
struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist, enum zone_type high_zoneidx, const nodemask_t *nodemask){ // 快速路径:尝试直接从空闲页分配struct page *page = fastpath_alloc_pages(gfp_mask, order, zonelist, high_zoneidx, nodemask); if (page) return page; // 慢速路径:内存不足,启动救场机制 return __alloc_pages_slowpath(gfp_mask, order, zonelist, high_zoneidx, nodemask);}
触发内存不足的核心条件:
空闲页数量 < zone->watermark[WM_LOW]
且无法通过快速路径分配到连续页(order 表示连续页的阶数)。
2.2 内核态内存不足的三级救场流程
第一步:唤醒 kswapd 异步回收(非阻塞)
进入慢速路径后,内核首先唤醒 kswapd 内核线程(每个节点/Node一个),后台异步回收内存,此时当前分配进程不会阻塞,继续尝试分配。kswapd 作为内核专门负责内存异步回收的守护进程,核心工作就是扫描系统 LRU 链表,处理包括 page cache(文件页)在内的可回收页,其具体回收逻辑由 shrink_zones、shrink_page_list 函数实现,回收目标是将系统空闲内存恢复到 high 水位线,避免后续分配触发同步阻塞回收。
关键逻辑:kswapd 会优先处理并回收 page cache(文件页)——对于干净的 page cache(已与磁盘文件内容同步,无脏数据),会直接释放其物理内存,归还到伙伴系统;对于脏的 page cache(未同步到磁盘的修改数据),会触发页面回写(writeback)操作,将脏数据写入磁盘后再释放内存。只有当 page cache 回收至阈值后仍无法满足内存需求时,kswapd 才会处理匿名页,在开启 Swap 的前提下,将不常用的匿名页换出到交换分区,释放物理内存。此外,kswapd 在处理回写中的 page cache 时,若页面设置了 PG_PageReclaim 标志位,会直接跳过该页面、继续扫描下一个,无需等待回写完成,以此提升回收效率。
第二步:同步直接回收(阻塞当前进程)
若 kswapd 异步回收后仍无法分配到足够内存,内核会触发同步直接回收(__alloc_pages_direct_reclaim),此时当前分配进程会被阻塞,主动参与内存回收,直到回收足够内存或回收失败。
核心源码关联(mm/vmscan.c):
- • try_to_free_pages:初始化扫描控制参数(回收页数、优先级等),启动回收;
- • shrink_page_list:遍历 LRU 页,区分文件页和匿名页处理(文件页回收/写回,匿名页 Swap 换出);
- • 回收优先级:从 DEF_PRIORITY(12)逐步降低到 0,优先级越低,扫描越彻底,回收力度越大。
注意:内核态分配若指定了 GFP_ATOMIC(原子上下文,如中断处理),则不会触发同步回收,直接返回分配失败(避免阻塞中断),此时可能导致内核报错或功能降级。
第三步:OOM Killer 启动(最后手段)
若同步回收后仍无法分配内存,内核会调用 out_of_memory 函数,启动 OOM Killer,通过杀死进程释放内存,保住内核不崩溃。
内核态 OOM 的特殊点:
- 1. OOM Killer 不会杀死内核线程、init 进程(PID=1),仅杀死用户进程;
- 2. 默认情况下,OOM Killer 会遍历所有用户进程,通过 oom_badness 计算进程得分(内存占用越多、优先级越低,得分越高),杀死得分最高的进程;
- 3. 若开启内核参数 oom_kill_allocating_task=1(默认 0),则强制杀死触发 OOM 的内核态分配对应的进程(若分配进程是用户进程),否则继续按得分杀进程;
OOM 杀死进程后,内核会释放该进程的所有内存,然后回到内存分配逻辑,重试分配(goto retry)。
补充:OOM kill 后仍内存不足的处理流程
Linux 6.18.6 中,OOM Killer 杀死一个进程后,并非一定能释放足够内存满足当前分配需求,若释放内存后仍不足(如被杀进程内存占比低、系统内存碎片化严重,或存在其他进程持续占用内存),内核会进入“重复回收+重复OOM kill”的循环,直到内存充足或无进程可杀,具体流程如下:
- 1. OOM kill 进程后,内核通过 oom_reap_task 快速回收该进程的内存(包括匿名页、page cache、页表等),并更新系统空闲内存统计;
- 2. 回到内存分配逻辑(__alloc_pages_slowpath 的 retry 分支),重新尝试分配内存;
- 3. 若重试分配失败(空闲内存仍低于 low 水位),内核会再次触发同步内存回收(try_to_free_pages),扫描 LRU 链表回收可回收页(优先 page cache),尝试释放更多内存;
- 4. 回收后再次尝试分配,若仍失败,再次调用 out_of_memory,启动 OOM Killer 重新遍历所有存活用户进程,计算 oom_score,杀死当前得分最高的进程;
- 5. 重复步骤 1-4,直到内存分配成功,或系统中无可用杀进程(除内核线程、init 进程外无其他用户进程);
- 6. 若无进程可杀,内核触发 panic,系统崩溃(对应源码中 panic 分支)。
关键说明:该循环过程中,kswapd 会持续在后台异步回收内存(优先处理 page cache),与 OOM kill 循环并行执行,尽可能减少循环次数;若系统内存碎片化严重,即使杀死多个进程,也可能因无法分配连续阶数(order)的内存而持续触发 OOM kill,最终导致系统 panic。
2.3 内核态分配失败的特殊场景
若 OOM Killer 无法找到可杀进程(如所有用户进程均为不可杀进程),内核会触发 panic(系统崩溃),对应源码中 out_of_memory 函数的 panic 分支:
if (select_bad_process(&oc)) return true;// 无进程可杀,内核崩溃panic("Out of memory and no killable processes\n");
三、用户态内存分配不足:核心流程与特有细节
用户态内存分配与内核态核心救场逻辑(内存回收、Swap 换出、OOM Killer)一致,但存在自身特有流程(glibc 内存池、Page Fault 触发)和进程存活规则,以下重点梳理用户态特有细节。
3.1 用户态内存分配的特有路径
用户态内存分配并非直接向内核申请,而是先经过用户态缓存层,完整路径为:
应用调用 malloc/calloc/mmap → glibc 内存池(如 ptmalloc)分配 → 池内有空闲内存则直接返回;池不足则发起系统调用(brk 扩展堆、mmap 分配匿名页) → 内核 sys_brk/sys_mmap 处理 → 内核调用 __alloc_pages_nodemask 分配物理内存 → 内存不足进入慢速路径(救场机制)。
其中,Page Fault 异常是用户态触发内存不足和 OOM 最常见的场景:用户态应用访问未映射的虚拟地址(如 malloc 后首次访问、栈扩展),会触发缺页中断,内核在异常处理中分配物理页,若此时内存不足,直接进入救场流程。
关键源码片段(arch/x86/mm/fault.c,Page Fault 入口):
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code){ // 省略地址合法性检查... fault = handle_mm_fault(vma, address, flags); if (unlikely(fault & VM_FAULT_ERROR)) { if (fault & VM_FAULT_OOM) goto out_of_memory; // 内存不足,触发 OOM // 其他错误处理... } // 缺页处理成功,返回用户态}
3.2 用户态内存不足的救场流程(聚焦特有)
用户态救场流程仍遵循“内存回收 → Swap 换出 → OOM Killer”,但与内核态相比,有两个核心差异,其余重复流程不再赘述:
- 1. 回收范围:仅回收应用可回收内存(page cache、应用匿名页),不触碰内核核心内存,同步回收时会阻塞当前应用进程(非内核进程);
- 2. Swap 协同:OOM 触发前后,内核会结合 swappiness 参数(默认 60),加大匿名页 Swap 换出力度,辅助释放物理内存,减少 OOM 次数。
3.3 OOM 发生时的进程存活规则(核心重点)
这是用户态与内核态 OOM 最核心的差异,触发 OOM 的用户进程绝非必死无疑,具体规则如下:
规则1:默认配置(oom_kill_allocating_task=0):优先杀“内存大户”
OOM Killer 会遍历所有用户进程,通过 oom_badness 计算得分(内存占用越多、优先级越低,得分越高),杀死得分最高的进程(通常是内存大户),而非当前触发 OOM 的进程。
oom_badness 核心逻辑:得分 = 进程 RSS + 页表用量 + Swap 用量 + oom_score_adj 调整,不可杀进程(init、oom_score_adj=-1000)直接得 0 分。
此时,触发 OOM 的进程存活,OOM 杀进程释放内存后,内核回到 Page Fault 逻辑重试分配,成功后进程回到用户态继续执行。
规则2:仅两种情况,触发 OOM 的用户进程会被杀死
- 1. 当前进程 oom_score 最高,即自身是内存占用最大的进程;
- 2. 开启内核参数 oom_kill_allocating_task=1,强制杀死触发 OOM 的进程,跳过全进程扫描,降低延迟但增加应用崩溃风险。
规则3:OOM kill 后仍内存不足的处理
若被杀进程释放的内存仍无法满足分配需求,内核会进入“回收-重试-OOM kill”循环:
- 1. OOM 杀进程后,内核通过 oom_reap_task 回收其内存,重试分配;
- 2. 分配失败则再次触发同步回收(优先 page cache),kswapd 后台异步回收并行执行;
- 3. 重复杀得分最高的进程,直到内存充足或无可用杀进程;
- 4. 无进程可杀时:仅用户态分配则当前进程收 SIGKILL 退出;伴随内核态分配则系统 panic。
规则4:进程被杀死后的行为
若触发 OOM 的进程被杀死,内核发送 SIGKILL 信号(不可捕获),标记 TIF_MEMDIE 加速退出,回收其所有内存,Page Fault 处理终止,进程直接退出,不返回用户态。
3.4 Page Fault 异常中的延迟问题(用户态特有)
用户态 Page Fault 触发内存不足时,延迟集中在异常上下文,核心来源的(与内核态重复部分简化):
- 1. 同步回收+Swap 换出:主要延迟,受磁盘 I/O 影响(机械硬盘 10~100ms,SSD 1~10ms);
- 2. OOM Killer 扫描:微秒到毫秒级,进程越多延迟略高,非主要来源;
- 3. 正常 Page Fault 仅微秒级,内存不足时延迟叠加,通常不超过 200ms(磁盘 I/O 正常情况下)。
优化建议:降低 swappiness 减少 Swap 依赖,给核心进程设置 oom_score_adj=-1000,减少 OOM 误杀和延迟。
四、内核态 vs 用户态内存不足处理对比
| | |
|---|
| alloc_pages、kmalloc 等内核 API | malloc → brk/mmap 系统调用 → Page Fault |
| 支持异步(kswapd)和同步回收,原子上下文不触发同步回收;其中 kswapd 会优先处理 page cache(文件页),再处理匿名页,回收至系统内存达到 high 水位线后停止工作。 | 主要通过 Page Fault 触发同步回收,均为非原子上下文;同步回收前会先唤醒 kswapd 异步回收,kswapd 优先处理 page cache(文件页),同步回收则会同时处理 page cache 和匿名页。 |
| | 默认不杀触发进程,仅杀得分最高的进程;特殊配置下被杀 |
| 原子上下文分配失败无延迟(直接返回失败),同步回收延迟与用户态类似 | 延迟集中在 Page Fault 上下文,受回收、Swap 影响显著 |
| 可能导致内核报错、功能降级,无进程可杀时内核 panic | 进程被杀或分配失败(返回 ENOMEM),不影响内核运行 |
五、关键内核参数与调优建议(Linux 6.18.6)
结合本文分析,针对内存不足场景,可通过以下参数调整内核行为,优化延迟和进程存活策略:
- 1. /proc/sys/vm/oom_kill_allocating_task:默认 0,建议根据场景调整。对稳定性要求高的应用,保持 0;对延迟敏感、可接受应用崩溃的场景,可设为 1。
- 2. /proc/sys/vm/swappiness:默认 60,控制匿名页 Swap 倾向。内存密集型应用可降低(如 10),减少 Swap 延迟;内存紧张场景可提高(如 80),避免过早触发 OOM。
- 3. /proc/[pid]/oom_score_adj:进程级 OOM 权重,范围 -1000~1000。核心应用可设为 -1000(不可杀),避免被 OOM Killer 误杀。
- 4. /proc/sys/vm/panic_on_oom:默认 0,OOM 时不 panic;设为 1 时,全局 OOM 触发 panic,适用于对稳定性要求极高的场景(如服务器)。
六、总结
Linux 内存不足时的处理机制,本质是“分级救场、保核优先”:内核态和用户态均遵循“内存回收 → Swap 换出 → OOM Killer”的流程,但因分配上下文不同,行为存在差异。
核心要点回顾:
- 1. 内核态分配不足:优先异步回收,原子上下文不阻塞,OOM 不杀内核线程,无进程可杀时 panic;
- 2. 用户态分配不足:主要通过 Page Fault 触发救场,OOM 时默认不杀触发进程,仅杀内存大户,延迟主要来自回收和 Swap;
OOM 进程存活的关键:取决于 oom_score 得分和 oom_kill_allocating_task 参数,触发进程绝非必死无疑;
- 3. OOM kill 后仍内存不足:内核会进入“回收-重试-OOM kill”循环,重复杀死得分最高的进程,直到内存充足或无进程可杀,无进程可杀时内核 panic;
- 4. 延迟优化:调整 swappiness、oom_score_adj 等参数,减少 Swap 依赖和不必要的回收,可降低 Page Fault 延迟。
理解这套机制,不仅能帮助开发者排查内存溢出、应用崩溃问题,还能根据实际场景调优内核参数,提升系统稳定性和性能。