Виртуальная память: страничная организация адреса

Страничная организация виртуальной памяти - это способ, которым операционная система создаёт для каждого процесса иллюзию большого непрерывного адресного пространства, физически разбросанного по кускам оперативной памяти и диску. Процесс работает с виртуальными адресами, а реальное размещение данных по физическим ячейкам берёт на себя связка «таблица страниц плюс блок управления памятью». Разберём, как виртуальный адрес распадается на номер страницы и смещение, как таблица страниц переводит его в физический, зачем нужен буфер TLB и что происходит при page fault. Ниже есть калькулятор трансляции - впишите свой адрес и размер страницы, чтобы увидеть разбор по битам.
Зачем нужна виртуальная память
Без виртуальной памяти программа адресовала бы физические ячейки напрямую. Это порождает три проблемы: процессы могут затереть данные друг друга, программе нужен непрерывный блок памяти целиком, а суммарный объём всех процессов не может превысить размер ОЗУ. Виртуальная память снимает все три ограничения сразу.
Идея в том, чтобы ввести промежуточный слой адресации. Процесс оперирует виртуальными адресами из своего изолированного пространства (например, 0 .. ), а аппаратный блок управления памятью (MMU) на лету переводит каждый такой адрес в физический - реальный адрес в микросхемах ОЗУ. Перевод и называется трансляцией адреса.
Страничная организация - самый распространённый способ устроить эту трансляцию. И виртуальное, и физическое пространство нарезаются на блоки одинакового фиксированного размера: виртуальные блоки зовут страницами (pages), физические - фреймами или рамками (frames). Размер блока - степень двойки, классически 4 КБ ( байт).

Как виртуальный адрес делится на части
Ключевой приём страничной организации: виртуальный адрес делится на два поля чисто арифметически, без отдельного хранения границы.
- Смещение внутри страницы (offset) - младшие бит, где размер страницы равен байт. Для страницы 4 КБ это младшие 12 бит.
- Номер виртуальной страницы (VPN) - все старшие биты, оставшиеся после смещения.
Если виртуальный адрес занимает бит, а страница - байт, то под номер страницы отводится старших бит, под смещение - младших. Формально:
где - виртуальный адрес. Деление на и остаток от него - это просто сдвиг вправо на бит и взятие младших бит, поэтому MMU выполняет разбиение мгновенно, без арифметики деления.
Смысл такого деления в том, что смещение при трансляции не меняется: байт остаётся на той же позиции внутри блока, переезжает только сам блок. Поэтому переводить нужно лишь номер страницы - этим занимается таблица страниц. О том, как двоичная запись адреса соответствует этим полям, полезно держать в голове связь с позиционными системами счисления и кодированием: каждое поле адреса - это группа разрядов фиксированной длины.
Таблица страниц: отображение страниц во фреймы
Таблица страниц (page table) - структура в памяти, которая для каждого процесса задаёт отображение «номер виртуальной страницы → номер физического фрейма». Она индексируется по VPN: берём номер страницы как индекс, читаем соответствующую запись и получаем номер фрейма.
Физический адрес собирается обратно из двух частей:
то есть номер фрейма сдвигается влево на бит, и к нему дописывается неизменное смещение. Каждая запись таблицы (page table entry, PTE) хранит не только номер фрейма, но и служебные биты:
- бит присутствия (valid/present) - лежит ли страница сейчас в ОЗУ;
- биты прав доступа - чтение, запись, исполнение;
- бит обращения (accessed) и бит модификации (dirty) - для алгоритмов вытеснения;
- иногда бит «нельзя кэшировать» и биты пользователь/ядро.
Размер таблицы. Для 48-битного адреса и страниц 4 КБ номер страницы занимает 36 бит, то есть страниц $2^{36}$. Одноуровневая таблица из такого числа записей нереальна, поэтому используют многоуровневые (иерархические) таблицы - они хранят только реально используемые ветви.
Многоуровневые таблицы страниц
Плоская таблица на всё адресное пространство заняла бы сотни гигабайт, причём почти вся была бы пустой. Решение - иерархическая таблица: номер страницы сам разбивается на несколько полей, каждое из которых индексирует свой уровень.
В архитектуре x86-64 с 4 КБ страницами VPN из 36 бит делится на четыре поля по 9 бит. Каждое поле индексирует одну таблицу на 512 записей (). Трансляция идёт сверху вниз: старшие 9 бит выбирают запись в таблице верхнего уровня, та указывает на таблицу следующего уровня и так далее, пока последний уровень не даст номер фрейма.

