最近我发现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.
问题在于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
函数,则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?
编辑:我添加了一个示例,展示了glibc的free
函数在内部的munmap
调用失败时如何泄漏内存并返回错误码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]);
}
}