[英]munmap() failure with ENOMEM with private anonymous mapping
我最近發現,如果這導致VMA(虛擬內存區域)結構數量超過vm.max_map_count
情況,Linux不能保證使用munmap
釋放分配有mmap
內存。 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
永遠不會失敗。 顯然在Linux上並非如此,我無法在其他系統上找到有關類似行為的信息。
同時在我看來,應用於映射區域中間的部分取消映射預計會在任何操作系統上針對每個合理的實現失敗,但我沒有找到任何文檔說這種失敗是可能的。
我通常認為這是內核中的一個錯誤,但知道Linux如何處理內存過量使用和OOM我幾乎可以肯定這是一個“功能”,可以提高性能並減少內存消耗。
我能找到的其他信息:
MapViewOfFile
, UnmapViewOfFile
, VirtualAlloc
, VirtualFree
) - 它們根本不支持部分取消UnmapViewOfFile
。 malloc
實現不會創建超過65535
映射,在達到此限制時sbrk
到sbrk
: https : //code.woboq.org/userspace/glibc/malloc/malloc.c.html 。 這看起來像這個問題的解決方法,但它仍然可以free
默默泄漏內存。 mmap
/ munmap
因為這個問題(我不知道它是如何結束的)。 其他操作系統真的能保證內存映射的重新分配嗎? 我知道Windows會這樣做,但是其他類Unix操作系統呢? FreeBSD的? QNX?
編輯:我正在添加一個示例,顯示當內部munmap
調用因ENOMEM
失敗時,glibc的free
是如何泄漏內存的。 使用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]);
}
}
在Linux上解決這個問題的一種方法是一次mmap
多1頁(例如一次1 MB),並在其后映射分隔頁。 因此,您實際上在257頁內存上調用mmap
,然后使用PROT_NONE
重新映射最后一頁,以便無法訪問它。 這應該會破壞內核中的VMA合並優化。 由於您一次分配多個頁面,因此不應該遇到最大映射限制。 缺點是你必須手動管理你想要切割大型mmap
。
至於你的問題:
由於各種原因,系統調用可能在任何系統上失敗。 文檔並不總是完整的。
只要傳入的地址位於頁面邊界上,並且長度參數向上舍入到頁面大小的下一個倍數,就可以對mmap
d區域的一部分進行munmap
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.