Mmap linux что это

Обновлено: 06.07.2024

Я уже писал статью, в которой кратко представлял mmap () / munmap () раньше.Управление памятью Linux (9) mmap"Он относительно тонкий, поэтому я подробно разберусь с ним здесь.

Представьте использование двух функций с точки зрения часто используемых пользователей; затем основное внимание уделяется анализу процесса реализации ядра; наконец, выполните некоторые проверочные тесты для mmap () / munmap ().

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

Системный вызов mmap позволяет процессам совместно использовать память, отображая один и тот же общий файл. После того, как обычный файл отображается в адресное пространство процесса, процесс может получить доступ к файлу, как к обычной памяти, без вызова read () / write () и других операций.

mmap не выделяет пространство, а только сопоставляет файл с адресным пространством вызывающего процесса (занимая виртуальное адресное пространство), а затем вы можете использовать memcpy () и другие операции. Содержимое памяти не сразу обновляется в файле, но есть раздел Временная задержка может быть явно синхронизирована с помощью msync ().

Отмените карту памяти с помощью munmap ().

На следующем рисунке показано отображение памяти mmap, начальный адрес - это возвращенный addr, off и len соответствуют параметрам offset и length соответственно.


Использовать mmap () / munmap () относительно просто. Есть две комбинации параметров, которые приводят к разнообразию.protсflags。

Вот краткое введение в эти параметры:

  • адрес: если он не равен NULL, ядро ​​создаст отображение по этому адресу; в противном случае ядро ​​выберет подходящий виртуальный адрес. В большинстве случаев виртуальный адрес не указывается, что не имеет большого значения.Вместо этого ядро ​​решает вернуть адрес для использования в пользовательском пространстве.
  • длина: указывает размер адресного пространства, сопоставленного процессу.
  • prot: атрибуты чтения / записи / выполнения области памяти.
  • флаги: атрибуты отображения памяти, общая, частная, анонимная, файловая и т. д.
  • fd: указывает, что это отображение файла, а fd - дескриптор для открытия файла. Если это отображение файла, вам нужно указать fd; для анонимного сопоставления укажите специальный -1.
  • смещение: во время сопоставления файла он указывает смещение относительно заголовка файла; возвращаемый адрес - это виртуальный адрес, соответствующий смещению.

1.1 преимущества mmap

1.1.1 Повышение эффективности

Как правило, для чтения и записи файлов требуются открытие, чтение и запись.Дисковый файл должен быть сначала прочитан в буфер кэша ядра, а затем скопирован в область памяти пользовательского пространства, при этом предусмотрены две операции чтения и записи.

mmap сопоставляет файл на диске с пространством пользователя. Когда процесс читает файл, происходит прерывание из-за сбоя страницы, и соответствующая физическая память выделяется виртуальной памяти. Данные с диска считываются в физическую память посредством операции подкачки диска, чтобы реализовать данные пользовательского пространства.У всего процесса есть только одна копия памяти.

1.1.2 Используется для передачи больших объемов данных между процессами

Два процесса отображают один и тот же файл. В двух процессах виртуальное адресное пространство, отображаемое в одну и ту же файловую область, различается. Когда процесс работает с файлом, он сначала получает физическую память из-за сбоев страниц, а затем считывает данные файла в память с помощью операций подкачки файлов на диске.

Когда другой процесс обращается к файлу, обнаруживается, что никакая физическая страница не сопоставлена ​​с виртуальной памятью. Обработка сбоев страниц в fs используется для определения того, прочитала ли область кэша файл на диске, и, если есть, соотношение сопоставления устанавливается, так что два процесса могут выполняться через общую память.


1.1.3 Файл закрывается, и можно продолжать использовать память

Поскольку соответствующий дисковый файл был найден через fd в ядре, файл связан с vma.

Недостатки 1.2 mmap

Длина файла была определена во время сопоставления, и диапазон операции len не может быть доступен через mmap.

1.3 Комбинация частного / общего, файлового / анонимного сопоставления

Есть четыре комбинации, которые будут представлены ниже по порядку.

1.3.1 Отображение приватных файлов

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

Например, файл динамической библиотеки linux .so таким образом отображается в виртуальное адресное пространство каждого процесса.

1.3.2 Частное анонимное отображение

mmap создаст новое отображение, которое не является общим для каждого процесса и в основном используется для выделения памяти (malloc вызовет mmap для выделения большой памяти).

1.3.3 Отображение общих файлов

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

1.3.4 Совместное анонимное сопоставление

Этот механизм не использует копирование при записи при форках. Родительский и дочерний процессы полностью используют одну и ту же страницу физической памяти, которая является связью между родительским и дочерним процессами.

Точкой входа в системный вызов является entry_SYSCALL_64_fastpath, а затем найдите соответствующую функцию в sys_call_table согласно номеру системного вызова.

Системные вызовы, соответствующие mmap () и munmap ():SyS_mmap()сSyS_munmap() Проанализируем реализацию ниже.

2.0 путь вызова mmap / munmap

Прежде чем анализировать конкретную реализацию ядра, посмотрите путь вызова mmap / munmap через скрипт.

Добавляя функцию set_ftrace_filter и изменяя вызывающий объект функции, найденной current_tracer, путь вызова постепенно расширяется.

