Виртуальная память и трансляция адресов
Урок объясняет виртуальную память — механизм, дающий каждому процессу иллюзию собственного непрерывного адресного пространства.
Виртуальная память — абстракция, при которой программа работает с виртуальными адресами, а процессор и ОС переводят их в физические адреса ОЗУ. Страница — блок памяти фиксированного размера (обычно 4 КБ), единица трансляции.
Зачем виртуальная память
Без неё программы видели бы реальную физическую память напрямую — и мешали бы друг другу: одна могла бы затереть данные другой. Виртуальная память решает сразу несколько задач: изоляция (у каждого процесса своё адресное пространство), защита (нельзя залезть в чужую память), иллюзия большой памяти (можно использовать больше ОЗУ за счёт диска) и удобство (программа всегда «начинается с адреса 0»).
Страницы и трансляция
Виртуальное и физическое пространство бьются на страницы одинакового размера (4 КБ). Таблица страниц хранит, какой виртуальной странице соответствует какая физическая (фрейм). Виртуальный адрес = номер страницы + смещение внутри неё; транслируется только номер страницы, смещение остаётся.
виртуальный адрес:
┌────────────────┬──────────────┐
│ номер вирт.стр.│ смещение │
└───────┬────────┴──────────────┘
│ таблица страниц
▼ (вирт.стр -> физ.фрейм)
┌────────────────┬──────────────┐
│ номер физ.фрейма│ смещение(то же)│
└────────────────┴──────────────┘
физический адрес
Как работает под капотом: трансляция
Промоделируем трансляцию через таблицу страниц. Размер страницы 4096 байт: смещение — это addr % 4096, номер страницы — addr // 4096:
PAGE_SIZE = 4096
# таблица страниц: вирт. страница -> физ. фрейм
page_table = {0: 5, 1: 2, 2: 9}
def translate(vaddr):
vpage = vaddr // PAGE_SIZE
offset = vaddr % PAGE_SIZE
if vpage not in page_table:
return None # страница не отображена -> page fault
frame = page_table[vpage]
paddr = frame * PAGE_SIZE + offset
return paddr
for vaddr in (100, 4096 + 50, 2*4096 + 7, 99999):
paddr = translate(vaddr)
if paddr is None:
print(f"вирт {vaddr:>6} -> PAGE FAULT (нет в таблице)")
else:
print(f"вирт {vaddr:>6} -> физ {paddr}")Вывод:
вирт 100 -> физ 20580 вирт 4146 -> физ 8242 вирт 8199 -> физ 36871 вирт 99999 -> PAGE FAULT (нет в таблице)
TLB: кэш трансляций
Проблема: каждое обращение к памяти требует сначала прочитать таблицу страниц — то есть ещё одно обращение к памяти. Удвоение! Решение — TLB (Translation Lookaside Buffer): маленький быстрый кэш недавних трансляций «вирт.страница → физ.фрейм». Большинство обращений попадают в TLB, и трансляция почти бесплатна.
tlb = {} # кэш трансляций
page_table = {0: 5, 1: 2}
tlb_hits = tlb_misses = 0
def translate_with_tlb(vpage):
global tlb_hits, tlb_misses
if vpage in tlb:
tlb_hits += 1
return tlb[vpage] # быстро
tlb_misses += 1
frame = page_table[vpage] # медленно: лезем в таблицу страниц
tlb[vpage] = frame # запоминаем
return frame
for vp in [0, 0, 1, 0, 1, 1]:
translate_with_tlb(vp)
print(f"TLB попаданий {tlb_hits}, промахов {tlb_misses}")Вывод:
TLB попаданий 4, промахов 2
Связь с ОС и page fault
Если страницы нет в ОЗУ (выгружена на диск или ещё не выделена), происходит page fault — процессор передаёт управление операционной системе. ОС подгружает страницу с диска в ОЗУ, обновляет таблицу страниц и возобновляет программу. Так железо (MMU, TLB) и ОС работают в паре: железо быстро транслирует, ОС управляет тем, что в памяти.
Глубже в тему
Виртуальная память — одно из самых элегантных изобретений в архитектуре, потому что одним механизмом решает сразу четыре разные задачи. Изоляция: у каждого процесса своё адресное пространство, и адрес 0x1000 в одной программе физически не тот же, что в другой, — программы не могут случайно или злонамеренно затереть данные друг друга. Защита: биты прав в таблице страниц (чтение/запись/исполнение) позволяют, например, запретить исполнение кода из области данных, ломая целый класс атак. Иллюзия большой памяти: суммарный объём виртуальных пространств всех процессов может превышать физическую ОЗУ, а излишек хранится на диске (swap). Удобство для компилятора и загрузчика: каждая программа считает, что начинается с фиксированного адреса, и не должна знать, куда её разместит ОС физически. Один уровень абстракции — четыре выгоды; это образец хорошего инженерного решения.
Ключ к пониманию трансляции — осознать, что переводится только номер страницы, а смещение внутри страницы остаётся неизменным. Виртуальный адрес делится на старшую часть (номер виртуальной страницы) и младшую (смещение). Поскольку страница и физический фрейм имеют одинаковый размер (обычно 4 КБ), позиция байта внутри них совпадает — значит, младшие 12 бит адреса (log2 от 4096) проходят насквозь без изменений. Таблица страниц переводит только номер виртуальной страницы в номер физического фрейма. Это сильно упрощает аппаратуру: трансляция касается лишь старших бит, а конкатенация фрейма со смещением даёт физический адрес. Расчёт из урока с PAGE_SIZE = 4096 наглядно показывает эту арифметику: смещение — это остаток от деления на размер страницы, номер страницы — частное.
TLB решает проблему, которая иначе свела бы на нет весь выигрыш: ведь сама таблица страниц лежит в памяти, и наивная трансляция требовала бы лишнего обращения к памяти перед каждым настоящим обращением — фактически удвоение всех походов в память. TLB (Translation Lookaside Buffer) — это маленький, очень быстрый, полностью или высоко ассоциативный кэш недавних трансляций «виртуальная страница → физический фрейм». Благодаря локальности (программа долго работает в пределах нескольких страниц) попадание в TLB достигает 99% и выше, и трансляция становится почти бесплатной. Промах TLB дорог: приходится «прогуливаться» по таблице страниц (page walk), которая в реальных 64-битных системах многоуровневая (чтобы не хранить гигантскую плоскую таблицу для разреженного адресного пространства). Поэтому TLB-промахи — заметная статья потерь на программах с большим, разбросанным рабочим набором, и существуют «большие страницы» (huge pages, 2 МБ или 1 ГБ), снижающие давление на TLB.
Page fault — это пример красивого разделения труда между быстрым «глупым» железом и медленной «умной» ОС. Аппаратура (MMU) умеет лишь быстро транслировать адреса по готовой таблице; когда нужной страницы в памяти нет (она выгружена на диск, ещё не выделена или это первое обращение), MMU не знает, что делать, и поднимает исключение — page fault, передавая управление операционной системе. ОС разбирается: если страница законна, но на диске — подгружает её в свободный фрейм, обновляет таблицу страниц и возобновляет прерванную команду, как будто ничего не было; если же обращение незаконно (выход за пределы выделенной памяти), ОС шлёт процессу сигнал ошибки (тот самый segmentation fault). Эта пара «быстрое железо плюс гибкая ОС» — фундаментальный паттерн всей системной архитектуры: критичный к скорости общий случай делает аппаратура, а редкие сложные ситуации обрабатывает программное обеспечение. Тот же принцип мы видели в предсказании переходов и в обработке прерываний — это сквозная идея всего курса.
Частые ошибки
- Думать, что виртуальный адрес = физический. Они разные; перевод делает аппаратный MMU по таблице страниц.
- Забывать про TLB. Без него каждая трансляция стоила бы лишнего обращения к памяти.
- Путать page fault с ошибкой. Это штатное событие: ОС обрабатывает его и подгружает страницу (хотя обращение к чужой памяти — это уже ошибка защиты).
Итог
- Виртуальная память даёт каждому процессу изолированное адресное пространство.
- Адреса транслируются по страницам через таблицу страниц; смещение не меняется.
- TLB кэширует трансляции, а page fault передаёт управление ОС для подгрузки страницы.