logo

Linux 内存未耗尽却无法创建进程的一次内核级排查

作者:da吃一鲸8862026.11.07 01:08浏览量:0

简介:当系统提示 fork 失败、无法创建新进程时,很多人会直接判断为内存耗尽。但真实环境中常会出现 free 显示仍有可用内存、Swap 也大量空闲,进程却依然无法启动的矛盾现象。本文通过一次生产故障复盘,分析 Linux 内存超售、进程信用额度与 fork 失败之间的深层关系,帮助读者理解 free 输出为何会“说谎”、内核如何判断内存是否足够,以及如何从 dmesg、cgroup 和系统配置中定位真正瓶颈。

Linux 内存未耗尽却无法创建进程:一次内核级“假死”排查

一、从一次诡异的故障说起

在部分 Linux 服务器上,你可能会遇到下面这类错误:

  1. -bash: fork: Cannot allocate memory

按照直觉,第一反应通常是物理内存不足。但真正进入系统执行 free -m 后,却可能看到如下结果:

  1. total used free shared buff/cache available
  2. Mem: 15867 15010 112 41 744 353
  3. Swap: 4095 0 4095

从数字上看,物理内存仍有约 353 MB 可用,Swap 还有 3.5 GB 以上空闲。表面上的“可用资源”接近 4 GB,但系统却连一个轻量级 Shell 都无法 fork 出来。

这种情况不是传统意义上的内存耗尽,也不是某个数据库或应用突然失控,而是 Linux 内核在内存管理层面做出了一种更隐蔽的限制。它并不是在说“内存已经用完”,而是在说:“当前进程已经无法获得创建所需的内存担保。”

本文将围绕一次真实故障,从内核日志、内存记账机制、进程创建流程和系统配额四个维度逐步还原问题根因。

二、为什么 free 显示的“可用内存”并不可信

很多运维人员习惯将 free -m 中的 available 列理解为“还有多少内存可以随意使用”。但这个理解并不完全准确。

available 的计算逻辑大致可以表示为:

  1. available ≈ free + 可回收的 page cache + 可回收的 slab 对象

也就是说,available 是一个估算值,表示在回收缓存之后系统可能为普通内存请求释放出的空间。但它并不等同于“当前没有任何约束地分配给任意进程”的内存量。

在 Linux 中,是否允许进程获得内存,不只看 available,还要受到以下因素影响:

  1. 内核的内存超售策略。
  2. 进程所属 cgroup 的内存限制。
  3. 内核为进程记账所预留的“承诺额度”。
  4. 当前地址空间、页表、栈、文件描述符等资源占用。
  5. 系统参数对 overcommit 行为的限制。

因此,available 较高不代表可以无限 fork 新进程。它只是反映了一段时间内的内存回收潜力,而不直接等于进程可获取的内存上限。

三、fork 失败与内存超售的关系

3.1 fork 为什么需要“内存担保”

fork() 创建子进程时,内核需要为新进程复制父进程的地址空间描述、页表、内核栈、线程结构等元数据。虽然 Linux 使用写时复制技术,不会立刻复制所有物理页,但内核仍然需要为这些新结构预留内存。

如果系统配置了严格的内存超售策略,内核会先执行 __vm_enough_memory 检查,判断当前请求是否可以被“承诺”。一旦判断未通过,就会直接返回 ENOMEM,即使用户态看到物理内存还有空闲。

3.2 Linux 的 overcommit 策略

Linux 内核通过 vm.overcommit_memory 参数控制内存承诺方式,常见取值如下:

值 含义
0 启发式超售,内核根据可用内存和请求大小估算是否允许
1 总是允许内存超售,几乎不拒绝普通分配
2 严格限制,不允许明显超过物理内存与 Swap 总和

在严格策略下,内核会维护一个“承诺内存”总量,任何大额分配都会被严格校验。如果系统长期运行大量进程,即使每个进程实际占用不高,承诺总量也可能提前达到上限。

此时,fork() 并不是因为用完了物理内存,而是因为内核认为“如果继续承诺下去,未来可能无法兑现”。

四、真实故障的排查过程

4.1 初步现象

某台用于数据库测试的 Linux 服务器出现异常:从 root 用户切换到普通数据库用户时,直接提示:

  1. -bash: fork: Cannot allocate memory

同一时间执行内存检查,却发现物理内存与 Swap 均未耗尽。

4.2 查看内核日志

对于此类矛盾现象,第一步应检查内核环形缓冲区。使用:

  1. dmesg -T

可以看到类似如下记录:

  1. [Wed Jun 12 10:24:31 2024] __vm_enough_memory: pid: 20799, comm: su, not enough memory for the allocation

