Ошибка munmap() в ENOMEM с приватным анонимным отображением
Я недавно обнаружил, что 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
s каждую вторую страницу, чтобы создать отдельную структуру VMA для каждой оставшейся страницы. На моей машине последний munmap
звонок всегда терпит неудачу с ENOMEM
,
Сначала я думал, что munmap
никогда не завершается ошибкой, если используется с теми же значениями для адреса и размера, которые использовались для создания сопоставления. Очевидно, что это не так в Linux, и я не смог найти информацию о похожем поведении в других системах.
В то же время, по моему мнению, частичное отключение, применяемое к середине отображаемой области, как ожидается, приведет к сбою в любой ОС для каждой разумной реализации, но я не нашел никакой документации, в которой говорилось бы, что такой сбой возможен.
Обычно я считаю, что это ошибка в ядре, но, зная, как Linux справляется с перегрузкой памяти и 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?
РЕДАКТИРОВАТЬ: я добавляю пример, который показывает, как 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]);
}
}
1 ответ
Один из способов обойти эту проблему в Linux - это mmap
более 1 страницы за раз (например, 1 МБ за раз), а также отобразить разделительную страницу после нее. Итак, вы на самом деле звоните mmap
на 257 страницах памяти, затем переназначить последнюю страницу с PROT_NONE
, так что к нему нельзя получить доступ. Это должно победить оптимизацию слияния VMA в ядре. Поскольку вы выделяете много страниц одновременно, вы не должны сталкиваться с максимальным пределом отображения. Недостатком является то, что вы должны вручную управлять тем, как вы хотите нарезать большие mmap
,
Что касается ваших вопросов:
Системные вызовы могут произойти сбой в любой системе по разным причинам. Документация не всегда полная.
Вам разрешено
munmap
частьmmap
d область до тех пор, пока переданный адрес лежит на границе страницы, а аргумент длины округляется до следующего кратного размера страницы.