Что такое страница памяти
Страница памяти — это цифровой аналог семейного альбома: развернутый некролог, фотографии, случаи из жизни и воспоминания родных и друзей. Затем страницу можно дополнить фильмом, фотографией памятника и геокоординатами могилы. Примеры:
Газета некрологов — лучшее место для страницы памяти:
Цель Газеты некрологов — донести память о родных и близких до будущих поколений.
Цифровой памятник
В нашем изменчивом мире, где могилы живут не дольше людей, где сносят памятники и закрывают кладбища, где социальные сети меняют правила, а семейные архивы гибнут при очередном переезде, увековечить память о родных и близких можно только в цифровом виде на специализированном сайте.
Necrology.com — это виртуальный мемориал, миссия которого — сохранить память о человеке для его потомков и для всего человечества.
Портал не требует регулярных платежей и не публикует рекламу на мемориальных страницах. Это стало возможным, потому что стоимость хранения данных год от года падает, а историческая ценность базы растет.
Вечная память
Страница памяти публикуется один раз и навечно. Ни её содержание, ни адрес в сети никогда не изменятся.
Это значит, что через QR-код на надгробном памятнике посетитель могилы увидит на своем мобильном все то, что рассказали о покойном его родные или даже он сам: фрагменты из биографии, черты характера, случаи из жизни, прощальные слова друзей и близких и даже фото из семейных архивов.
Цифровой памятник становится онлайновым продолжением надгробия из гранита. Или альтернативой.
Интерактивный памятник — надгробие с «говорящим» экраном — оказался невостребованным решением. Сегодня у всех есть смартфоны, и прочитать то, что памятник готов рассказать посетителю, может каждый. Даже не находясь у могилы.
Краеугольный камень церемонии поминок
Страница памяти о покойном может стать центром внимания на поминках на 40 дней или годовщину после смерти. Ссылку на прообраз мемориальной страницы можно разослать гостям поминок или нанести её QR-код на фото и траурные открытки.
После церемонии страницу памяти можно дополнить лучшими высказываниями и воспоминаниями гостей, а также фото из архивов родных и близких.
Безупречный траурный подарок
Страница памяти — исключительный траурный подарок. Своевременный, уместный и бесконечно ценный.
Продолжаем говорить о памяти – Page Sharing

