Linux 带有私有匿名映射的ENOMEM的munmap()失败
我最近发现,如果导致VMA(虚拟内存区域)结构的数量超过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
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]);
}
}