Недавно я обнаружил, что Linux не гарантирует, что память, выделенная с помощью mmap
, может быть освобождена с помощью munmap
, если это приведет к ситуации, когда количество областей VMA (область виртуальной памяти) превышает vm.max_map_count
. Manpage ясно указывает на это (почти):
ENOMEM The process 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 справляется с overcommit памяти и OOM, я почти уверен, что это "функция", которая существует для повышения производительности и снижения потребления памяти.
Другая информация, которую я смог найти:
- Подобные API-интерфейсы в Windows не имеют этой "функции" из-за их дизайна (см.
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?
EDIT: Я добавляю пример, который показывает, как 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 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]);
}
}