Данная статья является второй из моего цикла статей, посвященного работе с памятью в среде виртуализации в целом, и технологии динамической памяти Hyper-V, которая появится появившейся в Windows Server 2008 R2 SP1 в частности.
Первая часть: habrahabr.ru/blogs/virtualization/93241
Здесь речь пойдет об одной из технологий «перевыделения памяти» под названием Page Sharing.
Как это работает?
Page Sharing – это одна из технологий динамического выделения памяти в гипервизорах, позволяющая выделять виртуальным машинам больше памяти, чем имеется физически – то есть то, что по-английски именуется «memory overcommitment».
Принцип работы этой технологии схож с некоторыми алгоритмами сжатия данных. Начинается все с того, что гипервизор «перелопачивает» все страницы памяти, имеющейся в системе, и вычисляет контрольные суммы (хэши) каждой страницы. Полученные значения заносятся в специальную таблицу. Затем гипервизор ищет в таблице одинаковые значения контрольных сумм, и при обнаружении совпадений производит побитовое сравнение соответствующих страниц. Если обнаруживаются полностью идентичные страницы — то из них остается только одна копия, и впоследствии виртуальные машины при обращении автоматически перенаправляются на нее. Так происходит ровно до тех пор, пока какая-то из виртуальных машин не захочет внести изменения в «расшаренную» страницу. Тогда создается отдельная копия этой страницы, и она уже не «расшаривается», а используется только одной виртуальной машиной. К сожалению, само «перелопачивание» памяти и вычисления хэшей, с дальнейшим поиском совпадений по таблице с побитовым сравнением – сам по себе процесс достаточно ресурсоемкий и долгий, по длительности может занимать вплоть до нескольких часов при больших объемах памяти.
Page Sharing, TLB, Large Memory Pages и все-все-все
Все х86-совместимые процессоры, и все современные 32- и 64-разрядные ОС используют страничную организацию физической и виртуальной памяти. Для каждого приложения производится сопоставление виртуального адреса страницы и физического адреса посредством таблицы страниц. Чтобы ускорить этот процесс сопоставления, современные процессоры используют буфер ассоциативной трансляции (translation lookaside buffer, TLB), который кэширует сопоставления физических и виртуальных адресов, к которым обращались наиболее недавно.
Как правило, область памяти, выделяемая приложению, не является непрерывной, и страницы памяти часто бывают фрагментированы. Но из-за того, что таблица страниц памяти скрывает физические адреса от приложений, приложения «думают», что предоставленная им область памяти является непрерывной. По аналогии – приложения, не работающие с файловой системой напрямую, не имеют понятия о фрагментации отдельных файлов.
Когда запущенное приложение осуществляет доступ к памяти, процессор использует таблицу страниц для преобразования виртуального адреса, используемого приложением, в физический адрес. Как уже было сказано выше, для ускорения этого процесса используется кэширующая система – буфер ассоциативной трансляции. Если запрошенный адрес находится в TLB – процессор может обработать запрос намного быстрее из-за отсутствия необходимости поиска соответствия по всей таблице страниц. Соответственно, если запрошенный адрес в кэше TLB отсутствует – то производится стандартная операция поиска соответствия виртуального и физического адресов в таблице страниц, и лишь после этого запрос может быть обработан.
Из-за огромного количества страниц, эффективность работы кэша TLB имеет огромную важность. В стандартном 32-битном сервере под управлением любой ОС – не важно, Windows это, Linux, или какой-то другой Unix, с 4 Гб RAM, таблица страниц будет содержать миллион записей о каждой 4-килобайтной странице. А теперь представьте, если у нас, к примеру, 64-битная ОС и, скажем, 32 Гб памяти? Получается аж 8 млн. 4-килобайтовых страниц.
Почему использование этой технологии [больших страниц] удобнее? Допустим, наше приложение пытается прочитать 1 Мбайт (1024 Кбайт) непрерывных данных, доступ к которым осуществлялся сравнительно давно (то есть в кэше TLB этот запрос не сохранился). Если страницы памяти имеют размер 4 Кбайт – то это означает, что придется осуществить доступ к 256 страницам. Получается, что нам придется произвести 256 операций поиска в таблице страниц, в которой могут быть миллионы записей. Это займет достаточно много времени.
Теперь представим, что размер страницы равен 2 Мбайт (2048 Кбайт). В этом случае поиск в таблице страниц придется осуществлять единожды, если блок данных в 1 Мбайт, который нам нужен, находится целиком в одной странице, или же дважды – в противном случае. А если еще используется TLB – то процесс протекает еще намного быстрее.
Для маленьких страниц, TLB содержит 32 записи для кэша L1 и 512 записей для кэша L2. Поскольку каждая запись соответствует 4-килобайтной странице, получается, что весь TLB покрывает всего 2 Мбайт виртуальной памяти.
Для больших страниц, буфер TLB содержит 8 записей. Поскольку каждая запись здесь адресует 2 Мбайт памяти, TLB адресуют 16 Мбайт виртуальной памяти. Этот механизм становится намного более эффективным при использовании приложений, требовательных к объему памяти. Представьте, что ваше приложение пытается прочитать, допустим, 2 Гбайт данных. Что будет быстрее — считывание тысячи закэшированных 2-мегабайтных страниц или «перелопачивание» полумиллиона маленьких 4-килобайтных страниц?
Путем нехитрых арифметических действий можно подсчитать количество записей в таблице страниц при различных объемах памяти. Если принять размер страницы в 4 килобайта, то для 4 Гб памяти их будет всего 1 миллион. Для 32 Гб – уже 8 миллионов, для 64 Гб – 16 млн. записей, и аж целых 256 миллионов записей для 1 Тб памяти. А теперь вспомним, что сервера уже давно поддерживают не то, что 32 или 64, а даже 192 Гб памяти (к примеру – HP DL385 G6), а недавно вышедший процессор Intel Nehalem EX по спецификации поддерживает до 256 Гб памяти на один процессорный сокет. Получается, что терабайт памяти – это уже не из области фантастики. Это всего лишь один четырехпроцессорный сервер. Если использовать старую модель организации памяти в виде 4-килобайтных страниц – получим 256 миллионов страниц, и работа с такими объемами памяти будет напоминать вычерпывание плавательного бассейна пивной кружкой. Так что использование больших страниц памяти – это не отдаленное будущее, а самое что ни на есть настоящее.
Подводя резюме: кэш ассоциативной трансляции – это достаточно важный системный ресурс, эффективность использования которого сильно влияет на производительность системы. Стандартом де-факто в 32-битных системах, поддерживающих максимум 4 Гб, памяти была организация памяти в виде страниц длиной 4 Кб. В настоящее время, когда все больше и больше используются 64-битные системы, а объемы памяти могут исчисляться десятками и сотнями гигабайт – использование такой организации памяти может серьезно снизить эффективность использования TLB и, следовательно – производительность системы в целом.
Нельзя так же не упомянуть про новую технологию, именуемую «преобразование адресов второго уровня» (Second Level Address Translation, SLAT). Эта технология именуется по-разному у разных вендоров (у AMD – Rapid Virtualization Indexing (RVI) или Nested Page Tables (NPT), у Intel – Extended Page Tables (EPT)). Она позволяет напрямую преобразовывать гостевые адреса (то есть внутри виртуальной машины) в физические адреса. Такое преобразование позволяет серьезно повысить производительность (официально подтвержден прирост в 20%, некоторые говорят о 100%-м росте производительности) по сравнению с системами, где эта фича не поддерживается. Так что для виртуализации SLAT – это полезно, и является одним из поводов для перехода на железо и софт, поддерживающие его.
Тем не менее, многие забывают, или просто не знают, что SLAT разрабатывали и оптимизировали для работы с большими страницами памяти. Если же поддержка больших страниц не включена – то кэш TLB работает менее эффективно, а использование SLAT с маленькими страницами может привести наоборот к 20%-му падению производительности. Кроме этого, мы не получим 10-20%-го роста производительности от самого использования больших страниц, и, соответственно – в общем и целом потеряем до 40% производительности.
Суммируя вышесказанное мы видим, что Large Memory Pages – очень важный фактор, способный дать прирост производительности до 40%, и вопрос, использовать ли их – является риторическим. Large Memory Pages является продуктом эволюции компьютерных систем, точно так же, как и 64-битная архитектура процессоров, или же jumbo frames в сетевых технологиях.
Казалось бы, причем тут Ю. Лужков?
Единственная проблема при использовании больших страниц заключается в том, что для работы Page Sharing необходимо найти полностью идентичные страницы памяти размером в 2 Мбайт (в сравнении с более мелкими 4-килобайтовыми страницами). Вероятность этого намного ниже (кроме пустых страниц в гостевой ОС, забитых целиком «нулями)), а ESX не пытается разделять большие страницы памяти и именно поэтому экономия памяти за счет использования TPS снижается, когда гипервизор сопоставляет все гостевые страницы с физическими большими страницами.
Говоря кратко — Page Sharing эффективно работает с 4-килобайтными страницами, но при использовании больших, 2-мегабайтных страниц не дает ровным счетом никаких преимуществ.
Пустые страницы
Как ни странно, но эффективнее всего Page Sharing работает при больших объемах неиспользуемой памяти – то есть когда имеется много страниц, забитых нулями. Они полностью идентичны, и «расшариваются» проще всего. Ну примерно как сжатие «квадрата Малевича» в формате bmp. Проблема в том, что некоторые ОС (в частности, Windows 7) стремятся использовать весь доступный объем памяти, и это не баг, а фича – в частности, SuperFetch в Windows Vista/7, о которой я писал ранее. Так вот, это тоже будет приводить к снижению эффективности технологии Page Sharing.
Резюме
В следующей статье мы продолжем разговор о технологиях «memory overcommitment», на этот раз речь пойдет о Second Level Paging.
Пишем операционную систему на Rust. Страничная организация памяти
В этой статье представляем страницы, очень распространённую схему управления памятью, которую мы тоже применим в нашей ОС. Статья объясняет, почему необходима изоляция памяти, как работает сегментация, что такое виртуальная память и как страницы решают проблему фрагментации. Также исследуем схему многоуровневых таблиц страниц в архитектуре x86_64.
Этот блог выложен на GitHub. Если у вас какие-то вопросы или проблемы, открывайте там соответствующий запрос.
Защита памяти
Одна из основных задач операционной системы — изоляция программ друг от друга. Например, браузер не должен вмешиваться в работу текстового редактора. Существуют различные подходы в зависимости от аппаратного обеспечения и реализации ОС.
Например, в некоторых процессорах ARM Cortex-M (во встраиваемых системах) есть блок защиты памяти (MPU), который определяет небольшое количество (например, 8) областей памяти с различными разрешениями доступа (например, нет доступа, только для чтения, для чтения и записи). При каждом доступе к памяти MPU гарантирует, что адрес находится в области с правильными разрешениями, в противном случае выдаёт исключение. Изменяя области и разрешения доступа, ОС гарантирует, что у каждого процесса есть доступ только к своей памяти, чтобы изолировать процессы друг от друга.
На x86 поддерживается два различных подхода к защите памяти: сегментация и страничная организация.
Сегментация
Сегментацию реализовали ещё в 1978 году, первоначально для увеличения объёма адресуемой памяти. В то время CPU поддерживали только 16-разрядные адреса, что ограничивало объём адресуемой памяти 64 КБ. Чтобы увеличить этот объём, ввели дополнительные сегментные регистры, каждый из которых содержит адрес смещения. CPU автоматически добавляет это смещение при каждом доступе к памяти, адресуя таким образом до 1 МБ памяти.
В первой версии сегментации регистры непосредственно содержали смещение и управление доступом не выполнялось. С появлением защищенного режима механизм изменился. Когда CPU работает в таком режиме, дескрипторы сегментов хранят индекс в локальной или глобальной таблице дескрипторов, которая в дополнение к адресу смещения содержит размер сегмента и разрешения доступа. Загружая отдельные глобальные/локальные таблицы дескрипторов для каждого процесса, ОС может изолировать процессы друг от друга.
Изменяя адреса памяти перед фактическим доступом, сегментация реализовала метод, который теперь используется почти везде: это виртуальная память.
Виртуальная память
Чтобы различать два типа адресов, адреса до преобразования называются виртуальными, а адреса после преобразования — физическими. Между ними одно важное различие: физические адреса уникальны и всегда ссылаются на одно и то же уникальное расположение в памяти. С другой стороны, виртуальные адреса зависят от функции преобразования. Два разных виртуальных адреса вполне могут ссылаться на один физический адрес. Кроме того, идентичные виртуальные адреса могут ссылаться на разные физические адреса после преобразования.
В качестве примера полезного использования этого свойства можно привести параллельный запуск одной и той же программы дважды:
Здесь одна и та же программа запускается дважды, но с разными функциями преобразования. У первого экземпляра смещение сегмента 100, так что его виртуальные адреса 0-150 преобразуются в физические адреса 100-250. У второго экземпляра смещение 300, которое преобразует виртуальные адреса 0-150 в физические адреса 300-450. Это позволяет обеим программам выполнять один и тот же код и использовать одни и те же виртуальные адреса, не мешая друг другу.
Ещё одно преимущество в том, что теперь программы можно размещать в произвольных местах физической памяти. Таким образом, ОС использует весь объём доступной памяти без необходимости перекомпиляции программ.
Фрагментация
Различие виртуальных и физических адресов — реальное достижение сегментации. Но есть и проблема. Представьте, что мы хотим запустить третью копию программы, которую видели выше:
Хотя в физической памяти более чем достаточно места, третий экземпляр никуда не помещается. Проблема в том, что ему нужен непрерывный фрагмент памяти и мы не можем использовать отдельные свободные участки.
Один из способов борьбы с фрагментацией — приостановить выполнение программ, переместить используемые части памяти ближе друг к другу, обновить преобразование, а затем возобновить выполнение:
Теперь для запуска третьего экземпляра достаточно места.
Недостаток такой дефрагментации — необходимость копирования больших объёмов памяти, что снижает производительность. Данную процедуру приходится выполнять регулярно, пока память не стала слишком фрагментированной. Производительность становится непредсказуемой, программы останавливаются в произвольное время и могут перестать отвечать на запросы.
Фрагментация — одна из причин, почему в большинстве систем не используется сегментация. На самом деле она больше не поддерживается даже в 64-разрядном режиме на x86. Вместо сегментации используются страницы, которые полностью исключают проблему фрагментации.
Страничная организация памяти
Идея состоит в том, чтобы разделить пространство виртуальной и физической памяти на небольшие блоки фиксированного размера. Блоки виртуальной памяти называются страницами, а блоки физического адресного пространства — фреймами. Каждая страница индивидуально сопоставляется с фреймом, что позволяет разделить большие области памяти между несмежными физическими фреймами.
Преимущество становится очевидным, если повторить пример с фрагментированным пространством памяти, но на этот раз с использованием страниц вместо сегментации:
В этом примере размер страницы 50 байт, то есть каждая из областей памяти разделена на три страницы. Каждая страница сопоставляется с отдельным фреймом, поэтому непрерывную область виртуальной памяти можно сопоставить с изолированными физическими фреймами. Это позволяет запустить третий экземпляр программы без дефрагментации.
Скрытая фрагментация
По сравнению с сегментацией, в страничной организации используется множество небольших областей памяти фиксированного размера вместо нескольких больших областей переменного размера. У каждого фрейма одинаковый размер, так что фрагментация из-за слишком маленьких фреймов невозможна.
Но это только видимость. На самом деле существует скрытый вид фрагментации, так называемая внутренняя фрагментация из-за того, что не каждая область памяти в точности кратна размеру страницы. Представьте в вышеприведённом примере программу размером 101: ей всё равно понадобятся три страницы размером 50, поэтому она займёт на 49 байт больше, чем нужно. Для ясности фрагментацию из-за сегментации называют внешней фрагментацией.
Во внутренней фрагментации ничего хорошего, но часто это меньшее зло, чем внешняя фрагментация. По-прежнему расходуется лишняя память, но теперь не нужно проводить дефрагментацию, а объём фрагментации предсказуем (в среднем полстраницы на каждую область памяти).
Таблицы страниц
Мы увидели, что каждая из миллионов возможных страниц индивидуально сопоставляется с фреймом. Эту информацию о трансляции адресов нужно где-то хранить. При сегментации используются отдельные регистры сегментов для каждой активной области памяти, что невозможно в случае со страницами, потому что их намного больше, чем регистров. Вместо этого здесь используется структура под названием таблицы страниц.
Для вышеприведённого примера таблицы будут выглядеть следующим образом:
При каждом доступе к памяти CPU считывает указатель таблицы из регистра и ищет соответствующий фрейм в таблице. Это полностью аппаратная функция, которая выполняется полностью прозрачно для запущенной программы. Для ускорения процесса во многих процессорных архитектурах есть специальный кэш, который запоминает результаты последних преобразований.
В зависимости от архитектуры, в поле флагов таблицы страниц могут храниться и атрибуты, такие как права доступа. В приведенном выше примере флаг r/w делает страницу доступной для чтения и записи.
Многоуровневые таблицы страниц
Требуется всего четыре физических фрейма, но в таблице страниц более миллиона записей. Мы не можем пропустить пустые записи, потому что тогда CPU в процессе преобразования не сможет перейти напрямую к правильной записи (например, больше не гарантируется, что четвёртая страница использует четвёртую запись).
Для уменьшения потерь памяти можно использовать двухуровневую организацию. Идея в том, что мы используем разные таблицы для разных областей. Дополнительная таблица, которая называется таблицей страниц второго уровня, выполняет преобразование между областями адресов и таблицами страниц первого уровня.
Страница 0 попадает в первую область 10_000 байт, поэтому использует первую запись таблицы страниц второго уровня. Эта запись указывает на таблицу страниц T1 первого уровня, которая определяет, что страница 0 ссылается на фрейм 0.
Принцип двухуровневых таблиц можно расширить на три, четыре и больше уровней. В целом такая система называется многоуровневой или иерархической таблицей страниц.
Зная о страничной организации и многоуровневых таблицах, можно посмотреть, как реализована страничная организация в архитектуре x86_64 (предполагаем, что процессор работает в 64-разрядном режиме).
Страничная организация на x86_64
Архитектура x86_64 использует четырёхуровневую таблицу с размером страницы 4 КБ. Независимо от уровня, в каждой таблице страниц 512 элементов. Каждая запись имеет размер 8 байт, поэтому размеры таблиц 512 × 8 байт = 4 КБ.
Как видим, каждый табличный индекс содержит 9 бит, что имеет смысл, потому что в таблицах 2^9 = 512 записей. Нижние 12 бит — это смещение в 4-килобайтную страницу (2^12 байт = 4 КБ). Биты от 48 до 64 отбрасываются, так что x86_64 на самом деле не 64-разрядная система, а поддерживает только 48-разрядные адреса. Есть планы расширить размер адреса до 57 бит через 5-уровневую таблицу страниц, но ещё не создан такой процессор.
Хотя биты от 48 до 64 отбрасываются, им нельзя задать произвольные значения. Все биты в этом диапазоне должны быть копиями бита 47, чтобы сохранить уникальные адреса и разрешить будущее расширение, например, до 5-уровневой таблицы страниц. Это называется расширением знака (sign-extension), потому что очень похоже на расширение знака в дополнительном коде. Если неправильно расширить адрес, CPU выдаёт исключение.
Пример преобразования
Рассмотрим на примере, как работает преобразование адреса:
С помощью этих индексов мы теперь можем пойти по иерархии таблиц страниц и найти соответствующий фрейм:
Хотя в этом примере используется только один экземпляр каждой таблицы, обычно в каждом адресном пространстве присутствует несколько экземпляров каждого уровня. Максимум:
Формат таблицы страниц
В архитектуре x86_64 таблицы страниц по сути представляют собой массивы из 512 записей. В синтаксисе Rust:
Размер каждой записи 8 байт (64 бита) и следующий формат:
| Бит(ы) | Название | Значение |
|---|---|---|
| 0 | present | страница в памяти |
| 1 | writable | разрешена запись |
| 2 | user accessible | если бит не установлен, то доступ к странице только у ядра |
| 3 | write through caching | запись напрямую в память |
| 4 | disable cache | отключить кэш для этой страницы |
| 5 | accessed | CPU устанавливает этот бит, когда страница используется |
| 6 | dirty | CPU устанавливает этот бит, когда происходит запись на страницу |
| 7 | huge page/null | нулевой бит в P1 и P4 создаёт страницы 1 КБ в P3, страницу 2 МБ в P2 |
| 8 | global | страница не заполняется из кэша при переключении адресного пространства (должен быть установлен бит PGE регистра CR4) |
| 9-11 | available | ОС может их свободно использовать |
| 12-51 | physical address | выровненный по странице 52-битный физический адрес фрейма или следующей таблицы страниц |
| 52-62 | available | ОС может их свободно использовать |
| 63 | no execute | запрещает выполнение кода на этой странице (должен быть установлен бит NXE в регистре EFER) |
Мы видим, что для хранения физического адреса фрейма используются только биты 12-51, а остальные работают как флаги или могут свободно использоваться операционной системой. Такое возможно, потому что мы всегда указываем или на выровненный по 4096 байтам адрес, или на выровненную страницу таблиц, или на начало соответствующего фрейма. Это означает, что биты 0-11 всегда равны нулю, так что их можно не хранить, они просто обнуляются на аппаратном уровне перед использованием адреса. То же самое относится и к битам 52-63, поскольку архитектура x86_64 поддерживает только 52-разрядные физические адреса (и только 48-разрядные виртуальные адреса).
Подробнее рассмотрим доступные флаги:
Буфер ассоциативной трансляции (TLB)
Из-за четырёх уровней для каждого преобразование адреса требуется четыре доступа к памяти. Ради повышения производительности x86_64 кэширует последние несколько переводов в так называемом буфере ассоциативной трансляции (TLB). Это позволяет пропустить преобразование, если оно ещё в кэше.
Важно не забывать чистить TLB после каждого изменения таблицы страниц, иначе CPU продолжит использовать старую трансляцию, что приведёт к непредсказуемым ошибкам, которые очень трудно отладить.
Реализация
Мы не упомянули одну вещь: наше ядро уже поддерживает страничную организацию. Загрузчик из статьи «Минимальное ядро на Rust» уже установил четырёхуровневую иерархию, которая сопоставляет каждую страницу нашего ядра с физическим фреймом, потому что страничная организация обязательна в 64-разрядном режиме на x86_64.
Благодаря страничной организации ядро уже относительно безопасно: каждый доступ за пределы допустимой памяти вызывает ошибку страницы, а не допускает запись в физическую память. Загрузчик даже установил правильные разрешения доступа для каждой страницы: исполняемыми будут только страницы с кодом, а доступны для записи только страницы с данными
Ошибки страницы (PageFault)
Попробуем вызвать PageFault, обратившись к памяти за пределами ядра. Во-первых, создаём обработчик ошибок и регистрируем его в нашем IDT, чтобы видеть специфическое исключение вместо двойной ошибки общего типа:
Теперь получаем доступ к памяти вне ядра:
После запуска мы видим, что происходит вызов обработчика ошибок страницы:
Если закомментить последнюю строку, то мы можем убедиться, что чтение работает, а запись вызывает ошибку PageFault.
Доступ к таблицам страниц
Теперь взглянем на таблицы страниц для ядра:
После запуска видим такой результат:
Level 4 page table at: PhysAddr(0x1000)
После запуска видим такой результат:
Вместо работы с небезопасными указателями напрямую можно использовать тип PageTable из x86_64 :
x86_64 также предоставляет некоторые абстракции для отдельных записей, чтобы сразу увидеть установленные флаги:
Следующий шаг — последовать по указателям в записи 0 или записи 1 к таблице страниц уровня 3. Но теперь у нас опять возникает проблема, что 0x2000 и 0x6e5000 представляют собой физические адреса, поэтому мы не можем получить к ним прямой доступ. Эта проблема будет решена в следующей статье.
Резюме
В статье представлены два метода защиты памяти: сегментация и страничная организация. Первый метод использует области памяти переменного размера и страдает от внешней фрагментации, второй использует страницы фиксированного размера и позволяет гораздо более детальный контроль над правами доступа.
Страничная организация хранит информацию о трансляции страниц в таблицах одного или нескольких уровней. Архитектура x86_64 использует четырёхуровневые таблицы с размером страницы 4 КБ. Оборудование автоматически обходит таблицы страниц и кэширует результаты преобразования в буфере ассоциативной трансляции (TLB). При изменении таблиц страниц его следует принудительно очищать.
Мы узнали, что наше ядро уже поддерживает страничную организацию, а при несанкционированном доступе к памяти выпадает PageFault. Мы попытались получить доступ к текущим активным таблицам страниц, но удалось получить доступ только к таблице четвёртого уровня, так как в таблицах страниц хранятся физические адреса, а мы не можем получить к ним доступ напрямую из ядра.


