linux - 具有私有(private)匿名映射的 ENOMEM 的 munmap() 失败

标签 linux posix mmap memory-mapping enomem

我最近发现 Linux 不保证用 mmap 分配的内存可以用 munmap 释放,如果这导致 VMA(虚拟内存区域)数量结构超过 vm.max_map_count。联机帮助页(几乎)清楚地说明了这一点:

 ENOMEM The process's maximum number of mappings would have been exceeded.
 This error can also occur for munmap(), when unmapping a region
 in the middle of an existing mapping, since this results in two
 smaller mappings on either side of the region being unmapped.

问题是 Linux 内核总是尽可能尝试合并 VMA 结构,这使得 munmap 即使对于单独创建的映射也会失败。我能够编写一个小程序来确认此行为:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#include <sys/mman.h>

// value of vm.max_map_count
#define VM_MAX_MAP_COUNT        (65530)

// number of vma for the empty process linked against libc - /proc/<id>/maps
#define VMA_PREMAPPED           (15)

#define VMA_SIZE                (4096)
#define VMA_COUNT               ((VM_MAX_MAP_COUNT - VMA_PREMAPPED) * 2)

int main(void)
{
    static void *vma[VMA_COUNT];

    for (int i = 0; i < VMA_COUNT; i++) {
        vma[i] = mmap(0, VMA_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

        if (vma[i] == MAP_FAILED) {
            printf("mmap() failed at %d\n", i);
            return 1;
        }
    }

    for (int i = 0; i < VMA_COUNT; i += 2) {
        if (munmap(vma[i], VMA_SIZE) != 0) {
            printf("munmap() failed at %d (%p): %m\n", i, vma[i]);
        }
    }
}

它使用 mmap 分配大量页面(默认允许的最大值的两倍),然后每第二页 munmap 为每个剩余页面创建单独的 VMA 结构。在我的机器上,最后一次 munmap 调用总是失败并返回 ENOMEM

最初我认为 munmap 如果与用于创建映射的地址和大小的值相同,则永远不会失败。显然,在 Linux 上情况并非如此,我无法找到有关其他系统上类似行为的信息。

与此同时,在我看来,在任何操作系统上,对于每个合理的实现,应用于映射区域中间的部分取消映射预计会失败,但我还没有找到任何文档表明这种失败是可能的。

我通常认为这是内核中的错误,但了解 Linux 如何处理内存过度使用和 OOM 我几乎可以肯定这是一个旨在提高性能和减少内存消耗的“功能”。

我能找到的其他信息:

  • 由于其设计原因,Windows 上的类似 API 没有此“功能”(参见 MapViewOfFileUnmapViewOfFileVirtualAlloc VirtualFree) - 它们根本不支持部分取消映射。
  • glibc malloc 实现不会创建超过 65535 的映射,当达到此限制时退回到 sbrk:https://code.woboq.org/userspace/glibc/malloc/malloc.c.html .这看起来像是解决此问题的方法,但仍然有可能使 free 悄悄泄漏内存。
  • jemalloc 遇到了这个问题,并试图避免使用 mmap/munmap 因为这个问题(我不知道他们是如何结束的)。

其他操作系统真的保证内存映射的释放吗?我知道 Windows 会这样做,但是其他类 Unix 操作系统呢?自由系统? QNX?


编辑:我正在添加示例,说明当内部 munmap 调用因 ENOMEM 而失败时,glibc 的 free 会如何泄漏内存。使用 strace 查看 munmap 失败:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#include <sys/mman.h>

// value of vm.max_map_count
#define VM_MAX_MAP_COUNT        (65530)

#define VMA_MMAP_SIZE           (4096)
#define VMA_MMAP_COUNT          (VM_MAX_MAP_COUNT)

// glibc's malloc default mmap_threshold is 128 KiB
#define VMA_MALLOC_SIZE         (128 * 1024)
#define VMA_MALLOC_COUNT        (VM_MAX_MAP_COUNT)

int main(void)
{
    static void *mmap_vma[VMA_MMAP_COUNT];

    for (int i = 0; i < VMA_MMAP_COUNT; i++) {
        mmap_vma[i] = mmap(0, VMA_MMAP_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

        if (mmap_vma[i] == MAP_FAILED) {
            printf("mmap() failed at %d\n", i);
            return 1;
        }
    }

    for (int i = 0; i < VMA_MMAP_COUNT; i += 2) {
        if (munmap(mmap_vma[i], VMA_MMAP_SIZE) != 0) {
            printf("munmap() failed at %d (%p): %m\n", i, mmap_vma[i]);
            return 1;
        }
    }

    static void *malloc_vma[VMA_MALLOC_COUNT];

    for (int i = 0; i < VMA_MALLOC_COUNT; i++) {
        malloc_vma[i] = malloc(VMA_MALLOC_SIZE);

        if (malloc_vma[i] == NULL) {
            printf("malloc() failed at %d\n", i);
            return 1;
        }
    }

    for (int i = 0; i < VMA_MALLOC_COUNT; i += 2) {
        free(malloc_vma[i]);
    }
}

最佳答案

在 Linux 上解决此问题的一种方法是一次 mmap 多个页面(例如,一次 1 MB),并在其后映射一个分隔页。所以,你实际上是在257页内存上调用了mmap,然后用PROT_NONE重新映射了最后一页,这样就无法访问了。这应该会破坏内核中的 VMA 合并优化。由于您一次分配多个页面,因此不应遇到最大映射限制。缺点是您必须手动管理如何对大型 mmap 进行切片。

关于您的问题:

  1. 由于各种原因,系统调用在任何系统上都可能失败。文档并不总是完整的。

  2. 您可以munmap mmapd 区域的一部分,只要传入的地址位于页面边界上,并且长度参数是向上舍入到页面大小的下一个倍数。

关于linux - 具有私有(private)匿名映射的 ENOMEM 的 munmap() 失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43743555/

相关文章:

linux - mmap 是原子的吗?

c++ - mmap如何分配超过20Gb?

linux - 如何获取当前时间的 docker 统计信息?

c - "fork()"后printf异常

c - 在单独的线程上打印奇数和偶数的程序

c - 只读来自 FILE 对象的缓冲日期

c++ - 如何找出子进程是否仍在运行?

c - 如何在 Linux 中正确安装 gsl 库?

java - 是否可以在没有连接设备的情况下写入串行端口?

c - 使用 mmap 在 C 中逐行读取文件的最佳方法?