[Перевод] Заметки о Unix: исследование munmap() на нулевой странице и на свободном адресном пространстве

Однажды на Fediverse мне попался интересный вопрос о munmap():

Чем именно занимается munmap () в Linux если адрес установлен в 0? В Linux подобный вызов каким-то образом срабатывает, а вот во FreeBSD — нет. Полагаю, что всё дело — в различной семантике команд, но не могу найти никаких пояснений по поводу такого поведения munmap ().

(Там было ещё это дополнение, а тут находится краткая версия ответа)

ivovtbwunynoq1egmm_txaxbc80.jpeg

Когда я увидел этот вопрос, то я реально удивился тому, что подобный вызов munmap() в Linux работоспособен, так как я ожидал, что он даст сбой на любом адресном пространстве, которое не входит в состав памяти, выделенной некоему процессу. Нулевая страница в Linux (да и в любой вменяемой системе), определённо, не входит в состав распределённой памяти. Поэтому предлагаю взглянуть на описание munmap() из документации SUS, выдержку из которого я привожу ниже.
Функция munmap () должна завершиться с ошибкой в следующих случаях:

[EINVAL]

Адреса в диапазоне [addr, addr+len) находятся за пределами допустимого диапазона адресов адресного пространства процесса.

(Похожую формулировку можно найти в справке по FreeBSD).

Когда я впервые это прочитал, я решил, что речь идёт о текущем адресном диапазоне процесса. Но, на практике, в Linux и во FreeBSD это не так, и я полагаю, что так же дело обстоит и в теории (в документации POSIX/SUS речь идёт о процессе вообще (of a process), а не о конкретном процессе (of this process)). В обеих этих Unix-системах можно применить munmap(), по меньшей мере, к некоему объёму неиспользуемого адресного пространства. Мы продемонстрируем это с помощью небольшой тестовой программы, которая отображает что-то в память с помощью mmap(), а потом дважды применяет к этому munmap().

Разница между Linux и FreeBSD заключается в том, что в этих системах понимается под нахождением памяти «за пределами допустимого диапазона адресов адресного пространства процесса». Во FreeBSD, очевидно, «за пределами» находятся нулевая страница (и, возможно, в целом, нижняя область памяти). Обращение к такой памяти и вызывает сбой munmap(). А в Linux это не так. И хотя эта ОС обычно не разрешает применять mmap() к памяти в этой области, что делается не без причины, эта память, в силу своей природы, не находится за пределами адресного пространства. Если я правильно понял прочитанный мной код Linux, то нижние области памяти вообще никогда не считаются недопустимыми. Такими считаются лишь диапазоны адресов, выходящие за пределы верхней области памяти пользовательского пространства.

(Я мельком глянул на соответствующий FreeBSD-код из файла vm_mmap.c, и я думаю, что он отказывается выполнять команды munmap(), которые выходят за нижние или верхние пределы адресного пространства, отображение которого выполнил процесс. Это означает более серьёзные ограничения, чем я ожидал.)

В конечном итоге неудивительно то, что в OpenBSD принята несколько иная интерпретация munmap(), такая, которая ближе к тому, чего я ожидал от этой команды. Вот цитата из справки по munmap () из OpenBSD, в которой идёт речь о причинах возникновения ошибки:

[EINVAL]

Параметры addr и len указывают на область, которая простирается за пределы конца адресного пространства, или некоторая часть области, над которой пытаются выполнить операцию munmap (), не является частью текущего допустимого адресного пространства.

OpenBSD требует, чтобы команду munmap() применяли только к тому, память под что уже отображена, и не позволяет применять эту команду к произвольным областям потенциального адресного пространства даже в том случае, если эти области расположены между нижним и верхним пределами используемого адресного пространства (а FreeBSD это позволила бы). То, находится ли это в полном соответствии с POSIX, представляет собой интересный, но несущественный вопрос, так как я сомневаюсь в том, что разработчики OpenBSD будут это менять (и я не думаю, что они должны это менять).

Я, занимаясь всем этим, узнал одну интересную вещь. Оказывается, что в Linux, во FreeBSD и в OpenBSD используется нечто вроде различных интерпретаций стандарта POSIX (это — при условии, что я правильно понял код ядра FreeBSD). Linux-интерпретация оказывается самой «свободной», так как в ней разрешено применять munmap() ко всему, что может быть отображено в память в определённых обстоятельствах. OpenBSD, если нужно, вероятно решит, что «допустимый диапазон для адресного пространства процесса» — это диапазон памяти, на который что-то уже отображено, в результате поведение системы соответствует POSIX/SUS, но такой подход, определённо, сдвигает интерпретацию стандарта в необычном направлении, уходя от чётких формулировок спецификации (хотя — это именно то поведение, которого я и ожидал). А во FreeBSD имеется, так сказать, нечто среднее, возможно, связанное с деталями реализации системы.

P.S. В справке по munmap() из Linux даже не говорится о «допустимом адресном пространстве всех процессов или некоего конкретного процесса» как о причине ошибки munmap(); там есть лишь абстрактные рассуждения о том, что ядру могут не понравиться параметры addr или len.

Примечание: небольшая тестовая программа


Вот тестовая программа, которой я пользовался:
#include 
#include 
#include 
#include 

#define MAPLEN  (128*1024)

int main(int argc, char **argv)
{
  void *mp;

  puts("Starting mmap and double munmap test.");
  mp = mmap(0, MAPLEN, PROT_READ, MAP_ANON|MAP_SHARED, -1, 0);
  if (mp == MAP_FAILED) {
    printf("mmap error: %s\n", strerror(errno));
    return 1;
  }
  if (munmap(mp, MAPLEN) < 0) {
    printf("munmap error on first unmap: %s\n", strerror(errno));
    return 1;
  }
  if (munmap(mp, MAPLEN) < 0) {
    printf("munmap error on second unmap: %s\n", strerror(errno));
    return 1;
  }
  puts("All calls succeeded without errors, can munmap() unmapped areas.");
  return 0;
}

Полагаю, что, теоретически, подобная программа может завершиться с ошибкой во FreeBSD, в том случае, если наш вызов mmap() установит новую верхнюю или нижнюю границу адресного пространства процесса. На практике же более вероятно то, что мы вызовем mmap(), передав этой команде сведения о пустом диапазоне между нижней частью адресного пространства (с текстом программы) и его верхней частью (возможно, со стеком).

Встречались ли вы с различиями в поведении каких-нибудь системных вызовов в разных Unix-подобных ОС?

oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru