带有私有匿名映射的ENOMEM的munmap()失败

Sta*_*irl 20 linux posix mmap memory-mapping enomem

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

 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.
Run Code Online (Sandbox Code Playgroud)

问题是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]);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

我通常认为这是内核中的一个错误,但知道Linux如何处理内存过量使用和OOM我几乎可以肯定这是一个"功能",可以提高性能并减少内存消耗.

我能找到的其他信息:

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

其他操作系统真的能保证内存映射的重新分配吗?我知道Windows会这样做,但是其他类Unix操作系统呢?FreeBSD的?QNX?


编辑:我正在添加一个示例,显示free内部munmap调用失败时glibc如何泄漏内存ENOMEM.使用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]);
    }
}
Run Code Online (Sandbox Code Playgroud)

jxh*_*jxh 5

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

至于你的问题:

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

  2. 只要传入的地址位于页面边界上,并且长度参数向上舍入到页面大小的下一个倍数,就允许您访问d 区域munmap的一部分。mmap