Операционные системы
Управление памятью
Каждый вызов `malloc()`, каждый запуск программы, каждое открытие файла - OS распределяет память между тысячами процессов. Но как компьютер с 16 ГБ RAM запускает программы, требующие в сумме 50 ГБ? Почему процесс не может просто обратиться к любому адресу памяти? Почему игра вылетает с 'out of memory', когда в диспетчере задач показано 8 ГБ свободной RAM?
- **Почему Chrome занимает 2 ГБ памяти?** Каждая вкладка - отдельный процесс с виртуальным адресным пространством. OS создаёт иллюзию, что у каждого процесса 2⁶⁴ байт памяти, хотя физической RAM всего 16 ГБ.
- **Как Redis держит 100 ГБ данных в памяти?** Виртуальная память + swapping. Часто используемые данные в RAM (hot cache), редкие - на SSD. MMU и OS делают это прозрачно для приложения.
- **Почему игры требуют 'непрерывный блок памяти'?** Старые движки используют contiguous allocation для текстур. Современные (Unreal, Unity) используют paging-based allocators, устраняя проблему фрагментации.
Цели урока
- Объяснить иерархию: registers → L1/L2/L3 → RAM → SSD → HDD по скорости и цене за байт
- Различать address binding на compile, load и execution time
- Сравнить contiguous allocation, paging, segmentation и их trade-offs
- Знать internal vs external fragmentation и стратегии compaction
- Понимать роль MMU: трансляция логического адреса в физический
Иерархия памяти
**Память компьютера** организована как иерархия уровней, каждый из которых отличается скоростью доступа, объёмом и стоимостью. Понимание этой иерархии - ключ к написанию эффективного кода.
**Почему иерархия важна?** Доступ к регистру занимает **0.3 наносекунды**. Доступ к RAM - **100 наносекунд** (в 300 раз медленнее!). Доступ к диску - **10 миллисекунд** (в 33 миллиона раз медленнее регистра). Если бы обращение к регистру длилось 1 секунду, то чтение с диска заняло бы **1 год**.
Латентность в человеческом масштабе
Если масштабировать время доступа к памяти на человеческую шкалу: • **L1 Cache** - 1 секунда (взять чашку рядом с собой) • **RAM** - 2 минуты (дойти до кухни за чаем) • **SSD** - 1 день (съездить в другой город) • **HDD** - 1 год (кругосветное путешествие) Вот почему программы кешируют данные в быстрой памяти!
**Принцип локальности** - основа эффективного использования иерархии памяти: • **Temporal locality (временная):** Если данные использовались недавно, вероятно, будут использованы снова • **Spatial locality (пространственная):** Если данные по адресу X использовались, вероятно, скоро понадобятся данные рядом (X+1, X+2)
**Cache miss vs Cache hit:** • **Cache hit** - данные найдены в кеше (~1-10 нс) • **Cache miss** - нужно идти в RAM (~100 нс) или на диск (мкс-мс) Программа с 90% cache hit в 10 раз быстрее программы с 50% cache hit!
Почему массивы быстрее списков
Массив хранит элементы **последовательно** в памяти: `[1][2][3][4]`. Когда CPU загружает элемент `[0]`, он автоматически подгружает в кеш `[1][2][3]` (spatial locality). Обход массива - сплошные cache hits. Список хранит элементы **вразброс** по памяти через указатели. Каждый переход к следующему элементу - потенциальный cache miss. Обход списка может быть в 10-100 раз медленнее массива того же размера!
**Задача OS:** Управлять RAM (main memory), предоставляя каждому процессу изолированное адресное пространство и перемещая редко используемые данные на диск (swapping).
Процессор читает 1 МБ данных. Где будет самый быстрый доступ?
Привязка адресов
**Адресная привязка (address binding)** - процесс сопоставления логических адресов программы с физическими адресами в RAM. Программа оперирует виртуальными адресами, OS переводит их в физические.
**Три момента привязки адресов:** 1. **Compile time** - адреса жёстко прописаны в коде (MS-DOS .COM файлы). Программа может загрузиться только по фиксированному адресу 2. **Load time** - адреса определяются при загрузке в память. Требует перемещаемый код (relocatable code) 3. **Execution time** - адреса переводятся "на лету" при выполнении. Требует аппаратную поддержку (MMU)
Современные OS используют **execution time binding** через MMU (Memory Management Unit) - специальный чип в процессоре, который транслирует виртуальные адреса в физические.
**Логическое vs физическое адресное пространство:** • **Логическое (виртуальное)** - адреса, которые видит программа. На 64-битной системе от 0 до 2⁶⁴ (16 exabytes!) • **Физическое** - реальные адреса в RAM. При 16 ГБ RAM физических адресов максимум 16 ГБ
Виртуальная память как P.O. Box
Аналогия: почтовый ящик (P.O. Box). Адрес получателя - **P.O. Box 123** (виртуальный адрес). Почтальон знает, где физически находится этот ящик на складе - **Shelf A, Row 5** (физический адрес). Для внешнего мира адрес постоянный (**P.O. Box 123**), но физически ящик может переместиться на другую полку - для отправителя это незаметно. Так же работает виртуальная память: программа использует постоянный виртуальный адрес, а OS может перемещать данные по физической памяти.
**Релокация (relocation)** - изменение адресов при загрузке программы. Простейший механизм: **base register + offset**.
Почему браузер не крашится при открытии 100 вкладок
Каждая вкладка Chrome - отдельный процесс с виртуальным адресным пространством 0 - 2⁶⁴. Все процессы используют адреса типа `0x7fff...`, но благодаря MMU они указывают на **разные** физические области RAM. Один процесс физически не может записать в память другого - MMU просто не переведёт виртуальный адрес в чужую физическую область.
Что произойдёт, если программа попытается обратиться к виртуальному адресу, для которого нет отображения в физическую память?
Непрерывное размещение
**Непрерывное размещение (contiguous allocation)** - каждый процесс занимает **единый непрерывный блок** физической памяти. Самый простой метод, использовался в ранних OS.
**Задача размещения:** Когда приходит новый процесс размером N, OS должна найти свободный блок >= N. Существует несколько стратегий:
**Алгоритмы размещения:** • **First Fit** - взять первый подходящий блок (быстро, но может оставить маленькие дырки) • **Best Fit** - взять наименьший достаточный блок (экономит место, но медленный поиск) • **Worst Fit** - взять наибольший блок (надежда что останется полезная дырка, редко применяется) • **Next Fit** - как First Fit, но продолжать с места последней аллокации
malloc() под капотом
При вызове `malloc(1024)` библиотека C делает примерно следующее: 1. Проверяет список свободных блоков (free list) 2. Ищет блок >= 1024 байт алгоритмом First/Best Fit 3. Если нашла - помечает занятым, возвращает адрес 4. Если не нашла - просит OS выделить больше памяти через `sbrk()` или `mmap()` Вызов `free(ptr)` помечает блок свободным и добавляет обратно в free list.
**Защита памяти в contiguous allocation:** Используются два регистра - **base** и **limit**. MMU проверяет каждый адрес:
Почему игры вылетают с 'out of memory'
В системе может быть 2 ГБ свободной RAM, но разбитой на маленькие блоки (8 МБ, 16 МБ, 32 МБ). Игра запрашивает **непрерывный** блок 512 МБ для текстур. Contiguous allocation не может выполнить запрос - нет единого блока 512 МБ, хотя в сумме свободных 2 ГБ. Результат: **out of memory** error. Современные OS решают это через **paging** (нефизически непрерывное размещение).
**Проблемы contiguous allocation:** • Фрагментация памяти (следующая концепция) • Невозможность роста процесса (нужно переместить весь блок) • Неэффективное использование RAM
В памяти есть свободные блоки: 10 КБ, 40 КБ, 25 КБ, 50 КБ (в таком порядке). Процесс запрашивает 30 КБ. Какой блок выберет алгоритм Best Fit?
Фрагментация памяти
**Фрагментация (fragmentation)** - главная проблема управления памятью. Память есть, но она разбита на маленькие куски и не может быть использована эффективно.
**Два типа фрагментации:** • **Внешняя фрагментация (external)** - достаточно свободной памяти, но она разбита на маленькие несмежные блоки • **Внутренняя фрагментация (internal)** - процессу выделено больше памяти, чем нужно, остаток тратится впустую
**Решения внешней фрагментации:** 1. **Compaction (уплотнение)** - сдвинуть все процессы вместе, объединив свободную память в один блок. Проблема: очень дорого (нужно переписать много данных и обновить адреса) 2. **Paging (страничная организация)** - разделить память на маленькие фиксированные блоки (страницы). Процесс может занимать несмежные страницы. Решает внешнюю фрагментацию!
Внешняя фрагментация в реальной жизни
Аналогия: парковка на 100 мест. Машины приезжают и уезжают в течение дня. К вечеру свободно 30 мест, но они разбросаны: тут 2 места, там 3, там 5. Приезжает автобус (нужно 10 мест подряд) - **не может припарковаться**, хотя свободных мест 30! Это внешняя фрагментация. **Compaction** - попросить всех пересесть ближе к входу (дорого!). **Paging** - разрешить автобусу занять 10 мест вразброс (требует "виртуальную парковку").
**Внутренняя фрагментация (internal fragmentation):** Возникает когда память выделяется **фиксированными блоками** (например, страницами по 4 КБ). Процесс запрашивает 3.5 КБ, получает целую страницу 4 КБ. 0.5 КБ тратится впустую внутри выделенного блока.
Почему страницы обычно 4 КБ?
Выбор размера страницы - компромисс: **Маленькие страницы (1 КБ):** • Меньше internal fragmentation (в среднем 0.5 КБ на процесс) • Больше таблиц страниц (overhead) • Больше page faults (медленно) **Большие страницы (64 КБ):** • Больше internal fragmentation (в среднем 32 КБ на процесс!) • Меньше таблиц страниц • Меньше page faults **4 КБ** - золотая середина, используется большинством OS. Хотя базы данных часто используют huge pages (2 МБ) для уменьшения overhead.
**Paging полностью решает внешнюю фрагментацию**, но вводит небольшую внутреннюю. Это приемлемый trade-off - в среднем теряется 0.5 страницы (2 КБ) на процесс против невозможности использовать сотни мегабайт при внешней фрагментации.
**Интересный факт:** Исследования показали, что в системе с contiguous allocation после нескольких часов работы может тратиться впустую **до 50%** RAM из-за внешней фрагментации! Paging снижает потери до **1-3%** (internal fragmentation).
Фрагментация - это когда в системе заканчивается память
Фрагментация - когда память ЕСТЬ, но она разбита на куски и не может быть использована эффективно
Классический пример: в системе свободно 1 ГБ RAM, но разбитый на блоки по 16 МБ. Программа запрашивает непрерывный блок 512 МБ - получает out of memory, хотя свободной памяти хватает. Проблема не в количестве, а в фрагментации. Paging решает это, позволяя процессу использовать несмежные страницы, создавая иллюзию непрерывности через MMU.
Ключевые идеи
- **Иерархия памяти:** От регистров (0.3 нс) до HDD (10 мс) - разница в 33 миллиона раз! Эффективная программа использует принципы локальности для минимизации cache misses.
- **Address binding:** Программа работает с виртуальными адресами, MMU транслирует их в физические на лету. Это позволяет OS перемещать процессы, изолировать их и использовать больше памяти, чем физически установлено (swapping).
- **Contiguous allocation:** Процесс занимает единый блок памяти. Просто, но приводит к внешней фрагментации. Алгоритмы First/Best/Worst Fit пытаются эффективно использовать дырки в памяти.
- **Фрагментация:** Внешняя (память разбита на куски) решается paging. Внутренняя (выделено больше чем нужно) - неизбежный overhead фиксированных страниц (~1-3% потерь).
Связанные темы
Управление памятью - основа для понимания виртуальной памяти и производительности:
- Виртуальная память (Virtual Memory) — Paging и swapping позволяют программам использовать больше памяти, чем физически доступно. Demand paging загружает страницы по требованию
- Процессы — Каждый процесс имеет изолированное адресное пространство благодаря MMU. fork() копирует виртуальные страницы через copy-on-write
- Файловые системы — Memory-mapped files (mmap) отображают файл в адресное пространство процесса. OS подгружает страницы файла по требованию как обычную память
- Алгоритмы замещения страниц — Когда RAM заканчивается, какую страницу вытеснить на диск? LRU, Clock, Second Chance - алгоритмы выбора жертвы для swapping
Вопросы для размышления
- Почему 64-битная система может адресовать 2⁶⁴ байт (16 exabytes), но никто не устанавливает столько RAM? Где виртуальные адреса, которые не отображены в физическую память?
- Если paging решает внешнюю фрагментацию, почему malloc всё ещё использует free lists и алгоритмы типа First Fit? Разве OS не управляет всей памятью?
- Почему увеличение размера страницы с 4 КБ до 2 МБ (huge pages) ускоряет базы данных, но может замедлить обычные приложения?
- Как OS гарантирует, что два процесса не получат доступ к одной и той же физической памяти случайно? Что произойдёт если баг в MMU?
Связанные уроки
- os-08-virtual-memory — Виртуальная память строится поверх физической организации
- os-02-processes — Каждый процесс имеет собственное адресное пространство
- os-16-memory-advanced — NUMA, huge pages, memory-mapped I/O - продвинутые темы
- rt-39