Наконец, используйте трекер function_graph, чтобы просмотреть отношения вызова следующим образом:

Начнем анализ с этого пути.

2.1 mmap()

Ядром системного вызова mmap () является do_mmap (), который можно разделить на три части.

Первая часть использует функцию get_unmapped_area () для поиска виртуального адреса в диапазоне [addr, addr + len].

Пользовательские процессы обычно не указывают адрес, то есть ядро ​​указывает, где находится первый адрес этого виртуального пространства.

Перед тем, как функция do_mmap_pgoff () вызовет get_unmapped_area (), addr будет предварительно определен, реализован round_hint_to_min (), а затем вызван с этим предварительно заданным addr в качестве параметраget_unmapped_area()。

Вторая часть определяет флаги линейной области vma, которые различаются для файлов, анонимности, приватности и общего доступа.

Третья часть - это фактическое создание области прецедента vma с помощью функцииmmap_region()достигать.

get_unmapped_area () находит виртуальное пространство, которое удовлетворяет условиям через get_area () в соответствии с входным адресом и другими параметрами, и возвращает первый адрес этого виртуального пространства.

get_area () - указатель на функцию, есть две возможности использовать mm-> get_unmapped_area () или file-> f_op-> get_unmapped_area ().

Глядя на имя arch_get_unmapped_area (), вы можете знать, что каждая архитектура может иметь свою собственную функцию реализации. Здесь мы анализируем с помощью платформо-независимых функций.

arch_get_unmapped_area() Завершите создание нового отображения от нижнего адреса к высокому адресу, и arch_get_unmapped_area_topdown () завершит создание нового отображения от высокого адреса к низкому адресу.

mmap_region () сначала вызывает find_vma_links (), чтобы определить, есть ли адрес в линейной области vma, если есть вызов do_munmap (), чтобы убить vma.

Linux не хочет пустоты между vma и vma. Пока атрибут flags вновь созданной vma и передняя или задняя vma fairy boy, попытайтесь объединить их в новую vma, чтобы уменьшить потребление блочного кэша, а также уменьшить расточительство дыр.

Если его нельзя объединить, вам необходимо создать новую виртуальную машину и инициализировать структуру виртуальной машины перед закрытием членов; в зависимости от того, имеет ли виртуальная машина флага блокировки страницы (VM_LOCKED), решается, следует ли выделять физическую страницу немедленно.

Наконец, вставьте вновь созданный vma в красно-черное дерево vma пространства процесса и вернитесь к addr.

2.2 munmap

Проверьте, используется ли целевой адрес в виртуальном пространстве текущего процесса. Если он уже используется, старое сопоставление будет отменено. Если эта операция завершится неудачно, перейдите к free_vma. Поскольку бит флага flags равен 1, когда MAP_FIXED равен 1, это не проверяется.

munmap () используется для удаления отображения памяти, и его основная функция - do_munmap ().

2.3 msync()

Изменения процесса в содержимом отображаемого пространства памяти не записываются напрямую обратно на диск, и операция обычно выполняется после вызова munmap ().

Функция msync () синхронизирует содержимое отображаемого пространства памяти с файлом на диске.

2.4 Связь между malloc и brk () / mmap ()

к getconf PAGESIZE Проверьте текущий размер системной страницы, мы увидим, что текущий размер системной страницы равен 4096.

malloc () выделяет память, не обязательно через brk (); если выделенная память достигает 128 КБ, это необходимо сделать через mmap.

Давайте посмотрим, как разные размеры MAX влияют на malloc.

Когда MAX равно (4096 * 31 + 4072), системные вызовы слежения выглядят следующим образом:

.
brk(0x244c000) = 0x244c000
brk(0x242c000) = 0x242c000
exit_group(0) = ?
+++ exited with 0 +++

Когда MAX равен (4096 * 31 + 4073), системные вызовы слежения выглядят следующим образом:

