Ядро ОС (Linux) выделяет виртуальную память процессу, используя набор смежных блоков памяти, называемых vm_area_struct. Каждая VMA состоит из:

Ядро группирует все эти распределения в mm_struct внутри дескриптора процесса task_struct.

mm_struct содержит список всех выделенных VMA в двух структурах данных, список VMA, отсортированный по адресам, и как часть красно-черного дерева для каждого процесса. Всякий раз, когда мы вызываем системный вызов mmap(), мы обычно передаем addr=NULL, чтобы ядро ​​предоставило нераспределенное пространство и сопоставило его с адресным пространством нашего процесса. Это красно-черное дерево (mm_rb), которое используется для более быстрого поиска незанятого пространства в адресном пространстве процесса. Я попытался изучить, как ядро ​​выполняет этот процесс mmap, и дальнейшее чтение — это попытка написать об этом в блоге.

Это карта виртуальной памяти процесса:

Как видно, TASK_UNMAPPED_BASE = 0x40000000 используется в качестве базового адреса для всей общей памяти, отображений памяти и общих библиотек в 32-разрядной системе. Это значение будет использоваться в функции vma_unmapped_area() для начала поиска нераспределенной области. Существуют разные подходы к распределению: восходящий и сверху вниз. Я рассмотрю восходящий подход, при котором распределение начинается с TASK_UNMAPPED_BASE. В вашей системе вы можете проверить, каков порядок распределения, проанализировав 2 выхода.

Проверьте порядок загрузки библиотеки с помощью readelf:

Здесь libpthread.so должен быть отображен первым, затем libc.so, порядок может быть дополнительно подтвержден запуском strace на исполняемом файле.

Хотя libpthread загружается первым, он сопоставляется с более высоким адресом, что указывает на подход сверху вниз к распределению.

При восходящем подходе
vma → vm_start = TASK_UNMAPPED_BASE
vma → vm_end = vma → vm_start + len;

Ядро подпрограмм mmap() начинается с функции do_mmap_pgoff():

После проверки работоспособности переданного адреса и смещения, переданного пользователем, вызывается get_unmapped_area().

Это вызовет функцию get_unmapped_area(), назначенную mm_struct, или, если предоставлена ​​файловая система, вызовет функцию get_unammaped_area из f_op( ).

В любом случае нам нужно вызвать приведенную ниже процедуру, чтобы получить ожидаемую функцию vma(), если пользователь указал адрес.

Если адрес не был указан, мы создадим запрос vm_unmapped_area_info, а затем вызовем vm_unmapped_area().

Здесь это выглядит как очень общий запрос,
low_limit =TASK_UNMAPPED_BASE, high_limit = TASK_SIZE.

Итак, первый случай: если нет памяти из зоны mmap (слово зоны не имеет отношения к зонам памяти), то помечаем адрес highest_vm_end как начало и возвращаемся.

Если есть корневой узел или последующие узлы, нам нужно найти подходящее место или дыру, соответствующую нашему запросу (len байт). Для оптимизации этого процесса ядро ​​поддерживает переменную «rb_subtree_gap».

Вычисление rb_subtree_gap() происходит следующим образом:

Основываясь на этом методе расчета гэпа, я написал ниже несколько примеров:

  1. Если присутствует только корневой узел, разрыв указывает на vm_start.
  2. Если vma выделена по более высокому адресу, то она должна стать правым дочерним элементом корневого узла.
  3. Если vma выделена по более низкому адресу, она должна стать левым дочерним элементом корневого узла.
  4. Промежуток рассчитывается как
    max(начало — предыдущее →конец,дети →rb_subtree_gap)

Итак, на основании рис. 5 и комментария, написанного в начале функции unmapped_area(), можно найти подходящее отверстие, соответствующее нашему запросу. Для этого адреса будет вызван метод mmap_region(), чтобы выделить vm_area_struct и вставить его как в список, так и в красно-черное дерево.

Я все еще изучаю это, поэтому некоторая информация может быть упрощена, чтобы я запомнил концепцию. Приветствуются любые исправления, предложения и т.д.

Использованная литература: