login
Python mmap内存分配异常解析
2025/07/25·StepFun can make mistakes, check important info.
在机器物理内存还够的情况下,python收到了这个异常,可能的原因有哪些? unable to mmap 125840 bytes from file <filename not specified>: Cannot allocate memory (12)
Reason
思考过程

我们正在处理一个Python异常:"unable to mmap 125840 bytes from file : Cannot allocate memory (12)"。尽管机器物理内存足够,但还是出现了这个问题。我们需要分析可能的原因。

首先,理解错误信息:

  • mmap 是Unix/Linux系统调用,用于将文件或设备映射到内存。在Python中,它通常由mmap模块使用,或者由某些库(如NumPy)在内部使用。
  • 错误信息表明,尝试映射125840字节(约123KB)的内存失败,原因是“Cannot allocate memory”(无法分配内存)。
  • 注意:<filename not specified> 意味着这次映射可能不是直接映射一个文件,可能是匿名映射(anonymous mapping),即分配内存而不与文件关联。

虽然物理内存足够,但内存分配失败可能有多种原因:

  1. 虚拟内存限制:每个进程都有虚拟地址空间限制。在64位系统上,理论上很大,但实际中可能受到系统配置(如vm.max_map_count)或进程本身限制(如RLIMIT_DATA)的影响。

    • 检查是否有太多的内存映射(例如,打开了大量文件并映射到内存)导致进程达到虚拟地址空间上限。
    • 使用ulimit -a查看当前shell的资源限制,特别是max memory sizemax locked memory。但注意,这个错误是分配内存,而不是锁定内存。
  2. 内存碎片:尽管总内存足够,但可能由于内存碎片,系统无法找到连续的物理内存块来满足映射请求。尤其是当请求较大的连续内存块时(但这里只有123KB,通常不会出现连续性问题,除非系统内存极度碎片化)。

  3. 内存映射数量限制:Linux系统有一个参数vm.max_map_count,它控制一个进程可以拥有的内存映射区域的最大数量。如果进程创建了大量的映射(比如通过频繁的mmap调用),可能会达到这个限制。

    • 可以通过cat /proc/sys/vm/max_map_count查看当前值。
    • 默认值通常是65530,但如果进程创建了过多的映射,就会达到上限。
  4. 内存不足(OOM):尽管物理内存足够,但可能因为系统或用户的内存使用限制(cgroup、ulimit)导致进程无法分配更多内存。

    • 检查进程的cgroup内存限制(如果运行在容器中,如Docker,可能设置了内存限制)。
    • 检查是否有其他进程消耗了大量内存,导致系统无法分配更多内存(但错误是mmap失败,而mmap通常使用虚拟内存,所以可能不是物理内存不足,而是虚拟内存地址空间不足或者映射数量限制)。
  5. 32位进程限制:如果Python进程是32位的,那么每个进程的虚拟地址空间限制为4GB(实际用户空间通常为3GB左右)。如果进程已经使用了大部分地址空间,再分配123KB可能会失败。但123KB很小,所以除非进程已经使用了接近3GB的内存,否则不太可能。但如果是32位系统且进程已经很大,则可能。

  6. 交换空间(Swap)不足:当物理内存不足时,系统会使用交换空间。如果交换空间也用尽,则分配内存会失败。但是,错误信息是“Cannot allocate memory”,而不是“Out of memory”,所以可能不是交换空间的问题,但也不能完全排除。

  7. 内存分配策略:某些系统配置(如overcommit_memory)可能会影响内存分配。Linux有三种overcommit模式:

    • 0(默认):启发式overcommit,允许适度的overcommit。
    • 1:总是overcommit,不检查。
    • 2:不允许overcommit,分配内存时检查是否有足够的物理内存和交换空间。
    • 如果设置为2,并且系统当前可用的物理内存加上交换空间不足以满足分配请求,就会失败。注意,这里请求的是123KB,但可能进程已经申请了很大的内存,导致剩余的可分配内存(物理+交换)不足。
  8. 特定于mmap的限制:mmap分配内存时,除了物理内存和交换空间,还可能受到其他限制,比如映射区域的对齐要求等。

  9. 文件描述符限制:虽然错误信息没有指定文件名(可能是匿名映射),但如果映射的是文件,可能会受到文件描述符数量的限制。但这里没有文件名,所以可能是匿名映射,因此不太可能是文件描述符问题。

  10. 内存泄漏或资源耗尽:程序可能已经映射了大量内存区域但没有释放,导致进程的虚拟地址空间耗尽(例如,在64位系统上,虽然地址空间很大,但进程的虚拟地址空间被分割成很多小块,导致无法找到连续地址空间?但123KB很小,所以不太可能,除非进程已经映射了非常多的区域,导致地址空间碎片化)。

  11. NUMA架构问题:在NUMA系统中,如果内存分配请求必须来自特定的NUMA节点,而该节点内存不足,也可能失败。但通常mmap不会指定节点,所以可能性较小。

  12. 内核问题:可能是内核bug或者临时状态异常。

