Продолжаем говорить про память. В прошлых постах мы спроектировали Stack и Heap. Но то была картина изнутри программы. Теперь поднимемся на уровень выше и посмотрим, как всё это выглядит со стороны ОС.
————
Снова надеваем каску инженера. Наша команда проектирует операционную систему, и нам поручили разобраться с менеджментом памяти — нашей ОС доступен определённый её объём, и его нужно каким-то образом распределять между процессами.
Попытка 1: Нарезаем RAM — прямой доступ
Самое простое решение — поделить физическую RAM на кусочки: программе A отдадим адреса
0x0000–0x1000, программе B — 0x1000–0x2000, и так далее.На первый взгляд, всё работает, но... Сразу же вылезает огромный букет проблем.
1. Безопасность: Что мешает Программе А обратиться к адресу программы Б и прочитать пароли из её памяти? Ничего. А если она туда что-то запишет, то Программа Б просто сломается.
2. Изоляция и адресация: Компилятору нужно заранее знать, по каким адресам будут лежать переменные, чтобы скомпилировать код. Но мы не можем заранее знать, какие физические адреса будут свободны в момент запуска программы 🗿
🟢Очевидно, что напрямую пускать процессы к железу нельзя. Думаем дальше.
Попытка 2: Иллюзия одиночества — базовый адрес и граница
Что ж, забираем у программы прямой доступ к адресам. Мы всё ещё будем присваивать ей какую-то область, но адресуем сами. То есть, каждая программа будет думать, что ей доступна вся доступная память: от
0x0000 до capacity. К примеру, [0x0000, 0x1000]. Мы же просто добавляем соответствующее смещение и следим, чтобы программа не вылезала за свои пределы.Допустим мы выдали программе диапазон
[0x3000, 0x4000]. Сама она при этом работает с адресами [0x0000, 0x1000]. Когда программа обращается к ячейке 0x0050, мы просто добавляем к ней смещение +0x3000 и получаем адрес: 0x3050. А если она просит больше, чем ей доступно, выдаём ошибку.Это уже лучше! Но вылезает ещё более коварная проблема — фрагментация.
Представьте, что у вас 1гб памяти, и вы запустили 500 мелких программ, которым нужно ~2мб. В итоге, ваш гигабайт будет нарезан на 500 мелких кусочков.
Далее мы закрываем половину этих программ, чтобы освободить место для одной тяжёлой программы. И вот проблема: мы освободили 250 кусочков по 2мб, но они разбросаны рандомно по всему пространству! И у нас нет ни одного свободного промежутка хотя бы в 100мб:
[== Физическая память 1 ГБ ==]
1. Память забита (по 2 МБ):
|█|█|█|█|█|█|█|█|█|█|█|█|█|█|█|
2. Закрыли половину (ДЫРЫ):
|█| |█| | |█|█| |█| |█| | |█|█|
^ ^ ^ ^ ^ ^ ^
2мб 2мб 4мб 2мб 2мб 4мб 2мб
(Суммарно места много, но оно
разбито на мелкие куски)
3. Нужен цельный кусок 100 МБ:
Требуется: |██████████|
Результат: ОШИБКА! Не влезает...
Попытка 3: Страничная организация (Paging)
Очевидно, нам нужно уметь из мелких кусочков собирать большие. Давайте будем нарезать память на одинаковые мелкие кусочки — страницы (обычно по 4 КБ), а затем маппить запрошенные программой адреса с реальными через специальную таблицу (Page Table).
То есть, ОС будет вести полный учёт — кому какая страница принадлежит и правильно сопоставлять адреса с помощью таблицы
Программа же не догадывается об этой машинерии — она всё так же работает в своём виртуальном диапазоне,
[0x0000,0x2000]. Например:1. Программа пишет по адресу
0x10502. ОС смотрит в таблицу и сопоставляет: виртуальный адрес
0x1050 с физическим 0x8A3050Что это нам даёт?
Безопасность: у каждой программы своя таблица страниц — свой изолированный мир. До чужой памяти физически не дотянуться, а попытка вылезти за пределы своего пространства — ошибка. Привет, Segmentation fault! 👍
Дефрагментация: виртуальные страницы программы могут идти строго по порядку, одна за другой. А вот в физической памяти ОС может раскидать их как угодно по любым свободным промежуткам.
Готово?! Да, концептуально оно работает, но есть нюанс.. 👀
На практике мы замечаем, что наша ОС жутко тормозит — топорный программный поиск по таблице, это слишком тяжёлая операция. В следующем посте будем это оптимизирвать.
#guide #memory