Выигрыш в том, что таблицы нижних уровней создаются только для используемых участков адресного пространства. Если процесс задействует немного памяти, большинство ветвей дерева отсутствует, и накладные расходы малы. Платой становится несколько обращений к памяти на одну трансляцию - для четырёх уровней это четыре чтения, прежде чем мы доберёмся до данных.
TLB: кэш трансляций
Если бы каждый доступ к памяти требовал прохода по всем уровням таблицы, программа замедлилась бы в разы. Спасает TLB (Translation Lookaside Buffer) - небольшой ассоциативный кэш внутри MMU, который хранит последние использованные пары «номер страницы → номер фрейма».
Алгоритм трансляции с TLB:
- MMU берёт VPN из виртуального адреса и ищет его в TLB.
- Попадание (TLB hit) - фрейм берётся из кэша за один такт, проход по таблице не нужен.
- Промах (TLB miss) - MMU выполняет полный обход таблицы (page walk), находит фрейм, дописывает пару в TLB и продолжает.
TLB работает потому, что у программ высокая локальность ссылок: обращения группируются вокруг небольшого числа страниц. На практике процент попаданий превышает 99%, поэтому средняя стоимость трансляции близка к одному такту. Эффективное время доступа считают как взвешенное среднее:
где - доля попаданий в TLB. Тот же принцип взвешенного среднего лежит в основе оценки кэшей всех уровней процессора.
Page fault: страница не в памяти
Если бит присутствия в записи таблицы сброшен, нужной страницы в ОЗУ нет - она либо ещё не загружалась, либо вытеснена на диск. Обращение к ней вызывает page fault - аппаратное исключение, которое передаёт управление операционной системе.
Обработчик page fault действует так:
- Проверяет, легально ли обращение (адрес в пределах выделенной памяти, права доступа корректны). Если нет - процесс получает ошибку сегментации.
- Находит свободный фрейм; если свободных нет, выбирает жертву по алгоритму вытеснения (LRU, Clock, FIFO) и при необходимости записывает её dirty-страницу на диск.
- Загружает требуемую страницу с диска в выбранный фрейм.
- Обновляет запись таблицы (ставит бит присутствия и номер фрейма) и перезапускает прервавшуюся инструкцию.
Не путайте page fault с ошибкой сегментации. Page fault - штатное событие механизма подкачки: страница есть, но временно на диске. Segmentation fault - обращение к невыделенному или защищённому адресу, и оно завершается аварийно.
Слишком частые page fault приводят к трешингу (thrashing): система тратит почти всё время на подкачку страниц вместо полезной работы. Это происходит, когда совокупному набору активных страниц процессов не хватает физической памяти.
Частые ошибки
- Путают страницу и фрейм. Страница - блок виртуального пространства, фрейм - блок физического. Они равны по размеру, и трансляция меняет номер блока, но не размер.
- Считают, что смещение тоже переводится. Смещение переходит из виртуального адреса в физический без изменений - переводится только номер страницы.
- Берут неверное число бит под смещение. Под смещение идёт ровно от размера страницы: для 4 КБ это 12 бит, для 8 КБ - 13, а не «округлённое» значение.
- Смешивают page fault и segmentation fault. Первое - нормальная подкачка, второе - недопустимое обращение.
- Забывают про TLB при оценке скорости. Без учёта высокого процента попаданий в TLB кажется, что многоуровневая таблица катастрофически медленна, хотя на деле почти все трансляции идут из кэша.
FAQ
Чем страничная организация отличается от сегментной? При сегментной памяти пространство делится на логические блоки переменной длины (код, стек, данные), и адрес состоит из номера сегмента и смещения внутри него. Страничная использует блоки фиксированного размера, что исключает внешнюю фрагментацию. Современные системы чаще комбинируют: сегментация поверх страничной (сегментно-страничная организация).
Почему размер страницы - всегда степень двойки? Чтобы деление адреса на номер страницы и смещение сводилось к битовым операциям. При размере смещение - это младшие бит, а номер страницы - старшие; MMU выделяет их сдвигом, без медленного деления. Произвольный размер потребовал бы настоящего деления на каждом доступе.
Что хранит запись таблицы страниц кроме номера фрейма? Биты присутствия, прав доступа (чтение/запись/исполнение), бит обращения и бит модификации (для алгоритмов вытеснения), а также флаги кэшируемости и уровня привилегий. Сам номер фрейма занимает старшие биты записи, служебные биты - младшие.
Коротко
Страничная организация виртуальной памяти разбивает виртуальный адрес на номер страницы (старшие биты) и смещение (младшие бит при размере страницы ). Таблица страниц отображает номер страницы во фрейм, смещение переходит без изменений, а физический адрес собирается как . Многоуровневые таблицы экономят память, TLB кэширует трансляции и держит среднюю стоимость близкой к одному такту, а page fault штатно подгружает отсутствующую страницу с диска.
Читайте также

Абстрактный класс и интерфейс: в чём отличие
Абстрактный класс и интерфейс: чем отличаются в ООП, когда наследовать поведение, а когда задавать контракт, как выбрать на примерах Java, C# и Python.

Алгоритм AdaBoost: как слабые классификаторы дают сильный
Алгоритм AdaBoost простыми словами: адаптивный бустинг, перевзвешивание объектов, формула веса классификатора, итоговый ансамбль и разбор шага на примере с формулами.

Алгоритм CatBoost: бустинг с обработкой категорий
Алгоритм CatBoost простыми словами: упорядоченный бустинг против сдвига прогноза, кодирование категориальных признаков через ordered target statistics, симметричные деревья и разбор типовых задач.