Ошибка 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, затем munmaps каждую вторую страницу, чтобы создать отдельную структуру 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,

Что касается ваших вопросов:

  1. Системные вызовы могут произойти сбой в любой системе по разным причинам. Документация не всегда полная.

  2. Вам разрешено munmap часть mmapd область до тех пор, пока переданный адрес лежит на границе страницы, а аргумент длины округляется до следующего кратного размера страницы.

Другие вопросы по тегам