由于错误信息中提到了“file ”,这通常意味着这是一个匿名映射(anonymous mmap),即用于分配内存而不是映射文件。

诊断步骤建议:

  1. 检查系统内存状态:

    • 使用free -h查看物理内存和交换空间的使用情况。
    • 使用cat /proc/meminfo获取更详细的内存信息。
  2. 检查进程的内存映射和限制:

    • 使用ps aux找到进程的PID。
    • 查看进程的内存映射:cat /proc/<pid>/mapspmap -x <pid>,观察映射数量以及地址空间使用情况。
    • 检查进程的限制:cat /proc/<pid>/limits,特别是Max address space(虚拟内存大小)和Max locked memory(虽然mmap不一定锁定)等。
  3. 检查系统级别的内存映射限制:

    • cat /proc/sys/vm/max_map_count
  4. 检查系统overcommit设置:

    • cat /proc/sys/vm/overcommit_memory
    • 以及/proc/sys/vm/overcommit_ratio/proc/sys/vm/overcommit_kbytes
  5. 如果运行在容器中(如Docker),检查容器的内存限制。

  6. 检查系统日志(如/var/log/syslogdmesg)是否有相关错误信息,比如OOM killer的活动。

可能的原因总结:

  • 进程达到了内存映射数量上限(vm.max_map_count限制)。
  • 进程的虚拟地址空间耗尽(在32位进程中更常见,或者64位进程映射了巨大的地址空间)。
  • 系统配置了严格的overcommit(模式2)且可分配内存(物理+交换)不足。
  • 系统内存碎片导致无法分配连续内存(尽管123KB很小,但在极端情况下可能)。
  • 资源限制(如ulimit或cgroup)导致进程无法分配更多内存。

由于问题发生在物理内存充足的情况下,重点应放在进程的虚拟地址空间限制、内存映射数量限制以及overcommit设置上。

当 Python 抛出 unable to mmap 125840 bytes from file <filename not specified>: Cannot allocate memory (12) 异常时,即使物理内存充足,也可能由以下原因导致。核心问题通常与虚拟内存管理、系统限制或资源碎片有关,而非物理内存不足。以下是可能的原因及详细分析:


1. 进程虚拟地址空间耗尽(最常见原因)

  • 原因mmap 需要连续的虚拟地址空间,但进程可能因长期运行或内存泄漏导致地址空间碎片化,无法找到连续的 125840 字节(约 123 KB)区域。
  • 触发场景
    • 32 位 Python 进程:虚拟地址空间上限仅 2-3 GB。
    • 64 位进程:理论上空间巨大(TB 级),但频繁的 mmap/munmap 操作会导致碎片化,尤其当程序反复映射/解除映射不同大小的内存块时。
  • 检查方法
    • 使用 pmap -x <PID> 查看进程的内存映射,观察地址空间碎片情况。
    • 通过 cat /proc/<PID>/maps 分析是否有大量不连续的小内存段。

