Виртуальная память¶
Виртуальная память — это абстракция, которую операционная система предоставляет каждому запущенному процессу. Процесс работает с непрерывным виртуальным адресным пространством и не знает, где именно его данные находятся физически — в оперативной памяти, в файле на диске или в своп-разделе. Преобразование виртуальных адресов в физические выполняет аппаратный блок MMU (Memory Management Unit) при каждом обращении к памяти.
Зачем нужна виртуальная память¶
Без виртуальной памяти каждый процесс работал бы напрямую с физическими адресами RAM. Это порождает ряд проблем: ошибка в одном процессе повреждает память другого, невозможно одновременно запустить две копии одной программы по одному адресу, физическая память быстро заканчивается при запуске многих программ.
Виртуальная память решает все эти проблемы разом:
- Изоляция процессов. Каждый процесс видит только своё адресное пространство. Обращение к чужой памяти
невозможно без явного согласия ОС (разделяемая память,
mmap). - Удобство программирования. Каждый процесс считает, что занимает память с нулевого адреса, и не заботится о других процессах.
- Непрерывное пространство. Массив из миллиона элементов выглядит непрерывным, хотя физически его страницы могут быть разбросаны по RAM.
- Эффективное использование RAM. ОС выгружает редко используемые страницы в своп, загружает их обратно по первому
обращению, делит одну физическую страницу между несколькими процессами (разделяемые библиотеки,
fork+COW). - Защита памяти. Разным областям назначаются разные права: код (
r-x), данные (rw-), константы (r--). - ASLR. Рандомизация базовых адресов затрудняет атаки типа return-to-libc.
Типичная карта виртуального адресного пространства¶
На x86-64 пользовательскому процессу доступна «нижняя» половина 48-битного адресного пространства
(0x0000_0000_0000_0000 — 0x0000_7fff_ffff_ffff). Ядро занимает «верхнюю» половину
(0xffff_8000_0000_0000 — 0xffff_ffff_ffff_ffff).
Высокие адреса (0xffff…)
┌─────────────────────────────┐
│ Kernel space │ недоступно из user-space
├─────────────────────────────┤
│ Stack │ растёт вниз ↓
│ (локальные переменные, │
│ параметры функций) │
├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
│ │
│ Gap (свободное место) │
│ │
├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
│ Heap │ растёт вверх ↑
│ (malloc, new, mmap) │
├─────────────────────────────┤
│ .bss │ неинициализированные глобальные переменные (нули)
├─────────────────────────────┤
│ .data │ инициализированные глобальные переменные
├─────────────────────────────┤
│ .rodata │ строковые литералы, константы
├─────────────────────────────┤
│ .text │ машинный код (r-x)
├─────────────────────────────┤
│ ELF-заголовки │
Низкие адреса (0x0000…)
Карта памяти процесса: /proc/PID/maps¶
Файл /proc/<pid>/maps показывает все активные отображения виртуального адресного пространства процесса.
Пример вывода:
Address Perms Offset Dev Inode Pathname
55c3d3000000-55c3d302f000 r--p 00000000 08:10 123456 /usr/bin/a.out
55c3d302f000-55c3d304d000 r-xp 0002f000 08:10 123456 /usr/bin/a.out
55c3d304d000-55c3d3056000 r--p 0004d000 08:10 123456 /usr/bin/a.out
55c3d3057000-55c3d305a000 rw-p 00056000 08:10 123456 /usr/bin/a.out
55c3d305a000-55c3d307b000 rw-p 00000000 00:00 0 [heap]
7fe04e000000-7fe04e21b000 r-xp 00000000 08:10 234567 /lib/x86_64-linux-gnu/libc.so.6
7fe04e21b000-7fe04e3ab000 ---p 0021b000 08:10 234567 /lib/x86_64-linux-gnu/libc.so.6
7fe04e3ab000-7fe04e3af000 r--p 0021b000 08:10 234567 /lib/x86_64-linux-gnu/libc.so.6
7fe04e3af000-7fe04e3b1000 rw-p 0021f000 08:10 234567 /lib/x86_64-linux-gnu/libc.so.6
7fff0e5dd000-7fff0e5fe000 rw-p 00000000 00:00 0 [stack]
7fff0e5fe000-7fff0e601000 r--p 00000000 00:00 0 [vvar]
7fff0e601000-7fff0e603000 r-xp 00000000 00:00 0 [vdso]
Поле Perms задаёт права доступа:
| Символ | Значение |
|---|---|
r |
чтение |
w |
запись |
x |
исполнение |
p |
private — Copy-on-Write, изменения локальны |
s |
shared — изменения видны другим процессам |
Специальные области:
| Область | Описание |
|---|---|
[heap] |
Динамическая память, управляемая malloc; растёт вверх |
[stack] |
Стек главного потока; растёт вниз |
[vdso] |
Virtual Dynamic Shared Object — код ядра в user-space для быстрых syscall |
[vvar] |
Переменные ядра, доступные vdso (например, время) |
[vsyscall] |
Устаревший механизм (заменён vdso), фиксированный адрес |
Страничная организация памяти¶
Физическая и виртуальная память делятся на страницы фиксированного размера — обычно 4 КБ на x86-64. Каждая виртуальная страница независимо отображается на физическую страницу (или помечается как отсутствующая в памяти).
Фиксированный размер страницы упрощает управление: ОС работает не с отдельными байтами, а с блоками одного размера, что исключает фрагментацию на уровне управления адресным пространством.
Таблицы страниц¶
Отображение виртуальных адресов на физические хранится в таблицах страниц (page tables). На x86-64 используется четырёхуровневая иерархия:
Виртуальный адрес (64 бит, значимы 48):
Биты 47–39 → индекс в PML4 (Page Map Level 4, 512 записей)
Биты 38–30 → индекс в PDPT (Page Directory Pointer Table)
Биты 29–21 → индекс в PD (Page Directory)
Биты 20–12 → индекс в PT (Page Table)
Биты 11–0 → смещение внутри страницы (0–4095)
PML4[47:39] → PDPT[38:30] → PD[29:21] → PT[20:12] → физическая страница
Каждая таблица занимает ровно одну страницу (512 записей × 8 байт = 4 КБ). Вся иерархия для одного процесса хранится в оперативной памяти; корневой указатель на PML4 хранится в регистре CR3. При переключении контекста ядро записывает в CR3 указатель на таблицу нового процесса.
Запись в таблице страниц (Page Table Entry, PTE) — 64-битное значение:
Биты 51–12 Физический номер страницы (Page Frame Number)
Бит 6 D — Dirty (страница была изменена)
Бит 5 A — Accessed (к странице обращались)
Бит 2 U/S — User/Supervisor (1 = доступна user-space)
Бит 1 R/W — Read/Write (0 = только чтение, 1 = чтение+запись)
Бит 0 P — Present (1 = страница в памяти, 0 = отсутствует)
Статистику по виртуальной и физической памяти процесса можно посмотреть в /proc/<pid>/status:
cat /proc/self/status | grep -E "VmPeak|VmSize|VmRSS|VmHWM"
# VmPeak — пиковый размер виртуального адресного пространства
# VmSize — текущий размер виртуального адресного пространства
# VmRSS — текущее количество страниц в RAM (Resident Set Size)
# VmHWM — пиковый RSS
MMU и TLB¶
MMU (Memory Management Unit) — аппаратный блок процессора, который выполняет трансляцию виртуального адреса в физический при каждом обращении к памяти. Без дополнительного кэша каждая такая трансляция потребовала бы четырёх обращений к RAM (обход всей иерархии таблиц), что катастрофически медленно.
TLB (Translation Lookaside Buffer) — небольшой ассоциативный кэш внутри MMU, в котором хранятся последние трансляции «виртуальная страница → физическая страница». Благодаря принципу локальности большинство обращений к памяти попадает в TLB.
Алгоритм трансляции адреса¶
1. CPU выдаёт виртуальный адрес.
2. MMU проверяет TLB.
├─ TLB Hit → физический адрес получен (~1–4 нс, ~99% случаев).
└─ TLB Miss → переходим к Page Walk.
3. Page Walk: обход таблиц страниц в RAM.
PML4 → PDPT → PD → PT → физический адрес.
(~200 нс, четыре обращения к памяти)
4. Физический адрес добавляется в TLB (вытесняя старую запись).
5. Если страница отсутствует или нарушены права — генерируется Page Fault.
Современные процессоры имеют раздельные TLB для инструкций и данных:
- iTLB (Instruction TLB) — используется на этапе fetch конвейера.
- dTLB (Data TLB) — используется на этапах load/store.
Раздельность позволяет параллельно выбирать инструкцию и обращаться к данным.
TLB при переключении контекста¶
После переключения на другой процесс все записи TLB из предыдущего пространства невалидны. Наивное решение — полная
очистка TLB (TLB flush), что приводит к волне TLB-промахов сразу после переключения. Современные процессоры
поддерживают ASID (Address Space ID): каждая запись в TLB помечается идентификатором процесса, что позволяет
сохранять записи при переключении контекста.
Влияние локальности на TLB¶
Обходы структур с хорошей пространственной локальностью (массивы) порождают значительно меньше TLB-промахов, чем обходы разбросанных структур (связные списки):
#include <vector>
#include <list>
// Хорошая локальность: последовательный обход массива.
// Одна страница вмещает 1024 int — большинство обращений дают TLB Hit.
std::vector<int> vec(100'000);
for (int& x : vec) x++;
// Плохая локальность: узлы списка разбросаны по heap.
// Каждый узел может находиться на отдельной странице — много TLB Miss.
std::list<int> lst(100'000);
for (int& x : lst) x++;
Посмотреть статистику TLB можно через perf:
Page Fault¶
Page Fault — исключение процессора (вектор 14 на x86), возникающее при обращении к виртуальной странице, которую MMU не может транслировать напрямую: страница отсутствует в памяти или нарушены права доступа. Управление передаётся обработчику в ядре ОС, который решает, что делать.
Виды Page Fault¶
Minor (мягкий) Page Fault — страница уже в RAM, но в таблицах страниц данного процесса запись отсутствует или устарела. ОС просто обновляет PTE без каких-либо дисковых операций. Возникает, например:
- при первом обращении к странице после
fork()(COW); - при повторном использовании анонимной страницы, которую ядро уже подготовил;
- при разделяемых отображениях (
MAP_SHARED), которые уже загружены другим процессом.
Стоимость: единицы–десятки микросекунд.
Major (жёсткий) Page Fault — страница отсутствует в RAM и должна быть загружена с диска. Возникает при:
- первом обращении к страницам
mmap-файла; - обращении к странице, вытесненной в своп;
- загрузке нового исполняемого файла или разделяемой библиотеки.
Стоимость: единицы–десятки миллисекунд (на SSD) — примерно в тысячу раз дороже Minor Fault.
Segmentation Fault (SIGSEGV) — обращение к адресу, для которого у процесса нет разрешения:
- разыменование нулевого указателя;
- выход за границы стека;
- запись в страницу только для чтения;
- обращение к неотображённому адресу.
Обработка Page Fault в ядре¶
1. Исключение Page Fault, CR2 содержит виртуальный адрес.
2. ОС проверяет: является ли адрес валидным для данного процесса?
├─ Нет → SIGSEGV → процесс убит.
└─ Да → продолжаем.
3. ОС определяет тип fault:
├─ Страница в RAM (COW, shared) → Minor Fault → обновить PTE.
└─ Страница на диске (своп, файл) → Major Fault:
а. Найти свободный физический фрейм (возможно, вытеснить другую страницу).
б. Инициировать ввод-вывод.
в. Процесс переходит в состояние ожидания, CPU отдаётся другому процессу.
г. По завершении IO обновить PTE, разбудить процесс.
4. Процесс повторяет прерванную инструкцию — теперь успешно.
Copy-on-Write и Minor Fault¶
Системный вызов fork() создаёт дочерний процесс, не копируя физическую память: обе копии таблиц страниц
указывают на одни и те же физические страницы, помеченные как «только чтение». При первой же записи в такую
страницу возникает Minor Fault, ядро создаёт физическую копию страницы и перенаправляет PTE дочернего процесса
на неё. Родительский процесс продолжает видеть исходное значение.
#include <unistd.h>
#include <stdio.h>
int data = 42;
int main() {
pid_t pid = fork();
if (pid == 0) {
// Дочерний процесс: первая запись вызовет COW Minor Fault.
data = 100;
printf("child: data = %d\n", data); // 100
} else {
wait(NULL);
printf("parent: data = %d\n", data); // 42 — не изменилось
}
}
Просмотр статистики Page Fault¶
# В колонках MINFLT и MAJFLT — счётчики Minor и Major Fault для каждого процесса.
ps -eo pid,comm,minflt,majflt
# Подробная статистика для одного запуска:
/usr/bin/time -v ./program 2>&1 | grep -E "page fault"
# Major (requiring I/O) page faults: 3
# Minor (reclaiming a frame) page faults: 482
Системные вызовы для работы с виртуальной памятью¶
mmap и munmap¶
mmap создаёт новое отображение в адресном пространстве процесса — анонимное (память) или файловое.
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
Флаг flags |
Значение |
|---|---|
MAP_PRIVATE |
Copy-on-Write; изменения не видны другим процессам и не пишутся в файл |
MAP_SHARED |
Изменения видны всем, кто отобразил тот же файл |
MAP_ANONYMOUS |
Анонимное отображение (не связано с файлом); fd = -1 |
MAP_FIXED |
Ядро обязано разместить отображение по точному адресу addr |
// Анонимная память (аналог malloc для больших блоков)
char *buf = mmap(NULL, 4096,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
buf[0] = 'A';
munmap(buf, 4096);
// Разделяемое файловое отображение
int fd = open("data.bin", O_RDWR);
char *view = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
view[0] = 'X'; // запись видна другим процессам и попадает в файл
munmap(view, 4096);
close(fd);
mprotect¶
mprotect изменяет права доступа к уже существующему отображению (подробно рассматривается в теме «Защита памяти»).
char *mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
mem[0] = 'A';
mprotect(mem, 4096, PROT_READ); // теперь только чтение
// mem[0] = 'B'; // вызвало бы SIGSEGV
brk и sbrk¶
Традиционный интерфейс для управления концом сегмента heap (program break). Современный malloc использует их
только для небольших выделений; для крупных блоков (обычно > 128 КБ) применяется mmap.
#include <unistd.h>
int brk(void *addr); // установить program break
void *sbrk(intptr_t incr); // сдвинуть break на incr байт, вернуть старое значение
void *before = sbrk(0);
char *mem = sbrk(4096); // расширить heap на 4 КБ
void *after = sbrk(0);
// after - before == 4096
Прямой вызов sbrk в пользовательском коде не рекомендуется: это нарушает работу стандартного malloc.
mremap¶
Linux-специфичный вызов для изменения размера или перемещения существующего отображения без копирования данных.
#include <sys/mman.h>
void *mremap(void *old_address, size_t old_size,
size_t new_size, int flags, ...);
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// Расширить до 8 КБ; MREMAP_MAYMOVE разрешает переместить отображение.
ptr = mremap(ptr, 4096, 8192, MREMAP_MAYMOVE);
Связанные темы¶
- Устройство памяти и аллокация — детали сегментов
.text/.data/.bss/heap/stack - mmap и маппинг файлов — подробно о
mmap,MAP_SHARED/MAP_PRIVATE,msync - Защита памяти —
mprotect, ASLR, NX-бит, W^X - Реализация malloc и free — как
brkиmmapиспользуются внутри кучи glibc - Файловые дескрипторы — дескрипторы, передаваемые в файловый
mmap - Процессы: основы — адресное пространство создаётся при
fork/exec - Кэши процессора — TLB как специализированный кэш трансляции адресов
Источники¶
man 5 proc— формат/proc/PID/mapsи других файловman 2 mmap— создание отображений в памятиman 2 munmap— удаление отображенийman 2 mprotect— изменение прав доступа к страницамman 2 brk— управление program breakman 2 mremap— изменение размера отображенияman 2 madvise— подсказки ядру по управлению страницами- Intel® 64 and IA-32 Architectures Software Developer's Manual, Volume 3A — глава 4 (Paging)
- Linux kernel source: mm/memory.c — обработчик Page Fault