我最近发现,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
每隔一页 s 为每个剩余页面创建单独的 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 操作系统呢?自由BSD?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]);
}
}