2. 内存映射数量上限(vm.max_map_count 限制)

  • 原因:Linux 系统限制单个进程的 mmap 映射数量(默认通常为 65530)。如果进程创建了大量映射(如打开许多文件、使用 numpy 等库),可能触达上限。
  • 诊断
    • 检查当前值:sysctl vm.max_map_countcat /proc/sys/vm/max_map_count
    • 查看进程映射数:wc -l /proc/<PID>/maps
  • 解决
    • 临时提高限制:sudo sysctl -w vm.max_map_count=262144
    • 永久生效:在 /etc/sysctl.conf 添加 vm.max_map_count=262144

3. Overcommit 内存策略限制

  • 原因:Linux 的 vm.overcommit_memory 策略影响内存分配:
    • 0(默认):启发式 Overcommit,可能因保守拒绝分配。
    • 2(严格模式):禁止 Overcommit,要求 物理内存 + Swap ≥ 申请值。即使物理内存足够,若 Swap 不足或进程已占用过多虚拟内存,也会失败。
  • 诊断
    • 检查设置:cat /proc/sys/vm/overcommit_memory
    • 检查可用内存:free -h 关注 Swap 使用量。
  • 解决
    • 临时设置:sudo sysctl -w vm.overcommit_memory=1(允许 Overcommit,但有 OOM 风险)。
    • 增加 Swap 空间:sudo fallocate -l 2G /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile

4. 资源限制(ulimit/cgroups)

  • 原因
    • 用户级限制ulimit -v(虚拟内存)或 ulimit -l(锁定内存)可能过低。
    • 容器/Docker 限制:cgroups 可能限制了进程的 memory.memsw.limit_in_bytes(内存 + Swap)。
  • 诊断
    • 检查 ulimit:ulimit -a
    • 容器中检查:cat /sys/fs/cgroup/memory/memory.limit_in_bytes
  • 解决
    • 调整 ulimit:ulimit -v unlimited(临时生效)。
    • Docker 示例:运行容器时添加 --memory-swap=-1 取消 Swap 限制。

5. 内存碎片化

  • 原因:物理内存或虚拟地址空间碎片导致无法分配连续块。尽管 123 KB 较小,但在以下场景可能发生:
    • 系统长期运行,物理内存被分割成小块。
    • 进程频繁分配/释放不同大小的内存,导致虚拟地址空间“空洞”。
  • 缓解
    • 优化代码:减少 mmap 调用频率,复用内存区域。
    • 使用 madvise(MADV_DONTNEED) 释放不再使用的映射。

6. 特定库或代码行为

  • 常见触发源
    • NumPy/PyTorch:使用 mmap 处理大数组,可能因未释放映射导致累积。
    • 多进程编程fork() 后子进程继承父进程的映射,可能快速耗尽资源。
    • 匿名映射:如 mmap.mmap(-1, size) 申请内存,错误中 <filename not specified> 表明此类操作。
  • 诊断
    • 检查代码中是否显式调用 mmap 模块。
    • 使用 tracemalloc 跟踪内存分配来源。

诊断步骤总结

  1. 定位进程ps aux | grep python 获取 PID。
  2. 检查映射cat /proc/<PID>/maps | wc -l 查看映射数量。
  3. 系统配置
    • sysctl vm.max_map_count
    • sysctl vm.overcommit_memory
    • free -h(关注 Swap)
  4. 资源限制
    • ulimit -a
    • 容器中检查 cgroups 配置。
  5. 日志分析dmesg -T | grep -i "out of memory"/var/log/syslog 查找内核级错误。

解决方案建议

  • 短期缓解
    bash
  • 长期优化
    • 升级到 64 位 Python(如果是 32 位环境)。
    • 优化代码:减少 mmap 调用,确保及时 munmap() 释放。
    • 增加 Swap 空间。
    • 在容器中运行时,明确配置更高的内存/Swap 限制。

注意:若问题发生在容器(如 Docker)中,需在宿主机和容器内同时检查限制。例如 Docker 默认禁用 Swap,需显式启用 --memory-swap 标志。