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

Linux 带有私有匿名映射的ENOMEM的munmap()失败,linux,posix,mmap,memory-mapping,enomem,Linux,Posix,Mmap,Memory Mapping,Enomem,我最近发现,如果导致VMA(虚拟内存区域)结构的数量超过vm.max\map\count时,Linux不能保证使用mmap分配的内存可以通过munmap释放。手册页(几乎)清楚地说明了这一点: 问题是Linux内核总是尽可能地尝试合并VMA结构,使得munmap即使对于单独创建的映射也会失败。我能够编写一个小程序来确认这种行为: #include <stdio.h> #include <stdlib.h> #include <errno.h> #includ

我最近发现,如果导致VMA(虚拟内存区域)结构的数量超过
vm.max\map\count
时,Linux不能保证使用
mmap
分配的内存可以通过
munmap
释放。手册页(几乎)清楚地说明了这一点:

问题是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]);
        }
    }
}

在Linux上解决此问题的一种方法是
mmap
一次映射多个页面(例如,一次1 MB),并在其后面映射分隔符页面。因此,实际上在257页内存中调用
mmap
,然后用
PROT\u NONE
重新映射最后一页,这样就无法访问它。这将击败内核中的VMA合并优化。由于一次分配多个页面,因此不应达到最大映射限制。缺点是您必须手动管理如何分割大型
mmap

关于你的问题:

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

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


  • 首先观察(您认为)您映射的vm是允许的两倍。但这可能被优化为一个单一的VMA。因此,当您开始在其中打孔时,您开始增加VMA的数量,直到超过限制。换句话说,由于优化,您认为的“非部分取消映射”很可能是由于优化而导致的部分取消映射。是的,这正是正在发生的事情。我在第二段中提到了这种优化。问题是代码不知道合并了哪些VMA。想象两个库在同一个过程中工作。每个内存块分配一个内存块,两个内存块合并到一个VMA中。可靠地释放此内存的唯一方法是通过单个munmap()调用删除整个VMA。显然,这对于两个独立的库是不可能的,因为它们彼此都不知道。我希望内核能够保证为每个mmap()调用分配单独的VMA,以避免此类失败。好的,那么我不太知道您打算怎么做。我不认为这是内核中的错误,但我同意应用程序很难理解有多少VMA在使用。但是碎片是任何内存分配方案的问题。为了避免失败,您可以计算正在使用的MMAMED段的数量。只要你不超过VMA限制就可以了。它可以优化为使用更少的内存。您的示例失败,因为您分配了两倍的限制,现在无法保证您不会遇到麻烦。您无法计算映射段的数量。为此,每个进程需要一个计数器。Glibc的malloc()已经尝试这样做了,但是如果您的程序使用mmap()分配内存,这个计数器将无法保证任何事情。稍后我将发布一个示例,演示glibc的free()在这种情况下如何无法释放内存,而是以静默方式泄漏内存。这里的问题是,这只是内核进行的乐观优化,在某些情况下会导致用户代码泄漏内存。我几乎可以肯定的是,假设99.99%的程序永远不会达到这个极限,即使有些程序达到了,用户也可以手动增加vm.max\u map\u计数,以防止将来出现这种情况。这就是为什么我还称之为“功能”。这可能没那么糟糕,但从计算机科学的角度来看,这是一个bug。这里的问题是,至少Glibc和jemalloc尝试解决这个问题,所以发生这种情况的概率并不是很低。仍然存在一个变化,即两个线程可以mmap两个内存区域,这两个区域将是同一VMA结构的一部分,在这种情况下,后续的mprotect和munmap调用都将失败。至于系统调用失败:对于正常工作的程序来说,有些事情永远不应该被允许失败,因为处理这种失败可能是不可能的。@Ivan:您的程序可能会因为意外的原因而失败。最明显的是通过外部信号,如
    kill-9
    。不太明显的原因可能是磁盘已满,或者是Linux的过度分配内存策略导致的故障。没有人真的会说这些bug,这只是系统的局限性。我不明白你的线程问题。我的解决方案是通过让软件(而不是操作系统)管理单个页面来减少映射页面的数量,并要求
    mmap
    一次返回多个页面。显式终止的过程是一件独立的事情-这些不是不可预测的。内存过度使用和VMA合并在某些情况下都可以被定义为bug,而在其他情况下则可以被称为“特性”。这就是为什么并非所有内核都进行这样的优化:WindowsNT从不合并VMA。至于分割VMA-在多线程环境中,仍然存在一种竞争,这种竞争可能会触发mprotect失败,然后是munmap失败,并且进程仍然必须处理VMA计数限制。我能理解他们为什么这样做,我也知道这很少是个问题,特别是如果它还能提高内核性能的话。@Ivan:答案的重点是建议对单个页面进行进程管理,并对页面集群进行少量的
    mmap
    调用。因此,操作系统认为该进程只有与进程
    mmap
    调用相匹配的较低VMA计数。Linux允许您使用
    sysctl
    将映射计数限制提高到2^31,即8TiB或4KB页面的RAM,在munmap中不存在ENOMEM失败的可能性。这似乎是一个更简单的解决方案。
    #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]);
            }
        }
    }
    
    #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]);
        }
    }