Управление памятью в ОС: виртуальная память, страницы и сегменты🔗
Архитектурный контекст: зачем нужна виртуальная память🔗
Виртуальная память — это механизм абстракции, создающий для каждого процесса иллюзию владения монопольным и непрерывным адресным пространством. Физически данные при этом могут быть разбросаны по разным участкам RAM или вовсе находиться на диске.
Допустим, процесс — это повар, который готовит блюдо. В его распоряжении есть «бесконечная воображаемая поваренная книга», где рецепты всегда начинаются с первой страницы. На деле же «кухонный стол» (RAM) забит ингредиентами других кулинаров, и свободное место на нем сильно фрагментировано. Виртуализация позволяет мастеру не заботиться о том, в каком углу лежит его соль — для него она всегда находится в первой строке личного блокнота.
Иерархия взаимодействия выглядит так:
Диаграмма загружается…
Ключевым звеном в этой цепочке выступает Memory Management Unit (MMU) — аппаратный блок в составе CPU. Он на лету преобразует виртуальные адреса. С которыми работает код программы, в физические адреса в памяти. Без MMU операционной системе приходилось бы вручную пересчитывать каждый указатель при запуске приложения. Это сделало бы эффективную многозадачность невозможной.
| Характеристика |
Физическая адресация (RAM) |
Виртуальная адресация (Process) |
| Структура |
Фрагментированная, разреженная |
Линейная, непрерывная |
| Изоляция |
Отсутствует (доступ ко всей шине) |
Полная (процесс заперт в своем пространстве) |
| Объем |
Ограничен объемом модулей памяти |
Ограничен разрядностью архитектуры |
| Управление |
Контроллер памяти |
ОС + MMU |
С точки зрения архитектуры такое решение дает два преимущества. Безопасность: процесс не может повредить данные соседа, так как в его карте адресов просто нет чужих ссылок. Упрощение разработки: компилятору не нужно знать объем памяти в системе, он собирает бинарный файл так, будто всё пространство принадлежит только ему.
С другой стороны, Как вы думаете, насколько сильно замедляется работа системы из-за постоянной трансляции адресов через MMU?
Механика страничной организации: страницы и фреймы🔗
Страничная организация делит виртуальное адресное пространство на блоки фиксированного размера (страницы), а физическую память — на такие же участки (фреймы). Это позволяет операционной системе размещать логически связные данные в разрозненных ячейках RAM, полностью решая проблему внешней фрагментации.
Тем не менее, В современных архитектурах стандартный размер страницы составляет 4 КБ. При обращении к памяти процессор использует виртуальный адрес, который аппаратно разделяется на номер виртуальной страницы (VPN) и смещение (offset). Смещение указывает на конкретный байт внутри блока и остается неизменным при любых преобразованиях.
Трансляция адреса и структура Page Table Entry (PTE)🔗
Преобразованием адресов занимается устройство MMU (Memory Management Unit). В качестве справочника оно использует Таблицу страниц (Page Table) — структуру данных в оперативной памяти, выполняющую роль навигатора.
Каждая запись в этой таблице (PTE) хранит номер физического фрейма (PFN) и набор управляющих битов:
- Present/Absent: сигнализирует, загружена ли страница в RAM или вытеснена на диск.
- Read/Write: определяет права на изменение данных.
- User/Supervisor: ограничивает доступ в зависимости от привилегий процесса.
- Dirty: отмечает, менялось ли содержимое страницы (необходимо для оптимизации записи на диск).
Итоговый физический адрес вычисляется так:
Physical_Address=(PFN×Page_Size)+Offset
При этом Для 64-битных систем линейная таблица страниц занимала бы терабайты. Чтобы не тратить ресурсы впустую, применяются многоуровневые таблицы страниц. Они строятся по принципу дерева: верхние уровни ссылаются на нижние. Если какой-то диапазон адресов не используется, целые ветки этой структуры просто не создаются, что экономит мегабайты памяти.
Ускорение трансляции: TLB (Translation Lookaside Buffer)🔗
Прямое обращение к таблицам в RAM замедляет работу системы, так как на каждый запрос данных требуется дополнительный цикл поиска. Эту задержку нивелирует TLB — сверхбыстрый ассоциативный кэш внутри процессора, где хранятся последние успешные трансляции.
Диаграмма загружается…
Кроме того, При переключении контекста (Context Switch) накопленные в TLB данные становятся неактуальными, так как у нового процесса своя карта адресов. Если архитектура не поддерживает специальные идентификаторы (ASID), кэш приходится полностью сбрасывать. Это одна из причин, почему чрезмерно частое переключение между задачами снижает производительность: процессору нужно время, чтобы снова наполнить TLB.
Контроль доступа на уровне железа🔗
На базе записей PTE строится фундаментальная безопасность системы. Если код попытается записать данные в страницу. Помеченную битом Read-Only, железо мгновенно вызовет исключение (Segmentation Fault). Таблица страниц здесь выступает не только механизмом адресации, но и жестким барьером, обеспечивающим изоляцию ресурсов. Понимаете ли вы теперь, почему процесс не может просто так «заглянуть» в память соседа?
Удалось ли архитекторам найти баланс между гибкостью страниц и жесткой структурой кода и данных? Ответ кроется в сегментации, которую мы изучим позже.
Сегментация памяти: логическое разделение данных и защита🔗
Сегментация памяти — это способ организации адресного пространства, при котором память процесса разбивается на логические блоки переменной длины. Эти блоки, называемые сегментами, соответствуют реальной структуре программы: коду, стеку, куче или глобальным переменным.
В результате В отличие от страниц, которые «нарезают» адресное пространство на одинаковые куски механически, сегментация учитывает смысл данных. Компилятор или разработчик определяет, что функции проекта хранятся в сегменте CODE, а локальные переменные — в STACK. Такая избирательность позволяет гибко настраивать права доступа. Допустим, сегменту кода назначаются атрибуты RX (чтение и исполнение), что запрещает случайную или намеренную перезапись инструкций в процессе работы.
Структура сегментного адреса🔗
В этой системе логический адрес состоит из двух компонентов: селектора сегмента и смещения (offset). Селектор служит указателем на запись в специальной таблице дескрипторов, где хранятся базовый физический адрес и лимит (размер) конкретного сегмента.
Вычисление итогового адреса (на примере архитектуры x86) выглядит так:
Address=Segmentbase+Offset,где 0≤Offset<Segmentlimit
Если программа попытается затребовать данные по адресу, выходящему за рамки Segmentlimit, процессор мгновенно выдаст ошибку Segmentation Fault.
Сравнение подходов🔗
| Характеристика |
Страничная организация |
Сегментация |
| Размер блока |
Фиксированный (к примеру, 4 КБ) |
Переменный (зависит от логики) |
| Логическое деление |
Отсутствует (прозрачно для ПО) |
Выражено (код, данные, стек) |
| Фрагментация |
Внутренняя (пустоты внутри страниц) |
Внешняя (разрывы между сегментами в RAM) |
| Защита |
На уровне страниц |
На уровне логических объектов |
Гибридная модель🔗
Современные системы вроде Linux или Windows на архитектуре x86-64 редко используют сегментацию в чистом виде. Чаще применяется смешанная схема: виртуальное пространство сначала делится на логические сегменты, а затем каждый из них разбивается на стандартные страницы.
Диаграмма загружается…
Подобный подход совмещает сильные стороны обеих технологий: удобную изоляцию данных через сегменты и экономное использование физической памяти без потери места при внешней фрагментации.
Важно: Как вы считаете, почему разделение на код и данные на аппаратном уровне стало стандартом безопасности, а не осталось просто удобной фичей для разработчиков компиляторов?_
Практика: взгляд из кода через системные вызовы mmap и sbrk🔗
Системные вызовы mmap и brk/sbrk — это интерфейсы, через которые процесс запрашивает у ядра изменение границ адресного пространства. С их помощью операционная система выделяет новые страницы памяти или меняет права доступа к уже существующим.
Манипуляция кучей: sbrk и brk🔗
Исторически куча представляет собой сегмент данных, растущий вверх. Вызов brk устанавливает границу этого участка на конкретный адрес, а sbrk увеличивает его на заданное количество байт.
// Увеличение кучи на 4 КБ
void *current_break = sbrk(4096);
if (current_break == (void *)-1) {
perror("sbrk failed");
}
Простота sbrk порождает проблему фрагментации: память невозможно вернуть ядру из середины сегмента, только «откусить» её с края. Из-за этого ограничения современные аллокаторы используют другой инструмент для работы с крупными блоками.
Гибкость отображения: mmap🔗
mmap (memory map) — универсальный механизм создания отображений. Он связывает адресное пространство процесса с файлами. Создает «анонимные» участки, не привязанные к диску. Это база для работы разделяемой памяти и загрузки динамических библиотек. При вызове задаются права доступа (Protection Flags):
| Флаг |
Функциональность |
PROT_READ |
Разрешено только чтение. Попытка записи вызовет Segmentation Fault. |
PROT_WRITE |
Позволяет записывать данные в область памяти. |
PROT_EXEC |
Разрешает исполнение кода (защита NX/DEP). |
PROT_NONE |
Любой доступ запрещен. Используется для «защитных полос» (guard pages). |
Состояние памяти в Python🔗
Работу этих низкоуровневых механизмов легко отследить даже в Python. Обратите внимание, как меняется потребление ресурсов при обращении к выделенному участку:
import mmap
import os
import psutil
# Создаем анонимное отображение на 10 МБ
size = 10 * 1024 * 1024
shared_mem = mmap.mmap(-1, size, flags=mmap.MAP_PRIVATE | mmap.mmap.PROT_WRITE)
process = psutil.Process(os.getpid())
print(f"RSS (физическая память): {process.memory_info().rss / 1024:.2f} KB")
# Заполняем память данными
shared_mem.write(b'\xff' * size)
print(f"RSS после записи: {process.memory_info().rss / 1024:.2f} KB")
Механизм ленивого выделения: Page Fault🔗
При вызове mmap ядро не выделяет физическую память мгновенно. Оно лишь фиксирует в своих таблицах, что диапазон адресов теперь легитимен. Настоящая работа начинается только при первом обращении к данным.
Диаграмма загружается…
Событие, когда программа обращается к обещанному, но еще не привязанному к физическому фрейму адресу, называется Page Fault (ошибка страницы). ОС перехватывает аппаратное прерывание, находит место в RAM, обновляет таблицы и возвращает управление. Так реализуется «ленивая» стратегия: зачем тратить физические ресурсы на то, что может никогда не понадобиться?
При этом Попробуйте предположить, сколько таких микропауз происходит незаметно для пользователя при запуске обычного браузера.
Итоги и преимущества виртуализации памяти🔗
Виртуализация памяти — это механизм ОС, отделяющий логические адреса от физических ячеек RAM для изоляции процессов и гибкого распределения ресурсов. Она создает слой абстракции, позволяющий системе эффективно управлять выполнением программ, не ограничиваясь объемом установленных планок памяти.
Также Для системного ПО этот подход дает три критических преимущества:
Тем не менее, 1. Безопасность. Благодаря таблицам страниц процесс технически не может вмешаться в чужую память. Ошибка доступа к данным (SIGSEGV) одного потока на виртуальной кухне не испортит блюда коллег и не приведет к падению всего «ресторана» — ядра системы.
2. Упрощение линковки. Компилятору не нужно знать объем оперативной памяти конечного пользователя. Все исполняемые файлы могут начинаться с одного стандартного адреса, а ОС сама распределит их по свободным физическим фреймам.
3. Перерасход (Overcommit). За счет тактик ленивого выделения страниц и подкачки данных с диска, система способна запустить приложения, чей суммарный запрос на ресурсы в разы превышает емкость RAM.
Ключевые понятия темы🔗
Основные термины, описывающие механику работы подсистемы:
| Термин |
Краткое определение |
| Страница / Фрейм |
Минимальная единица деления виртуальной и физической памяти. |
| MMU |
Аппаратный контроллер процессора для быстрой трансляции адресов. |
| TLB |
Специализированный кэш, хранящий результаты последних преобразований. |
| Page Fault |
Исключение, возникающее при обращении к отсутствующей в RAM странице. |
Системные вызовы mmap и sbrk позволяют динамически расширять границы адресного пространства процесса. Однако ресурсы «железа» всегда конечны. По каким критериям операционная система решает, чьи данные пора выселить из быстрой памяти, когда свободные фреймы заканчиваются? Эффективность системы во многом зависит от выбора алгоритмов вытеснения страниц.