.
mmap(NULL, 135168, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f12b88c9000
munmap(0x7f12b88c9000, 135168) = 0
exit_group(0) = ?
+++ exited with 0 +++

Видно, что когда размер выделенной памяти близок к 128 КБ, malloc () будет выровнен до 128 КБ, а 1 страница будет добавлена ​​как пробел. Фактически выделенное виртуальное адресное пространство достигло 132 КБ.

3.1 Преимущества mmap () / munmap () перед read () / write ()

Как уже упоминалось выше, работа с памятью после mmap () происходит быстрее, чем при обычном read () / write (). Вот простой тест.

Сначала создайте два пустых файла по 128 КБ.

dd bs=1024 count=128 if=/dev/zero of=./mmap_test

dd bs=1024 count=128 if=/dev/zero of=./mmap_test2

Содержимое двух файлов изменилось на A и B. Видно, что mmap намного впереди:

Time of read/write: 134us
Time of mmap/munmap/msync: 91us

3.2 mmap и / proc / xxx / анализ карт

Выполните указанное выше приложение через strace и получите следующий процесс системного вызова.

Ниже по очереди проанализируйте влияние mmap () / munmap () на пространство отображения процессов.


Для объяснения, пожалуйста, обратитесь к следующему описанию в руководстве по системному программированию UNIX.

Следующие функции используются исключительно для поддержки механизма семафоров:

newary()

newary() обращается к ipc_alloc() для распределения памяти под новый набор семафоров. Она распределяет объем памяти достаточный для размещения дескриптора набора и всего набора семафоров. Распределенная память очищается и адрес первого элемента набора семафоров передается в ipc_addid(). Функция ipc_addid() резервирует память под массив элементов нового набора семафоров и инициализирует ( struct kern_ipc_perm) набор. Глобальная переменная used_sems увеличивается на количество семафоров в новом наборе и на этом инициализация данных ( struct kern_ipc_perm) для нового набора завершается. Дополнительно выполняются следующие действия:

  • В поле sem_base заносится адрес первого семафора в наборе.
  • Очередь sem_pending объявляетяс пустой.

Все операции, следующие за вызовом ipc_addid(), выполняются под глобальной блокировкой семафоров. После снятия блокировки вызывается ipc_buildid() (через sem_buildid()). Эта функция создает уникальный ID (используя индекс дескриптора набора семафоров), который и возвращается в вызывающую программу.

freeary()

Функция freeary() вызывается из semctl_down() для выполнения действий, перечисленных ниже. Вызов функции осуществляется под глобальной блокировкой семафоров, возврат управления происходит со снятой блокировкой.

  • Вызывается ipc_rmid() (через "обертку" sem_rmid()), чтобы удалить ID набора семафоров и получить указатель на набор семафоров.
  • Аннулируется список откатов для данного набора семафоров
  • Все ожидающие процессы пробуждаются, чтобы получить код ошибки EIDRM.
  • Общее количество семафоров уменьшается на количество семафоров в удаляемом наборе.
  • Память, занимаемая набором семафоров, освобождается.

semctl_down()

Функция semctl_down() предназначена для выполнения операций IPC_RMID и IPC_SET системного вызова semctl(). Перед выполнением этих операций проверяется ID набора семафоров и права доступа. Обе эти операции выполняются под глобальной блокировкой семафоров.

IPC_RMID

Операция IPC_RMID вызывает freeary() для удаления набора семафоров.

IPC_SET

Операция IPC_SET изменяет элементы uid , gid , mode и ctime в наборе семафоров.

semctl_nolock()

Функция semctl_nolock() вызывается из sys_semctl() для выполнения операций IPC_INFO, SEM_INFO и SEM_STAT.

IPC_INFO и SEM_INFO

Операции IPC_INFO и SEM_INFO заполняют временный буфер seminfo статическими данными. Затем под глобальной блокировкой семафора ядра sem_ids.sem заполняются элементы semusz и semaem структуры seminfo в соответствии с требуемой операцией (IPC_INFO или SEM_INFO) и в качестве результата возвращается максимальный ID.

SEM_STAT

Операция SEM_STAT инициализирует временный буфер semid64_ds. На время копирования значений sem_otime , sem_ctime , и sem_nsems в буфер выполняется глобальная блокировка семафора. И затем данные копируются в пространство пользователя.

semctl_main()

Функция semctl_main() вызывается из sys_semctl() для выполнения ряда операций, которые описаны ниже. Перед выполнением операций, semctl_main() блокирует семафор и проверяет ID набора семафоров и права доступа. Перед возвратом блокировка снимается.

GETALL

Операция GETALL загружает текущие значения семафора во временный буфер ядра и затем копирует его в пространство пользователя. При небольшом объеме данных, временный буфер размещается на стеке, иначе блокировка временно сбрасывается, чтобы распределить в памяти буфер большего размера. Копирование во временный буфер производится под блокировкой.

SETALL

Операция SETALL копирует значения семафора из пользовательского пространства во временный буфер и затем в набор семафоров. На время копирования из пользовательского пространства во временный буфер и на время проверки значений блокировка сбрасывается. При небольшом объеме данных, временный буфер размещается на стеке, иначе в памяти размещается буфер большего размера. На время выполнения следующих действий блокировка восстанавливается:

  • Информация копируется в набор семафоров.
  • Очищается очередь откатов для заданного набора семафоров.
  • Устанавливается значение sem_ctime для набора семафоров.
  • Вызывается функция update_queue() , которая проходит по списку ожидающих операций в поисках задач, которые могут быть завершены в результате выполнения операции SETALL. Будятся любые ожидающие задачи, которые больше не нужно блокировать.

IPC_STAT

Операция IPC_STAT копирует значения sem_otime , sem_ctime и sem_nsems во временный буфер на стеке. После снятия блокировки данные копируются в пользовательское пространство.

GETVAL

Операция GETVAL возвращает значение заданного семафора.

GETPID

Операция GETPID возвращает pid последней операции, выполненной над семафором.

GETNCNT

Операция GETNCNT возвращает число процессов, ожидающих на семафоре, когда тот станет меньше нуля. Это число подсчитывается функцией count_semncnt().

GETZCNT

Операция GETZCNT возвращает число процессов, ожидающих на семафоре, когда тот станет равным нулю. Это число подсчитывается функцией count_semzcnt().

SETVAL

Проверяет новое значение семафора и выполняет следующие действия:

  • В очереди отката отыскиваются любые корректировки данного семафора и эти корректировки сбрасываются в ноль.
  • Значение семафора устанавливается в заданное.
  • Корректируется значение sem_ctime .
  • Вызывается функция update_queue(), которая проходит по очереди ожидающих операций в поисках тех из них, которые могут быть завершены в результате выполнения операции SETVAL. Все задачи которые оказываются больше незаблокированными - пробуждаются.

count_semncnt()

count_semncnt() возвращает число процессов, ожидающих на семафоре, когда тот станет меньше нуля.

count_semzcnt()

count_semzcnt() возвращает число процессов, ожидающих на семафоре, когда тот станет равным нулю.

update_queue()

update_queue() проходит по очереди ожидающих операций заданного набора семафоров и вызывает try_atomic_semop() для каждой последовательности операций. Если статус элемента очереди показывает, что заблокированная задача уже была разбужена, то такой элемент пропускается. В качестве аргумента do_undo в функцию try_atomic_semop() передается флаг q-alter , который указывает на то, что любые изменяющие операции необходимо "откатить" перед возвратом управления.

Если последовательность операций заблокирована, то update_queue() возвращает управление без внесения каких-либо изменений.

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

Если последовательность операций не предполагает внесения изменений, то в качестве аргумента do_undo в функцию try_atomic_semop() передается ноль. Если выполнение этих операций увенчалось успехом, то они считаются выполненными и удаляются из очереди. Ожидающая задача активируется, а в поле status ей передается признак успешного завершения операций.

Если последовательность операций, которая предполагает внесение изменений в значение семафоров, признана успешной, то статус очереди принимает значение 1, чтобы активировать задачу. Последовательность операций не выполняется и не удаляется из очереди, она будет выполняться разбуженной задачей.

try_atomic_semop()

Функция try_atomic_semop() вызывается из sys_semop() и update_queue() и пытается выполнить каждую из операций в последовательности.

Если была встречена заблокированная операция, то процесс исполнения последовательности прерывается и все операции "откатываются". Если последовательность имела флаг IPC_NOWAIT, то возвращается код ошибки -EAGAIN. Иначе возвращается 1 для индикации того, что последовательность операций заблокирована.

Если значение семафора вышло за рамки системных ограничений, то выполняется "откат" всех операций и возвращается код ошибки -ERANGE.

Если последовательность операций была успешно выполнена и при этом аргумент do_undo не равен нулю, то выполняется "откат" всех операций и возвращается 0. Если аргумент do_undo равен нулю, то результат операций остается в силе и обновляется поле sem_otime .

sem_revalidate()

Функция sem_revalidate() вызывается, когда глобальная блокировка семафора временно была снята и необходимо снова ее получить. Вызывается из semctl_main() и alloc_undo(). Производит проверку ID семафора и права доступа, в случае успеха выполняет глобальную блокировку семафора.

freeundos()

Функция freeundos() проходит по списку "откатов" процесса в поисках заданной структуры. Если таковая найдена, то она изымается из списка и память, занимаемая ею, освобождается. В качестве результата возвращается указатель на следующую структуру "отката"в списке.

alloc_undo()

Вызов функции alloc_undo() должен производиться под установленной глобальной блокировкой семафора. В случае возникновения ошибки - функция завершает работу со снятой блокировкой.

Перед тем как вызовом kmalloc() распределить память под структуру sem_undo и массив корректировок, блокировка снимается. Если память была успешно выделена, то она восстанавливается вызовом sem_revalidate().

Далее новая структура инициализируется, указатель на структуру размещается по адресу, указанному вызывающей программой, после чего структура вставляется в начало списка "откатов" текущего процесса.

sem_exit()

Функция sem_exit() вызывается из do_exit() и отвечает за выполнение всех "откатов" по завершении процесса.

Если процесс находится в состоянии ожидания на семафоре, то он удаляется из списка sem_queue при заблокированном семафоре.

Производится просмотр списка "откатов" текущего процесса и для каждого элемента списка выполняются следующие действия:

  • Проверяется структура "отката" и ID набора семафоров.
  • В списке "откатов" соответствующего набора семафоров отыскиваются ссылки на структуры, которые удаляются из списка.
  • К набору семафоров применяются корректировки из структуры "отката".
  • Обновляется поле sem_otime в наборе семафоров.
  • Вызывается update_queue(), которая просматривает список отложенных операций и активирует задачи, которые могут быть разблокированы в результате "отката".
  • Память, занимаемая структурой, освобождается.

По окончании обработки списка очищается поле current->semundo.

Интерфейс системных вызовов

sys_msgget()

sys_msgctl()

IPC_INFO ( или MSG_INFO)

IPC_STAT ( или MSG_STAT)

IPC_SET

IPC_RMID

sys_msgsnd()

sys_msgrcv()

Привет. Меня зовут Вячеслав Бирюков. В Яндексе я руковожу группой эксплуатации поиска. Недавно для студентов Курсов информационных технологий Яндекса я прочитал лекцию о работе с памятью в Linux. Почему именно память? Главный ответ: работа с памятью мне нравится. Кроме того, информации о ней довольно мало, а та, что есть, как правило, нерелевантна, потому что эта часть ядра Linux меняется достаточно быстро и не успевает попасть в книги. Рассказывать я буду про архитектуру x86_64 и про Linux­-ядро версии 2.6.32. Местами будет версия ядра 3.х.

Эта лекция будет полезна не только системным администраторам, но и разработчикам программ высоконагруженных систем. Она поможет им понять, как именно происходит взаимодействие с ядром операционной системы.

Термины

Резидентная память – это тот объем памяти, который сейчас находится в оперативной памяти сервера, компьютера, ноутбука.
Анонимная память – это память без учёта файлового кеша и памяти, которая имеет файловый бэкенд на диске.
Page fault – ловушка обращения памяти. Штатный механизм при работе с виртуальной памятью.

Работа с памятью организована через страницы. Объём памяти, как правило, большой, присутствует адресация, но операционной системе и железу не очень удобно работать с каждым из адресов отдельно, поэтому вся память и разбита на страницы. Размер страницы – 4 KБ. Также существуют страницы другого размера: так называемые Huge Pages размером 2 MБ и страницы размером 1 ГБ (о них мы говорить сегодня не будем).

Виртуальная память – это адресное пространство процесса. Процесс работает не с физической памятью напрямую, а с виртуальной. Такая абстракция позволяет проще писать код приложений, не думать о том, что можно случайно обратиться не на те адреса памяти или адреса другого процесса. Это упрощает разработку приложений, а также позволяет превышать размер основной оперативной памяти за счёт описанных ниже механизмов. Виртуальная память состоит из основной памяти и swap-устройства. То есть объём виртуальной памяти может быть в принципе неограниченного размера.

  • 0 – значение по умолчанию. В этом случае используется эвристика, которая следит за тем, чтобы мы не смогли выделить виртуальной памяти в процессе намного больше, чем есть в системе;
  • 1 – говорит о том, что мы никак не следим за объёмом выделяемой памяти. Это полезно, например, в программах для вычислений, которые выделяют большие массивы данных и работают с ними особым способом;
  • 2 – параметр, который позволяет строго ограничивать объем виртуальной памяти процесса.

Memory Zones и NUMA

В современных системах вся виртуальная память делится на NUMA-ноды. Когда­-то у нас были компьютеры с одним процессором и одним банком памяти (memory bank). Называлась такая архитектура UMA (SMP). Всё было предельно понятно: одна системная шина для общения всех компонентов. В последствии это стало неудобно, начало ограничивать развитие архитектуры, и, как следствие, была придумана NUMA.


Как видно из слайда, у нас есть два процессора, которые общаются между собой по какому-­то каналу, и у каждого из них есть свои шины, через которые они общаются со своими банками памяти. Если мы посмотрим на картинку, то задержка от CPU 1 к RAM 1 в NUMA­-ноде будет в два раза меньше, чем от CPU 1 на RAM 2. Получить эти данные и прочую информацию мы можем, используя команду numactl ­­hardware .

Мы видим, что сервер имеет две ноды и информацию по ним (сколько в каждой ноде свободной физической памяти). Память выделяется на каждой ноде отдельно. Поэтому можно потребить всю свободную память на одной ноде, а другую — недогрузить. Чтобы такого не было (это свойственно базам данных), можно запускать процесс с командой numactl ­­interleave=all. Это позволяет распределять выделение памяти между двумя нодам равномерно. В противном случае ядро выбирает ноду, на которой был запланирован запуск этого процесса (CPU scheduling) и всегда пытается выделить память на ней.

Также память в системе поделена на Memory Zones. Каждая NUMA-­нода делится на какое­-то количество таких зон. Они служат для поддержки специального железа, которое не может общаться по всему диапазону адресов. К примеру, ZONE_DMA – это 16 MБ первых адресов, ZONE_DMA32 – это 4 ГБ. Смотрим на зоны памяти и их состояние через файл /proc/zoneinfo .

Page Cache

Через Page Cache в Linux по умолчанию идут все операции чтения и записи. Он динамического размера, то есть именно он съест всю вашу память, если она свободна. Как гласит старая шутка, если вам нужна свободная память в сервере, просто вытащите ее из сервера. Page Cache делит все файлы, которые мы читаем, на страницы (страница, как мы сказали, – 4 KБ). Посмотреть, есть ли в Page Cache какие-­то страницы какого-­то конкретного файла, можно с помощью системного вызова mincore() . Или с помощью утилиты vmtouch, которая написана с использованием этого системного вызова.

Как же происходит запись? Любая запись происходит на диск не сразу, а в Page Cache, и делается это практически моментально. Тут можно увидеть интересную «аномалию»: запись на диск идет намного быстрее, чем чтение. Дело в том, что при чтении (если данной странички файла в Page Cache нет) мы пойдем в диск и будем синхронно ждать ответа, а запись в свою очередь пройдет моментально в кеш.

Минусом такого поведения является то, что на самом деле данные никуда не записались, — они просто находятся в памяти, и когда-­то их нужно будет сбросить на диск. У каждой странички при записи проставляется флажок (он называется dirty). Такая «грязная» страничка появляется в Page Cache. Если накапливается много таких страничек, система понимает, что пора их сбросить на диск, а то можно их потерять (если внезапно пропадет питание, наши данные тоже пропадут).

Память процесса

Процесс состоит из следующих сегментов. У нас есть stack, который растет вниз; у него есть лимит дальше котрого он расти не может.


Затем идет регион mmap: там находятся все отображенные на память файлы процесса, которые мы открыли или создали через системный вызов mmap() . Далее идет большое пространство невыделенной виртуальной памяти, которую мы можем использовать. Снизу вверх растет heap – это область анонимной памяти. Внизу идут области бинарника, который мы запускаем.

Если мы говорим о памяти внутри процесса, то работать со страницами тоже неудобно: как правило, выделение памяти внутри процесса происходит блоками. Очень редко требуется выделить одну­-две странички, обычно нужно выделить сразу какой-­то промежуток страниц. Поэтому в Linux существует такое понятие, как область памяти (virtual memory area, VMA), которая описывает какое-­то пространство адресов внутри виртуального адресного пространства этого процесса. На каждую такую VMA есть свои права (чтения, записи, исполнения) и области видимости: она может быть приватная или общая (которая «шарится (share)» с другими процессами в системе).

Выделение памяти

Выделение памяти можно поделить на четыре случая: есть выделение приватной памяти и памяти, которой можем с кем-­то поделиться (share); двумя другими категорями являются разделение на анонимную память и ту, у которая связана с файлом на диске. Самые частые функции выделения памяти – это malloc и free. Если мы говорим о glibc malloc() , то он выделяет анонимную память таким интересным способом: использует heap для аллокации маленьких объемов (менее 128 KБ) и mmap() для больших объемов. Такое выделение необходимо для того, чтобы память расходовалась оптимальнее и её можно было запросто отдавать в систему. Если в heap не хватает памяти для выделения, вызывается системный вызов brk() , который расширяет границы heap. Системный вызов mmap() занимается тем, что отображает содержимое файла на адресное пространство. munmap() в свою очередь освобождает отображение. У mmap() есть флаги, которые регулируют видимость изменений и уровень доступа.


На самом деле, Linux не выделяет всю запрошенную память сразу. Процесс выделения памяти — Demand Paging — начинается с того, что мы запрашиваем у ядра системы страничку памяти, и она попадает в область Only Allocated. Ядро отвечает процессу: вот твоя страница памяти, ты можешь её использовать. И больше ничего происходит. Никакой физической аллокации не происходит. А произойдет она только в том случае, если мы попробуем в эту страницу произвести запись. В этот момент пойдёт обращение в Page Table – эта структура транслирует виртуальные адреса процесса в физические адреса оперативной памяти. При этом будут задействованы также два блока: MMU и TLB, как видно из рисунка. Они позволяют ускорять выделение и служат для трансляции виртуальных адресов в физические.


После того, как мы понимаем, что этой странице в Page Table ничего не соответствует, то есть нет связи с физической памятью, мы получаем Page Fault – в данном случае минорный (minor), так как отсутствует обращение в диск. После этого процесса система может производить запись в выделенную страницу памяти. Для процесса все это происходит прозрачно. А мы можем наблюдать увеличение счетчика минорных Page Fault для процесса на одну единицу. Также бывает мажорный Page Fault – в случае, когда происходит обращение в диск за содержимым страницы (в случае mmpa() ).

Один из трюков в работе с памятью в Linux – Copy On Write – позволяет делать очень быстрые порождения процессов (fork).

Работа с файлами и с памятью

Подсистема памяти и подсистема работы с файлами тесно связаны. Так как работа с диском напрямую очень медленна, ядро использует в качестве прослойки оперативную память.

malloc() использует больше памяти: происходит копирование в user space. Также потребляется больше CPU, и мы получаем больше переключений контекста, чем если бы мы работали с файлом через mmap() .

Какие выводы можно сделать? Мы можем работать с файлами, как с памятью. У нас есть lazy lоading, то есть мы можем замапить очень­-очень большой файл, и он будет подгружаться в память процесса через Page Cache только по мере надобности. Всё также происходит быстрее, потому что мы используем меньше системных вызовов и, в конце концов, это экономит память. Ещё стоит отметить, что при завершении программы память никуда не девается и остается в Page Cache.

В начале было сказано, что вся запись и чтение идут через Page Cache, но иногда по какой­-то причине, есть необходимость в отходе от такого поведения. Некоторые программные продукты работают таким способом, например MySQL с InnoDB.

  • posix_fadvide();
  • madvise();
  • mincore().

Readahead

Поговорим про Readahead. Если читать файлы с диска через Page Cache каждый раз постранично, то у нас будет достаточно много Page Fault и мы будем часто ходить на диск за данными. Поэтому мы можем управлять размером Readahead: если мы прочитали первую и вторую страничку, то ядро понимает, что, скорее всего, нам нужна и третья. И так как ходить на диск дорого, мы можем прочитать немного больше заранее, загрузив файл наперёд в Page Cache и отвечать в будущем из него. Таким образом происходит замена будущих тяжёлых мажорных (major) Page Faults на минорные (minor) page fault.

Итак мы выдали всем память, все процессы довольны, и внезапно память у нас закончилась. Теперь нам нужно ее как­-то освобождать. Процесс поиска и выделения свободной памяти в ядре называется Page Reclaiming. В памяти могут находится страницы памяти, которые нельзя забирать, – залокированные страницы (locked). Помимо них есть ещё четыре категории страниц. Cтраницы ядра, которые выгружать не стоит, потому что это затормозит всю работу системы; cтраницы Swappable – это такие страницы анонимной памяти, которые никуда, кроме как в swap устройство выгрузить нельзя; Syncable Pages – те, которые могут быть синхронизированы с диском, а в случае открытого файла только на чтение – такие страницы можно с лёгкостью выбросить из памяти; и Discardable Pages – это те страницы, от которых можно просто отказаться.

Источники пополнения Free List

Если говорить упрощённо, то у ядра есть один большой Free List (на самом деле, это не так), в котором хранятся страницы памяти, которые можно выдавать процессам. Ядро пытается поддерживать размер этого списка в каком­-то не нулевом состоянии, чтобы быстро выдавать память процессам. Пополняется этот список за счёт четырех источников: Page Cache, Swap, Kernel Memory и OOM Killer.

Мы должны различать участки памяти на горячую и холодную и как­-то пополнять за счет них наши Free Lists. Page Cache устроен по принципу LRU/2 ­очереди. Есть активный список страниц (Active List) и инактивный список (Inactive List) страничек, между которыми есть какая­-то связь. В Free List прилетают запросы на выделение памяти (allocation). Система отдаёт страницы из головы этого списка, а в хвост списка попадают страницы из хвоста инактивного (inactive) списка. Новые страницы, когда мы читаем файл через Page Cache, всегда попадают в голову и проходят до конца инактивного списка, если в эти страницы не было еще хотя бы одного обращения. Если такое обращение было в любом месте инактивного списка, то страницы попадают сразу в голову активного списка и начинают двигаться в сторону его хвоста. Если же в этот момент опять к ним происходит обращение, то страницы вновь пробиваются в верх списка. Таким образом система пытается сбалансировать списки: самые горячие данные всегда находятся в Page Cache в активном списке, и Free List никогда не пополняется за их счет.

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

Итого у нас получается пять таких листов: Active Anon, Inactive Anon, Active File, Inactive File, Unevictable. Такие списки создаются для каждой NUMA ноды и для каждой Memory Zone.


Как показано на рисунке выше, mmap - это метод управления этими устройствами. Так называемые рабочие устройства, такие как порты ввода-вывода (загораются светодиодные индикаторы), контроллеры ЖК-дисплея и контроллеры дисков, на самом деле являются Чтение и запись данных на физический адрес устройства.

Однако, поскольку прикладная программа не может напрямую управлять аппаратным адресом устройства, операционная система предоставляет такой механизм: Отображение памяти, адрес устройства отображается на виртуальный адрес процесса, mmap - это интерфейс, который реализует отображение памяти.

Есть много способов работы с оборудованием, например ioctl, ioremap.

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

Что такое отображение памяти?

Поскольку mmap - это интерфейс, реализующий отображение памяти, что такое отображение памяти? Посмотрите на картинку ниже


Каждый процесс имеет независимое адресное пространство. С помощью таблицы страниц и MMU виртуальные адреса могут быть преобразованы в физические адреса. Каждый процесс имеет независимые данные таблицы страниц. Это объясняет, почему два разных процесса имеют одинаковый виртуальный адрес. Он соответствует другому физическому адресу.

Что такое виртуальное адресное пространство?

Каждый процесс имеет виртуальное адресное пространство 4G, из которых пространство пользователя 3G, пространство ядра 1G (linux), каждый процесс разделяет пространство ядра, независимое пространство пользователя, это ярко выражено на следующем рисунке.


Драйвер работает в пространстве ядра, поэтому драйвер предназначен для всех процессов.

Есть два способа переключиться из пользовательского пространства в пространство ядра:

(1) Системный вызов, т.е. мягкое прерывание

(2) Аппаратное прерывание

Что находится в виртуальном адресном пространстве?

Зная, что такое виртуальное адресное пространство, что находится в виртуальном адресном пространстве? Посмотрите на картинку ниже


Виртуальное пространство, вероятно, содержит указанные выше данные. Отображение памяти, вероятно, сопоставляет адрес устройства с красным сегментом на приведенном выше рисунке. В настоящее время он называется «сегментом сопоставления памяти». Что касается того, на какой адрес отображается, он выделяется операционной системой. Операционная система делит пространство процесса на три части:

(1) Нераспределенный, то есть неиспользуемый адрес процесса

(2) Кэширование, страницы кэшируются в оперативной памяти

(3) Не кэшируется, не кэшируется в оперативной памяти

Операционная система выделит виртуальный адрес в нераспределенном адресном пространстве, чтобы установить сопоставление с адресом устройства. О том, как создать сопоставление, мы сообщим позже.

Теперь вы, вероятно, понимаете, что такое «отображение памяти», так как же ядро ​​управляет этими адресными пространствами? Любая сложная теория в конечном итоге отражается в различных структурах данных, и эта структура данных здесь является дескриптором процесса. С точки зрения ядра процесс - это носитель, который распределяет системные ресурсы (ЦП, память). Чтобы управлять процессом, ядро ​​должно четко описывать, что делает каждый процесс. Это дескриптор процесса. Ядро использует структуру task_struct для представления процесса. , И поддерживать связанный список структуры для управления всеми процессами. Структура содержит тысячи элементов, таких как статус процесса и информация о расписании. Здесь мы в основном сосредотачиваемся на дескрипторе процесса. Дескриптор памяти (struct mm_struct mm)

Дескриптор памяти

Конкретную структуру см. На рисунке ниже.


Теперь мы знаем, что отображение памяти должно отображать адрес устройства в адрес пространства процесса (примечание: не все отображение памяти отображается в адресное пространство процесса, ioremap отображается в виртуальное пространство ядра, mmap отображается на виртуальный адрес процесса) По сути, структура vm_area_struct выделяется и добавляется в адресное пространство процесса, то есть для отображения адреса устройства в эту структуру процесс отображения - это то, что делает драйвер.

Реализация отображения памяти

В качестве примера возьмем драйвер символьного устройства. Обычно символьное устройство работает следующим образом.


Основная задача отображения памяти - реализовать функцию mmap () в пространстве ядра. Давайте сначала разберемся со структурой драйвера символьного устройства.


Ниже приведен исходный код mmap_driver.c.

Ниже приведен тестовый код test_mmap.c

Ниже находится make-файл

  1. ifneq ($(KERNELRELEASE),)
  2. obj-m := mmap_driver.o
  3. else
  4. KDIR := /lib/modules/3.2.0-52-generic/build
  5. all:
  6. make -C $(KDIR) M=$(PWD) modules
  7. clean:
  8. rm -f *.ko *.o *.mod.o *.mod.c *

Следующая команда демонстрирует процесс компиляции, установки и тестирования драйвера (Примечание: другим пользователям необходимо изменить разрешения после mknod)

В отношении этого процесса задействованы некоторые термины.

(1) Файл устройства: в Linux оборудование виртуализировано как файл устройства, и к файлу устройства применимы различные операции с обычными файлами.

(2) Индексный узел: Linux использует индексный узел для записи информации о файле (такой как длина файла, время создания и модификации). Он хранится на диске. После чтения в память это структура inode. Файловая система поддерживает массив индексных узлов. Каждый элемент имеет однозначное соответствие с файлами или каталогами.

(3) Основной номер устройства: например, 999 выше, указывает тип устройства, например, является ли оно ЖК-дисплеем или USB и т. Д.

(4) Дополнительный номер устройства: например, 0 выше, означает разные устройства на этом типе устройства.

(5) Три структуры файлов (обычные файлы или файлы оборудования)

①Файловые операции: struct file_operations

② Файловый объект: struct file

③ Узел индекса файла: struct inode

Что касается реализации отображения памяти в драйвере, давайте сначала разберемся в процессе открытия и закрытия

(1) Процесс открытия драйвера устройства


①Приложение вызывает open ("/ dev / mmap_driver", O_RDWR);

③ Затем в соответствии с указателем open в file_operations в индексном узле файла устройства вызывается метод open драйвера.

④ Создание структуры файлового объекта files_struct, и система поддерживает связанный список files_struct, представляющий все открытые файлы в системе.

⑤ Вернуть файловый дескриптор fd и добавить fd в таблицу файловых дескрипторов процесса.

(2) Процесс закрытия драйвера устройства

Приложение вызывает close (fd), и, наконец, может быть вызвано закрытие драйвера.Почему можно найти функцию закрытия драйвера на основе простого типа int fd? Это тесно связано с тремя упомянутыми выше структурами (struct file_operations, struct file, struct inode), если fd = 3


(3) Процесс mmap драйвера устройства

Аналогично, известное как open и close, приложение, вызывающее mmap, в конечном итоге вызовет метод mmap в драйвере.

① функция mmap в приложении test.mmap.c

void* mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr: начальный адрес виртуального адреса после сопоставления, обычно NULL, автоматически назначается ядром

длина: размер области отображения

prot: права доступа к странице (PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE)

flags: справочная сетевая информация

fd: дескриптор файла

смещение: начальное смещение отображения файла

② функция mmap в mmap_driver.c драйвера

Как упоминалось выше, основная задача mmap - сопоставить адрес устройства с виртуальным адресом процесса, который представляет собой структуру vm_area_struct. Упомянутое здесь сопоставление - очень важная вещь. Какова его производительность в программе? —— Таблица страниц, да, это таблица страниц, и отображение предназначено для создания таблицы страниц. Адресное пространство процесса может быть сопоставлено с адресом устройства через таблицу страниц (программное обеспечение) и MMU (оборудование).



virt_to_phys (buf), buf - это адрес, на который подается запрос при открытии. Здесь virt_to_phys используется для преобразования buf в физический адрес, который является имитацией аппаратного устройства, то есть виртуальное устройство отображается на виртуальный адрес. На практике физический адрес может использоваться напрямую.

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

②Простой небольшой модуль драйвера включает управление процессами (адресное пространство процесса), управление памятью (таблица страниц и отображение кадров страниц), виртуальную файловую систему ( struct file 、 struct inode )

③Не все драйверы устройств можно использовать mmap Для сопоставления, например последовательных портов и других устройств, ориентированных на потоки, и должны отображаться в соответствии с размером страницы.

Читайте также: