Что такое виртуальное адресное пространство
Что такое виртуальное адресное пространство
Прилагательное «виртуальное» применительно к виртуальному адресному пространству означает, что это общее число доступных приложению уникально адресуемых ячеек памяти, но не общий объём памяти, установленной в компьютере, или выделенной в конкретный момент времени данному приложению.
В случае с нашим приложением объём его виртуального адресного пространства равен 15000 байт.
Для реализации виртуальной памяти в компьютере должен быть специальный аппаратный механизм управления памятью. Часто этот механизм называют устройством управления памятью (Memory Management Unit, MMU ). Если MMU отсутствует, при обращении процессора к памяти реальный адрес в памяти никогда не меняется — адрес 123 всегда соответствует одной физической ячейке ОЗУ.
Однако с MMU адреса проходят этап преобразования, прежде чем произойдёт обращение к памяти. Это значит, что адресу памяти 123 в одном случае может соответствовать физический адрес 82043, а в другом случае — физический адрес 20468. Но если сопоставлять виртуальные адреса физическим для каждого из миллиардов байт памяти, издержки будут слишком велики. Вместо этого, MMU делит ОЗУ на страницы — непрерывные блоки памяти заданного размера, которые рассматриваются MMU как одно целое.
Может показаться, что введение страниц и преобразования адресов — необязательный и непонятный дополнительный шаг. Однако он очень важен для реализации виртуальной памяти. Поэтому давайте рассмотрим следующую ситуацию.
Вернувшись к нашему гипотетическому приложению с виртуальным адресным пространством 15000 байт, предположим, что первая инструкция обращается к данным по адресу 12374. Также предположим, что в нашем компьютере всего 12288 байт физической памяти. Что произойдёт, если процессор попытается обратиться к адресу 12374?
Ошибка страницы — это последовательность событий, происходящих, когда программа пытается обращаться к данным (или коду) в своём адресном пространстве, которые в данный момент отсутствуют в ОЗУ. Операционная система может обработать ошибки страницы, каким-то образом загрузив запрашиваемые данные в память, что позволит программе продолжить работу, как если бы никакой ошибки не было.
В случае с нашим гипотетическим приложением, процессор сначала передаёт нужный адрес (12374) устройству MMU. Однако, MMU не может преобразовать этот адрес. Поэтому MMU прерывает процессор и вызывает программу, так называемый обработчик ошибки страницы. Затем обработчик ошибки страницы определяет, что нужно сделать для решения этой проблемы. Он может:
найти, где находится на диске запрошенная страница, и прочитать её (это обычно происходит в случае ошибки страницы с кодом)
определить, что запрошенная страница уже находится в ОЗУ (но выделена не текущему процессу), и подключить эту страницу, настроив MMU
указать на специальную страницу с одними нулями и выделить для процесса новую страницу, только если процесс когда-нибудь попытается записать в неё данные (этот приём называется копированием при записи и часто используется для страниц, изначально содержащих нули)
получить нужную страницу откуда-то ещё (это будет рассмотрено более подробно позже)
Тогда как первые три действия достаточно очевидны, последнее вовсе нет. Чтобы разобраться с ним, мы должны обсудить некоторые дополнительные темы.
Набор страниц физической памяти, в данный момент выделенных определённому процессу, называется рабочим множеством этого процесса. Число страниц в рабочем множестве может увеличиваться и уменьшаться, в зависимости от общей доступности страниц в системе в целом.
Рабочее множество процесса увеличивается при увеличении числа ошибок страницы, и уменьшается по мере того, как остаётся всё меньше свободных страниц. Чтобы память не была израсходована полностью, время от времени страницы должны исключаться из рабочих множеств процессов и становиться свободными, доступными для дальнейшего использования. Операционная система сжимает рабочие множества процессов:
записывая изменённые страницы в выделенную область устройства хранения (обычно эта область называется пространством подкачки ).
помечая неизменённые страницы как свободные (записывать эти страницы на диск нет необходимости, так как они не менялись)
Чтобы определить подходящий размер рабочих множеств всех процессов, операционная система должна следить за использованием всех страниц. Таким образом операционная система определяет, какие страницы используются активно (и должны остаться в памяти), а какие — нет (и следовательно, могут быть удалены из памяти). Для того, чтобы определить, какие страницы лучше удалить из рабочих множеств процессов, чаще всего применяется некая разновидность алгоритма учёта последних использованных страниц.
Хотя подкачка (запись изменённых страниц в пространство подкачки и последующая их загрузка) — обычное явление при работе системы, подкачка может быть слишком активной. Слишком активная подкачка нежелательна, так как при этом может легко произойти, а затем снова и снова повторяться следующая ситуация:
Страницы процесса выгружаются в пространство подкачки
Этот процесс становится исполняемым и пытается обратиться к выгруженной странице
Происходит ошибка страницы и нужная страница загружается в память (при этом, вероятнее всего, вытесняя страницу другого процесса)
Чуть позже страница снова выгружается на диск
Если эта последовательность событий повторяется неоднократно, происходит перегрузка и это говорит о том, что для текущей нагрузки памяти не хватает. Такие перегрузки крайне негативно влияют на производительность, так как возникающая в такой ситуации нагрузка процессора и подсистемы ввода/вывода быстро превышает полезную нагрузку системы. В экстремальных случаях компьютер может вообще не выполнять полезную работу, расходуя все свои ресурсы на перемещение страниц в память и из памяти.
Виртуальное адресное пространство
Виртуальное адресное пространство зависит от:
· операционной системы (которая может накладывать дополнительные ограничения)
Виртуальное адресное пространство не зависит от:
Адреса команд и переменных в готовой машинной программе, подготовленной к выполнению системой программирования, как раз и являются виртуальными адресами.
Виртуальное адресное пространство процесса.
13) Критические области. Семафоры. Мьютексы. Мониторы
В некоторых операционных системах процессы, работающие совместно, могут сообща использовать некое общее хранилище данных.
Каждый из процессов может считывать из общего хранилища данных и записывать туда информацию.
Это хранилище представляет собой участок в основной памяти (возможно, в структуре данных ядра) или файлобщего доступа. Местоположение совместно используемой памяти не влияет на суть взаимодействия и возникающие проблемы.
Ситуации, в которых два (и более) процесса считывают или записывают данные одновременно и конечный результат зависит от того, какой из них был первым, называютсясостояниями состязания.
Отладка программы, в которой возможно состояние состязания, вряд ли может доставить удовольствие. Результаты большинства тестовых прогонов будут хорошими, но изредка будет происходить
Нечто странное и необъяснимое
Как избежать состязания?
Основным способом предотвращения проблем в этой и любой другой ситуации, связанной с совместным использованием памяти, файлов и чего-либо еще, является запрет одновременной записи и чтения разделенных данных более чем одним процессом.
Говоря иными словами, необходимо взаимное исключение.
Это означает, что в тот момент, когда один процесс использует разделенные данные, другому процессу это делать будет запрещено.
Некоторый промежуток времени процесс занят внутренними расчетами и другими задачами, не приводящими к состояниям состязания. В другие моменты времени процесс обращается к совместно используемым данным или выполняет какое-то другое действие, которое может привести к состязанию.
Часть программы, в которой есть обращение к совместно используемым данным, называется критической областью или критической секцией.
Если нам удастся избежать одновременного нахождения двух процессов в критических областях, мы сможем избежать состязаний.
Несмотря на то что это требование исключает состязание, его недостаточно для правильной совместной работы параллельных процессови эффективного использования общих данных. Для этого необходимо выполнение четырех условий:
· 1. Два процесса не должны одновременно находиться в критических областях.
· 2. В программе не должно бытьпредположенийо скорости или количестве процессоров.
· 3. Процесс, находящийся вне критической области, не может блокировать другие процессы.
· 4. Невозможна ситуация, в которой процесс вечно ждет попадания в критическую область.
Семафо́р — объект, позволяющий войти в заданный участок кода не более чем N потокам. Определение введено Эдсгером Дейкстрой
В 1965 году Дейкстра предложил использовать целую переменную для подсчета сигналов запуска, сохраненных на будущее.
Им был предложен новый тип переменных, так называемые семафоры, значение которых может быть нулем (в случае отсутствия сохраненных сигналов активизации) или некоторым положительным числом, соответствующим количеству отложенных сигналов активации.
Дейкстра предложил две операции,down и up (обобщения sleep и wakeup).
1) Операция down сравнивает значение семафора с нулем. Если значение семафора больше нуля, операция down уменьшает его(то есть расходует один из сохраненных сигналов активации)и просто возвращает управление. Если значение семафора равно нулю, процедура down не возвращает управление процессу, а процесс переводится в состояние ожидания. Все операции проверки значения семафора, его изменения и перевода процесса в состояние ожидания выполняютсякак единое и неделимое элементарное действие.Тем самым гарантируется, что после начала операции ни
один процесс не получит доступа к семафору до окончания или блокирования операции.Элементарность операции чрезвычайно важна для разрешения проблемы синхронизации и предотвращения состояния состязания.
2) Операция up увеличивает значение семафора. Если с этим семафором связаны один или несколько ожидающих процессов, которые не могут завершить более раннюю операцию down, один из них выбирается системой (например, случайным образом) и ему разрешается завершить свою операцию down. Таким образом, после
операции up, примененной к семафору, связанному с несколькими ожидающими процессами, значение семафора так и останется равным 0, но число ожидающих процессов уменьшится на единицу.Операция увеличения значения семафора и активизации процесса тоже неделима.
Ни один процесс не может быть блокирован во время выполнения операции up.
Иногда используется упрощенная версия семафора, называемаямьютексом (mutex, сокращение от mutual exclusion — взаимное исключение).
Мьютекс не способен считать, он может лишь управлять взаимным исключением доступа к совместно используемым ресурсам или кодам.
Реализация мьютекса проста и эффективна, что делает использование мьютексов особенно полезным в случае потоков, действующих только в пространстве пользователя.
Мьютекс — переменная, которая может находиться в одном из двух состояний: блокированном или неблокированном. Поэтому для описания мьютекса требуется всего один бит, хотя чаще используется целая переменная, у которой 0 означает неблокированное состояние, а все остальные значения соответствуют блокированному состоянию. Значение мьютекса устанавливается двумя процедурами. Если поток (или процесс) собирается войти в критическую область, он вызывает процедуру mutex_lock(). Если мьютекс не заблокирован (то есть вход в критическую область разрешен), запрос выполняется и вызывающий поток может попасть в критическую область.
Напротив, если мьютекс заблокирован, вызывающий поток блокируется до тех пор, пока другой поток, находящийся к критической области, не выйдет из нее, вызвав процедуру mutex_unlock.
Если мьютекс блокирует несколько потоков, то из них случайным образомвыбираетсяодин.
Чтобы упростить написание программ, в 1974 году Хоар (Ноаге) [155] и Бринч Хансен (Brinch Hansen) [43] предложилипримитив синхронизации более высокого уровня, называемый монитором.
Их предложения несколько отличались друг от друга, как мы увидим дальше.
Монитор — набор процедур, переменных и других структур данных, объединенных в особый модуль или пакет.
Процессы могут вызывать процедуры монитора, но у процедур, объявленных вне монитора, нет прямого доступа к внутренним структурам данных монитора.
Реализации взаимных исключений способствует важное свойство монитора: при обращении к монитору в любой момент времени активным может быть только один процесс.
Мониторы являются структурным компонентом языка программирования, поэтому компилятор знает, что обрабатывать вызовы процедур монитора следует иначе, чем вызовы остальных процедур.
Обычно при вызове процедуры монитора первые несколько команд процедуры проверяют, нет ли в мониторе активного процесса. Если активный процесс есть, вызывающему процессу придется подождать, в противном случае запрос удовлетворяется.
Реализация взаимного исключения зависит от компилятора, но обычно используется мьютекс или бинарный семафор. Поскольку взаимное исключение обеспечивает компилятор, а не программист, вероятность ошибки гораздо меньше. В любом случае программист, пишущий код монитора, не должен задумываться о том, как компилятор организует взаимное исключение.
Достаточно знать, что, обеспечив попадание в критические области через процедуры монитора, можно не
бояться попадания в критическую область двух процессов одновременно.
Хотя мониторы предоставляют простой способ реализации взаимного исключения, этого недостаточно. Необходим также способ блокировки процессов, которые не могут продолжать свою деятельность.В случае проблемы производителя и потребителя достаточно просто поместить все проверки буфера на заполненность
и пустоту в процедуры монитора, но как процесс заблокируется, обнаружив полный буфер?
Решение заключается во введении переменных состояния и двух операций, wait и signal. Когда процедура монитора обнаруживает, что она не в состоянии продолжать работу (например, производитель выясняет, что буфер заполнен), она выполняет операцию wait на какой-либо переменной состояния, скажем, full. Это приводит к блокировке вызывающего процесса и позволяет другому процессу войти в монитор.
Другой процесс, в нашем примере потребитель может активизировать ожидающего напарника, например, выполнив операцию signal на той переменной состояния, на которой он был заблокирован.
Чтобы в мониторе не оказалось двух активных процессов одновременно, нам необходимо правило, определяющее последствия операции signal. Хоар предложил запуск «разбуженного» процесса и остановку второго.
Бринч Хансен предложил другое решение:процесс, выполнивший signal, должен немедленно покинуть монитор. Иными словами, операция signal выполняется только в самом конце процедуры монитора. Мы будем использовать это решение, поскольку оно в принципепроще и к тому же легче в реализации.
Если операция signal выполнена на переменной, с которой связаны несколько заблокированных процессов, планировщик выбирает и «оживляет» только один из них. Кроме этого, существует третье решение, не основывающееся на предположениях Хоара и Бринча Хансена: позволить процессу, выполнившему signal, продолжать работу и запустить ждущий процесс только после того, как первый процесс покинет монитор.
Переменные состояния не являются счетчиками. В отличие от семафоров они не аккумулируют сигналы, чтобы впоследствии воспользоваться ими. Это означает, что в случае выполнения операции signal на переменной состояния, с которой не связано ни одного блокированного процесса, сигнал будет утерян. Проще говоря, операция wait должна выполняться прежде, чем signal. Это правило существенно упрощает реализацию. На практике это правило не создает проблем, поскольку отслеживать состояния процессов при необходимости не очень трудно.
Общие условия выбора системы дренажа: Система дренажа выбирается в зависимости от характера защищаемого.
Механическое удерживание земляных масс: Механическое удерживание земляных масс на склоне обеспечивают контрфорсными сооружениями различных конструкций.
Организация стока поверхностных вод: Наибольшее количество влаги на земном шаре испаряется с поверхности морей и океанов (88‰).
виртуальное адресное пространство и физическое служба хранилища
максимальный объем физической памяти, поддерживаемой Microsoft Windows, составляет от 2 гб до 24 тб в зависимости от версии Windows. дополнительные сведения см. в разделе ограничения памяти для выпусков Windows. Виртуальное адресное пространство каждого процесса может быть меньше или больше, чем общая физическая память, доступная на компьютере. Подмножество виртуального адресного пространства процесса, находящегося в физической памяти, называется рабочим набором. Если потоки процесса пытаются использовать больше физической памяти, чем доступно в данный момент, система замещает часть содержимого памяти на диск. Общий объем виртуального адресного пространства, доступного для процесса, ограничен физической памятью и свободным местом на диске, доступным для файла подкачки.
Физическое хранилище и виртуальное адресное пространство каждого процесса организованы в виде страниц, единиц памяти, размер которых зависит от главного компьютера. Например, на компьютерах x86 размер страницы узла составляет 4 КБ.
Чтобы максимально повысить гибкость управления памятью, система может перемещать страницы физической памяти в файл подкачки на диске и из него. При перемещении страницы в физической памяти система обновляет карты страниц затрагиваемых процессов. Когда системе требуется место в физической памяти, она перемещает наименее недавно использованные страницы физической памяти в файл подкачки. Обработка физической памяти системой полностью прозрачна для приложений, которые работают только в своих виртуальных адресных пространствах.
Организация виртуальной памяти
Привет, Хабрахабр!
В предыдущей статье я рассказал про vfork() и пообещал рассказать о реализации вызова fork() как с поддержкой MMU, так и без неё (последняя, само собой, со значительными ограничениями). Но прежде, чем перейти к подробностям, будет логичнее начать с устройства виртуальной памяти.
Конечно, многие слышали про MMU, страничные таблицы и TLB. К сожалению, материалы на эту тему обычно рассматривают аппаратную сторону этого механизма, упоминая механизмы ОС только в общих чертах. Я же хочу разобрать конкретную программную реализацию в проекте Embox. Это лишь один из возможных подходов, и он достаточно лёгок для понимания. Кроме того, это не музейный экспонат, и при желании можно залезть “под капот” ОС и попробовать что-нибудь поменять.
Введение
Любая программная система имеет логическую модель памяти. Самая простая из них — совпадающая с физической, когда все программы имеют прямой доступ ко всему адресному пространству.
При таком подходе программы имеют доступ ко всему адресному пространству, не только могут “мешать” друг другу, но и способны привести к сбою работы всей системы — для этого достаточно, например, затереть кусок памяти, в котором располагается код ОС. Кроме того, иногда физической памяти может просто не хватить для того, чтобы все нужные процессы могли работать одновременно. Виртуальная память — один из механизмов, позволяющих решить эти проблемы. В данной статье рассматривается работа с этим механизмом со стороны операционной системы на примере ОС Embox. Все функции и типы данных, упомянутые в статье, вы можете найти в исходном коде нашего проекта.
Будет приведён ряд листингов, и некоторые из них слишком громоздки для размещения в статье в оригинальном виде, поэтому по возможности они будут сокращены и адаптированы. Также в тексте будут возникать отсылки к функциям и структурам, не имеющим прямого отношения к тематике статьи. Для них будет дано краткое описание, а более полную информацию о реализации можно найти на вики проекта.
Общие идеи
Аппаратная поддержка
Обращение к памяти хорошо описанно в этой хабростатье. Происходит оно следующим образом:
Процессор подаёт на вход MMU виртуальный адрес
Если MMU выключено или если виртуальный адрес попал в нетранслируемую область, то физический адрес просто приравнивается к виртуальному
Если MMU включено и виртуальный адрес попал в транслируемую область, производится трансляция адреса, то есть замена номера виртуальной страницы на номер соответствующей ей физической страницы (смещение внутри страницы одинаковое):
Если запись с нужным номером виртуальной страницы есть в TLB [Translation Lookaside Buffer], то номер физической страницы берётся из нее же
Если нужной записи в TLB нет, то приходится искать ее в таблицах страниц, которые операционная система размещает в нетранслируемой области ОЗУ (чтобы не было промаха TLB при обработке предыдущего промаха). Поиск может быть реализован как аппаратно, так и программно — через обработчик исключения, называемого страничной ошибкой (page fault). Найденная запись добавляется в TLB, после чего команда, вызвавшая промах TLB, выполняется снова.
Таким образом, при обращении программы к тому или иному участку памяти трансляция адресов производится аппаратно. Программная часть работы с MMU — формирование таблиц страниц и работа с ними, распределение участков памяти, установка тех или иных флагов для страниц, а также обработка page fault, ошибки, которая происходит при отсутствии страницы в отображении.
В тексте статьи в основном будет рассматриваться трёхуровневая модель памяти, но это не является принципиальным ограничением: для получения модели с бóльшим количеством уровней можно действовать аналогичным образом, а особенности работы с меньшим количеством уровней (как, например, в архитектуре x86 — там всего два уровня) будут рассмотрены отдельно.
Программная поддержка
Виртуальный адрес
Page Global Directory (далее — PGD) — таблица (здесь и далее — то же самое, что директория) самого высокого уровня, каждая запись в ней — ссылка на Page Middle Directory (PMD), записи которой, в свою очередь, ссылаются на таблицу Page Table Entry (PTE). Записи в PTE ссылаются на реальные физические адреса, а также хранят флаги состояния страницы.
То есть, при трёхуровневой иерархии памяти виртуальный адрес будет выглядеть так:
Значения полей PGD, PMD и PTE — это индексы в соответствующих таблицах (то есть сдвиги от начала этих таблиц), а offset — это смещение адреса от начала страницы.
В зависимости от архитектуры и режима страничной адресации, количество битов, выделяемых для каждого из полей, может отличаться. Кроме того, сама страничная иерархия может иметь число уровней, отличное от трёх: например, на x86 нет PMD.
Для обеспечения переносимости мы задали границы этих полей с помощью констант: MMU_PGD_SHIFT, MMU_PMD_SHIFT, MMU_PTE_SHIFT, которые в приведённой выше схеме равны 24, 18 и 12 соответственно их определение дано в заголовочном файле src/include/hal/mmu.h. В дальнейшем будет рассматриваться именно этот пример.
На основании сдвигов PGD, PMD и PTE вычисляются соответствующие маски адресов.
Эти макросы даны в том же заголовочном файле.
Для работы с виртуальной таблицами виртуальной памяти в некоторой области памяти хранятся указатели на все PGD. При этом каждая задача хранит в себе контекст struct mmu_context, который, по сути, является индексом в этой таблице. Таким образом, к каждой задаче относится одна таблица PGD, которую можно определить с помощью mmu_get_root(ctx).
Страницы и работа с ними
Размер страницы
В реальных (то есть не в учебных) системах используются страницы от 512 байт до 64 килобайт. Чаще всего размер страницы определяется архитектурой и является фиксированным для всей системы, например — 4 KiB.
С одной стороны, при меньшем размере страницы память меньше фрагментируется. Ведь наименьшая единица виртуальной памяти, которая может быть выделена процессу — это одна страница, а программам очень редко требуется целое число страниц. А значит, в последней странице, которую запросил процесс, скорее всего останется неиспользуемая память, которая, тем не менее, будет выделена, а значит — использована неэффективно.
С другой стороны, чем меньше размер страницы, тем больше размер страничных таблиц. Более того, при отгрузке на HDD и при чтении страниц с HDD быстрее получится записать несколько больших страниц, чем много маленьких такого же суммарного размера.
В дальнейшем речь пойдёт о страницах обычного размера.
Устройство Page Table Entry
В реализации проекта Embox тип mmu_pte_t — это указатель.
Каждая запись PTE должна ссылаться на некоторую физическую страницу, а каждая физическая страница должна быть адресована какой-то записью PTE. Таким образом, в mmu_pte_t незанятыми остаются MMU_PTE_SHIFT бит, которые можно использовать для сохранения состояния страницы. Конкретный адрес бита, отвечающего за тот или иной флаг, как и набор флагов в целом, зависит от архитектуры.
Можно установить сразу несколько флагов:
Здесь vmem_page_flags_t — 32-битное значение, и соответствующие флаги берутся из первых MMU_PTE_SHIFT бит.
Трансляция виртуального адреса в физический
Как уже писалось выше, при обращении к памяти трансляция адресов производится аппаратно, однако, явный доступ к физическим адресам может быть полезен в ряде случаев. Принцип поиска нужного участка памяти, конечно, такой же, как и в MMU.
Для того, чтобы получить из виртуального адреса физический, необходимо пройти по цепочке таблиц PGD, PMD и PTE. Функция vmem_translate() и производит эти шаги.
Сначала проверяется, есть ли в PGD указатель на директорию PMD. Если это так, то вычисляется адрес PMD, а затем аналогичным образом находится PTE. После выделения физического адреса страницы из PTE необходимо добавить смещение, и после этого будет получен искомый физический адрес.
Пояснения к коду функции.
mmu_paddr_t — это физический адрес страницы, назначение mmu_ctx_t уже обсуждалось выше в разделе “Виртуальный адрес”.
С помощью функции vmem_get_idx_from_vaddr() находятся сдвиги в таблицах PGD, PMD и PTE.
Работа с Page Table Entry
Для работы с записей в таблице страниц, а так же с самими таблицами, есть ряд функций:
Эти функции возвращают 1, если у соответствующей структуры установлен бит MMU_PAGE_PRESENT
Page Fault
Page fault — это исключение, возникающее при обращении к странице, которая не загружена в физическую память — или потому, что она была вытеснена, или потому, что не была выделена.
В операционных системах общего назначения при обработке этого исключения происходит поиск нужной странице на внешнем носителе (жёстком диске, к примеру).
Выталкивание страниц во внешнюю память и их чтение в случае page fault не реализовано. С одной стороны, это лишает возможности использовать больше физической памяти, чем имеется на самом деле, а с другой — не является актуальной проблемой для встраиваемых систем. Нет никаких ограничений, делающих невозможной реализацию данного механизма, и при желании читатель может попробовать себя в этом деле 🙂
Выделение памяти
Для виртуальных страниц и для физических страниц, которые могут быть использованы при работе с виртуальной памятью, статически резервируется некоторое место в оперативной памяти. Тогда при выделении новых страниц и директорий они будут браться именно из этого места.
Исключением является набор указателей на PGD для каждого процесса (MMU-контексты процессов): этот массив хранится отдельно и используется при создании и разрушении процесса.
Выделение страниц
Итак, выделить физическую страницу можно с помощью vmem_alloc_page
Функция page_alloc() ищет участок памяти из N незанятых страниц и возвращает физический адрес начала этого участка, помечая его как занятый. В приведённом коде virt_page_allocator ссылается на участок памяти, резервированной для выделения физических страниц, а 1 — количество необходимых страниц.
Выделение таблиц
Тип таблицы (PGD, PMD, PTE) не имеет значения при аллокации. Более того, выделение таблиц производится также с помощью функции page_alloc(), только с другим аллокатором (virt_table_allocator).
Участки памяти (Memory Area)
После добавления страниц в соответствующие таблицы нужно уметь сопоставлять участки памяти с процессами, к которым они относятся. У нас в системе процесс представлен структурой task, содержащей всю необходимую информацию для работы ОС с процессом. Все физически доступные участки адресного пространства процесса записываются в специальный репозиторий: task_mmap. Он представляет из себя список дескрипторов этих участков (регионов), которые могут быть отображены на виртуальную память, если включена соответствующая поддержка.
brk — это самый большой из всех физических адресов репозитория, данное значение необходимо для ряда системных вызовов, которые не будут рассматриваться в данной статье.
ctx — это контекст задачи, использование которого обсуждалось в разделе “Виртуальный адрес”.
struct dlist_head — это указатель на начало двусвязного списка, организация которого аналогична организации Linux Linked List.
За каждый выделенный участок памяти отвечает структура marea
Поля данной структуры имеют говорящие имена: адреса начала и конца данного участка памяти, флаги региона памяти. Поле mmap_link нужно для поддержания двусвязного списка, о котором говорилось выше.
Отображение виртуальных участков памяти на физические (Mapping)
Ранее уже рассказывалось о том, как происходит выделение физических страниц, какие данные о виртуальной памяти относятся к задаче, и теперь всё готово для того, чтобы говорить о непосредственном отображении виртуальных участков памяти на физические.
Отображение виртуальных участков памяти на физическую память подразумевает внесение соответствующих изменений в иерархию страничных директорий.
Подразумевается, что некоторый участок физической памяти уже выделен. Для того, чтобы выделить соответствующие виртуальные страницы и привязать их к физическим, используется функция vmem_map_region()
В качестве параметров передаётся контекст задачи, адрес начала физического участка памяти, а также адрес начала виртуального участка. Переменная flags содержит флаги, которые будут установлены у соответствующих записей в PTE.
Основную работу на себя берёт do_map_region(). Она возвращает 0 при удачном выполнении и код ошибки — в ином случае. Если во время маппирования произошла ошибка, то часть страниц, которые успели выделиться, нужно откатить сделанные изменения с помощью функции vmem_unmap_region(), которая будет рассмотрена позднее.
Рассмотрим функцию do_map_region() подробнее.
Макросы GET_PTE и GET_PMD нужны для лучшей читаемости кода. Они делают следующее: если в таблице памяти нужный нам указатель не ссылается на существующую запись, нужно выделить её, если нет — то просто перейти по указателю к следующей записи.
В самом начале необходимо проверить, выровнены ли под размер страницы размер региона, физический и виртуальный адреса. После этого определяется PGD, соответствующая указанному контексту, и извлекаются сдвиги из виртуального адреса (более подробно это уже обсуждалось выше).
Затем последовательно перебираются виртуальные адреса, и в соответствующих записях PTE к ним привязывается нужный физический адрес. Если в таблицах отсутствуют какие-то записи, то они будут автоматически сгенерированы при вызове вышеупомянутых макросов GET_PTE и GET_PMD.
Освобождение виртуального участка памяти (Unmapping)
После того, как участок виртуальной памяти был отображён на физическую, рано или поздно её придётся освободить: либо в случае ошибки, либо в случае завершения работы процесса.
Изменения, которые при этом необходимо внести в структуру страничной иерархии памяти, производятся с помощью функции vmem_unmap_region().
Все параметры функции, кроме последнего, должны быть уже знакомы. free_pages отвечает за то, должны ли быть удалены страничные записи из таблиц.
try_free_pte, try_free_pmd, try_free_pgd — это вспомогательные функции. При удалении очередной страницы может выясниться, что директория, её содержащая, могла стать пустой, а значит, её нужно удалить из памяти.
нужны как раз для случая двухуровневой иерархии памяти.
Заключение
Конечно, данной статьи не достаточно, чтобы с нуля организовать работу с MMU, но, я надеюсь, она хоть немного поможет погрузиться в OSDev тем, кому он кажется слишком сложным.