该日志表明,内核在 __vm_enough_memory 阶段拒绝了进程的内存请求。这里的关键不是物理内存耗尽,而是当前请求没有通过内核的内存承诺检查。

4.3 检查进程数量与内存承诺

接下来需要查看系统整体进程状态:

  1. ps -eLf | wc -l
  2. cat /proc/meminfo | grep -E 'CommitLimit|Committed_AS|MemAvailable'

其中:

  • Committed_AS:当前系统已经承诺出去的内存总量。
  • CommitLimit:内核允许承诺的最大内存量。
  • 如果 Committed_AS 接近或超过 CommitLimit,就会导致新进程创建失败。

例如:

  1. CommitLimit: 8177668 kB
  2. Committed_AS: 8014592 kB
  3. MemAvailable: 361712 kB

此时 available 仍有约 353 MB,但承诺内存已经接近上限。此时任何新的 fork() 都可能因为无法获得额外承诺而失败。

4.4 查找内存承诺的主要消耗者

可以通过遍历 /proc 下的进程信息,观察哪些进程的地址空间较大或线程数量异常:

  1. ps -eo pid,ppid,cmd,%mem,rss,vsz --sort=-vsz | head -20

在某些场景中,一个应用可能并没有占用大量常驻内存,但其虚拟地址空间非常大,或者内部启动了过多线程。这类进程会显著抬高系统的内存承诺总额。

4.5 检查 cgroup 限制

在现代 Linux 发行版中,还需要关注 cgroup 的限制。即使宿主机内存充足,如果当前会话所在的 cgroup 设置了较低的内存上限,也会出现 fork 失败。

可以通过以下方式查看:

  1. cat /sys/fs/cgroup/memory/memory.limit_in_bytes
  2. cat /sys/fs/cgroup/memory/memory.usage_in_bytes

如果 limit 设置过小,而 usage 已接近限制,就会出现与物理内存无关的 ENOMEM。

五、为什么 Swap 空闲也不能解决 fork 失败

很多人会疑惑:Swap 还有 3.5 GB,为什么不能用来创建进程?

这是因为 fork() 在请求内存承诺时,内核并不简单地将 Swap 视为可直接分配的空间。Swap 更多承担物理页换出后的落盘能力,而不是可以在任意时刻用于满足所有分配请求的“现金”。

此外,如果系统处于内存压力状态,Swap 的写入速度、页回收速度都会受到影响。对于某些不可回收的内核结构,例如内核栈、页表和任务结构,Swap 的作用非常有限。

因此,Swap 空闲只能说明系统尚未大规模换出页面,并不代表内核会允许新的进程承诺继续增长。

六、如何避免和修复类似问题

6.1 调整内存超售策略

如果业务负载明确且物理内存规划充足,可以考虑调整内核策略:

  1. sysctl vm.overcommit_memory=1

但需要注意,该配置会让内核几乎不再限制普通内存请求,可能在极端情况下触发 OOM Killer。更稳妥的方式是保持启发式策略,同时降低承诺内存占用。

6.2 控制进程数量和线程数量

对于数据库、中间件或测试环境,应重点关注单机进程数与线程总数。大量线程会显著增加内核栈、页表和任务结构的承诺内存。

可以通过系统配置控制用户可创建的进程数:

  1. ulimit -u

也可以检查应用是否在线程使用上存在泄漏或无限扩展。

6.3 优化应用的内存申请方式

某些语言运行时或应用框架在启动时会预留大量虚拟地址空间,即使实际使用很少,也会提高承诺内存总量。对于此类应用,应结合其内存管理参数调整堆、栈和线程池大小。

6.4 结合监控与日志建立预警

建议持续监控以下指标:

  • Committed_AS 与 CommitLimit 的比值。
  • 系统总进程数与线程数。
  • cgroup 内存使用率。
  • 内核日志中 __vm_enough_memory 的出现频率。

当承诺内存接近上限时提前告警,而不是等用户报告无法登录或无法切换用户后才被动响应。

七、总结

表面上看,系统还有可用内存和大量 Swap,却无法创建进程,这并非 Linux 的行为异常,而是内核在内存管理上执行了更严格的承诺机制。

free 提供的是一个估算视角,而不是最终分配结论。进程能否成功创建,还需通过 fork() 的内存担保、overcommit 策略、内存承诺总额以及 cgroup 限制等多层校验。

对于运维和开发人员而言,理解这一层逻辑比单纯记住“内存不够就看 free”更有价值。面对 fork 失败,不应只检查剩余内存,而应顺藤摸瓜,查看内核日志、承诺内存和进程数量,才能找到真正的瓶颈所在。

发表评论

活动