当前位置:首页>Linux>Linux内存分配善后一条龙过程

Linux内存分配善后一条龙过程

  • 2026-09-15 09:54:39
Linux内存分配善后一条龙过程

在 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. 1. OOM Killer 不会杀死内核线程、init 进程(PID=1),仅杀死用户进程;
  2. 2. 默认情况下,OOM Killer 会遍历所有用户进程,通过 oom_badness 计算进程得分(内存占用越多、优先级越低,得分越高),杀死得分最高的进程;
  3. 3. 若开启内核参数 oom_kill_allocating_task=1(默认 0),则强制杀死触发 OOM 的内核态分配对应的进程(若分配进程是用户进程),否则继续按得分杀进程;

OOM 杀死进程后,内核会释放该进程的所有内存,然后回到内存分配逻辑,重试分配(goto retry)。

补充:OOM kill 后仍内存不足的处理流程

Linux 6.18.6 中,OOM Killer 杀死一个进程后,并非一定能释放足够内存满足当前分配需求,若释放内存后仍不足(如被杀进程内存占比低、系统内存碎片化严重,或存在其他进程持续占用内存),内核会进入“重复回收+重复OOM kill”的循环,直到内存充足或无进程可杀,具体流程如下:

  1. 1. OOM kill 进程后,内核通过 oom_reap_task 快速回收该进程的内存(包括匿名页、page cache、页表等),并更新系统空闲内存统计;
  2. 2. 回到内存分配逻辑(__alloc_pages_slowpath 的 retry 分支),重新尝试分配内存;
  3. 3. 若重试分配失败(空闲内存仍低于 low 水位),内核会再次触发同步内存回收(try_to_free_pages),扫描 LRU 链表回收可回收页(优先 page cache),尝试释放更多内存;
  4. 4. 回收后再次尝试分配,若仍失败,再次调用 out_of_memory,启动 OOM Killer 重新遍历所有存活用户进程,计算 oom_score,杀死当前得分最高的进程;
  5. 5. 重复步骤 1-4,直到内存分配成功,或系统中无可用杀进程(除内核线程、init 进程外无其他用户进程);
  6. 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. 1. 回收范围:仅回收应用可回收内存(page cache、应用匿名页),不触碰内核核心内存,同步回收时会阻塞当前应用进程(非内核进程);
  2. 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. 1. 当前进程 oom_score 最高,即自身是内存占用最大的进程;
  2. 2. 开启内核参数 oom_kill_allocating_task=1,强制杀死触发 OOM 的进程,跳过全进程扫描,降低延迟但增加应用崩溃风险。

规则3:OOM kill 后仍内存不足的处理

若被杀进程释放的内存仍无法满足分配需求,内核会进入“回收-重试-OOM kill”循环:

  1. 1. OOM 杀进程后,内核通过 oom_reap_task 回收其内存,重试分配;
  2. 2. 分配失败则再次触发同步回收(优先 page cache),kswapd 后台异步回收并行执行;
  3. 3. 重复杀得分最高的进程,直到内存充足或无可用杀进程;
  4. 4. 无进程可杀时:仅用户态分配则当前进程收 SIGKILL 退出;伴随内核态分配则系统 panic。

规则4:进程被杀死后的行为

若触发 OOM 的进程被杀死,内核发送 SIGKILL 信号(不可捕获),标记 TIF_MEMDIE 加速退出,回收其所有内存,Page Fault 处理终止,进程直接退出,不返回用户态。

3.4 Page Fault 异常中的延迟问题(用户态特有)

用户态 Page Fault 触发内存不足时,延迟集中在异常上下文,核心来源的(与内核态重复部分简化):

  1. 1. 同步回收+Swap 换出:主要延迟,受磁盘 I/O 影响(机械硬盘 10~100ms,SSD 1~10ms);
  2. 2. OOM Killer 扫描:微秒到毫秒级,进程越多延迟略高,非主要来源;
  3. 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 和匿名页。
OOM 进程存活
触发 OOM 的内核线程不被杀,仅杀用户进程
默认不杀触发进程,仅杀得分最高的进程;特殊配置下被杀
延迟特点
原子上下文分配失败无延迟(直接返回失败),同步回收延迟与用户态类似
延迟集中在 Page Fault 上下文,受回收、Swap 影响显著
失败后果
可能导致内核报错、功能降级,无进程可杀时内核 panic
进程被杀或分配失败(返回 ENOMEM),不影响内核运行

五、关键内核参数与调优建议(Linux 6.18.6)

结合本文分析,针对内存不足场景,可通过以下参数调整内核行为,优化延迟和进程存活策略:

  1. 1. /proc/sys/vm/oom_kill_allocating_task:默认 0,建议根据场景调整。对稳定性要求高的应用,保持 0;对延迟敏感、可接受应用崩溃的场景,可设为 1。
  2. 2. /proc/sys/vm/swappiness:默认 60,控制匿名页 Swap 倾向。内存密集型应用可降低(如 10),减少 Swap 延迟;内存紧张场景可提高(如 80),避免过早触发 OOM。
  3. 3. /proc/[pid]/oom_score_adj:进程级 OOM 权重,范围 -1000~1000。核心应用可设为 -1000(不可杀),避免被 OOM Killer 误杀。
  4. 4. /proc/sys/vm/panic_on_oom:默认 0,OOM 时不 panic;设为 1 时,全局 OOM 触发 panic,适用于对稳定性要求极高的场景(如服务器)。

六、总结

Linux 内存不足时的处理机制,本质是“分级救场、保核优先”:内核态和用户态均遵循“内存回收 → Swap 换出 → OOM Killer”的流程,但因分配上下文不同,行为存在差异。

核心要点回顾:

  1. 1. 内核态分配不足:优先异步回收,原子上下文不阻塞,OOM 不杀内核线程,无进程可杀时 panic;
  2. 2. 用户态分配不足:主要通过 Page Fault 触发救场,OOM 时默认不杀触发进程,仅杀内存大户,延迟主要来自回收和 Swap;

OOM 进程存活的关键:取决于 oom_score 得分和 oom_kill_allocating_task 参数,触发进程绝非必死无疑;

  1. 3. OOM kill 后仍内存不足:内核会进入“回收-重试-OOM kill”循环,重复杀死得分最高的进程,直到内存充足或无进程可杀,无进程可杀时内核 panic;
  2. 4. 延迟优化:调整 swappiness、oom_score_adj 等参数,减少 Swap 依赖和不必要的回收,可降低 Page Fault 延迟。

理解这套机制,不仅能帮助开发者排查内存溢出、应用崩溃问题,还能根据实际场景调优内核参数,提升系统稳定性和性能。

最新文章

随机文章