Віртуальна пам'ять у роботі
Навіщо це
Section titled “Навіщо це”Програма виділяє 8 ГіБ на машині з чотирма — і працює. Файл на 40 ГіБ
відкривається через mmap миттєво. fork процесу, який займає 2 ГіБ,
триває мікросекунди.
За всіма трьома випадками стоїть один механізм: сторінка може бути в адресному просторі, не будучи в пам’яті. Біт дійсності в записі таблиці сторінок (модуль 11) перетворює будь-яке звернення на подію, яку ядро обробляє на власний розсуд.
Передумови. Таблиця сторінок і біт дійсності (модуль 11), переривання і винятки (модуль 2).
Підкачка на вимогу
Section titled “Підкачка на вимогу”Завантажувати всю програму в пам’ять перед запуском не потрібно, та й майже завжди марно: значна частина коду за весь сеанс так і не виконається.
Підкачка на вимогу (demand paging) означає, що сторінка потрапляє в пам’ять у момент першого звернення до неї, і не раніше. Ядро скидає біт дійсності в записах усіх ще не завантажених сторінок, а апаратура сама повідомляє, коли якась із них знадобилася.
Сторінковий виняток (page fault) не є помилкою, попри назву. Це штатне
повідомлення апаратури: відображення відсутнє, розберися. Помилкою він стає
лише тоді, коли адреса справді не належить процесу, і тоді ядро надсилає
SIGSEGV.
До диска більшість винятків узагалі не доходить:
- м’який трапляється, коли сторінка вже у фізичній пам’яті й досить додати запис у таблицю. Так буває зі спільною бібліотекою, яку вже завантажив інший процес;
- жорсткий вимагає читання з диска.
Порядки величин тут такі: м’який виняток коштує одиниці мікросекунд (на віртуальній машині з першого дотику до анонімної пам’яті виходить близько 2.3 мкс на сторінку), жорсткий на SSD — сотні мікросекунд, на диску, що обертається, — одиниці мілісекунд. Різниця в два-три порядки, і саме вона вирішує, чи система відповідає, чи стоїть.
Копіювання під час запису
Section titled “Копіювання під час запису”За задумом fork мав би скопіювати весь адресний простір. Замість цього
ядро позначає сторінки обох процесів як доступні тільки для читання
й лишає їх спільними.
Перший же запис у таку сторінку викликає сторінковий виняток. Ядро бачить, що сторінка спільна й позначена для копіювання, робить копію саме цієї сторінки, дає процесу право запису й повторює інструкцію.
Наслідки видно щодня. fork великого процесу коштує майже стільки ж,
скільки малого. Сотня процесів однієї програми ділить один екземпляр коду.
А щойно виділена через malloc пам’ять фізично не існує, поки в неї
не запишуть.
Заміщення сторінок
Section titled “Заміщення сторінок”Коли вільних фреймів немає, а сторінку треба завантажити, доводиться когось витіснити. Питання лише в тому, кого саме.
| Алгоритм | Правило | Проблема |
|---|---|---|
| Оптимальний | той, до кого звернуться найпізніше | потребує знання майбутнього; використовується як еталон |
| FIFO | найдавніше завантажений | давність завантаження не пов’язана з корисністю |
| LRU | найдавніше використаний | точна реалізація потребує оновлення позначки на кожному зверненні |
| Годинниковий | наближення LRU через біт звернення | працює, тому й застосовується насправді |
Оптимальний алгоритм нереалізовний, але потрібен: без нього немає з чим порівнювати решту.
LRU здається ідеальним наближенням, а в чистому вигляді майже не використовується, бо оновлювати часову позначку при кожному зверненні до пам’яті неможливо ні апаратно, ні програмно.
Реально працює годинниковий алгоритм. Сторінки утворюють кільце, і по ньому йде стрілка. Якщо біт звернення встановлений, стрілка скидає його й рухається далі, тобто сторінка отримує ще один шанс. Якщо скинутий, цю сторінку й витісняють. Апаратура ставить біт безкоштовно, а ядро зрідка його читає. Linux використовує варіант із двома списками, активним і неактивним.
Робочий набір і трешинг
Section titled “Робочий набір і трешинг”Робочим набором процесу називають множину сторінок, до яких він звертається в поточному проміжку часу. Поки робочі набори всіх активних процесів уміщаються у фізичну пам’ять, система працює нормально.
Щойно вони перестають уміщатися, починається трешинг. Процес витісняє сторінку, яка негайно знадобиться іншому, той витісняє її назад, і між винятками ніхто не встигає зробити нічого корисного.
Симптом упізнається одразу: завантаження процесора падає майже до нуля, диск завантажений на сто відсотків, система не реагує. Найгірше те, що трешинг сам себе підсилює — планувальник бачить простій процесора й додає процесів, а кожен приносить свій робочий набір.
Що з цим роблять:
- обмежують кількість одночасно активних процесів;
- виділяють фрейми пропорційно робочому набору, а не порівну;
- зупиняють або завершують частину процесів — цим займається OOM killer;
- у Linux читають індикатор тиску (
/proc/pressure/memory), який показує проблему до того, як система стане.
Що це дає на практиці
Section titled “Що це дає на практиці”mmap відображає файл в адресний простір, тож читання перетворюється
на звернення до пам’яті, а завантаження стає роботою механізму підкачки
на вимогу. Сорокагігабайтний файл відкривається миттєво, бо нічого
не читається, поки ви не звернетеся.
Кеш сторінок — це той самий механізм з іншого боку: прочитані з диска сторінки лишаються в пам’яті. Тому повторне читання файлу миттєве, а вся вільна пам’ять у Linux зайнята кешем (модуль 13).
Swap — місце на диску під витіснені анонімні сторінки, тобто ті, за якими немає файлу: купу й стек. Сам по собі він систему ні прискорює, ні сповільнює, а дає ядру можливість витіснити те, що не використовується, замість тримати його в пам’яті на місці корисного кеша. zram робить те саме, але замість диска слугує стиснена область у самій пам’яті: удвічі-втричі більше даних за ціну процесорного часу.
OOM killer спрацьовує, коли пам’яті немає, а витісняти вже нічого.
Жертву він обирає за оцінкою oom_score, грубо кажучи, найбільшого
споживача, і завершує її. Побачити його роботу можна в журналі ядра.
Як це насправді в Linux
Section titled “Як це насправді в Linux”ps -o min_flt,maj_flt,rss,vsz,comm -p $$min_flt — м’які винятки, maj_flt — жорсткі, з читанням із диска.
Здоровий процес має тисячі перших і одиниці других.
/usr/bin/time -v ls / 2>&1 | grep -E 'page faults|Maximum resident'Те саме для окремого запуску, разом із піковим RSS.
grep -E 'pgfault|pgmajfault|pswpin|pswpout' /proc/vmstatЛічильники з моменту завантаження. Два виміри з паузою покажуть поточну інтенсивність — саме так відрізняють трешинг від звичайного навантаження.
cat /proc/pressure/memoryІндикатор тиску на пам’ять (PSI): частка часу, яку задачі провели в очікуванні пам’яті. Показує проблему задовго до того, як система стане.
free -h; swapon --show; cat /sys/block/zram0/comp_algorithm 2>/dev/nullПам’ять, кеш і підкачка. Дивитись треба на available, а не на free.
journalctl -k --grep 'Out of memory|oom_reaper' | tail -5Історія спрацювань OOM killer: кого обрано жертвою і скільки пам’яті той займав.
python3 -c "import mmap, osf = os.open('/usr/bin/python3', os.O_RDONLY)m = mmap.mmap(f, 0, prot=mmap.PROT_READ)print('відображено', len(m), 'байтів, прочитано з диска: 0')print('перший байт:', m[0:4])"Відображення відбувається миттєво незалежно від розміру файлу: дані приходять сторінками під час звернення.
Типові помилки розуміння
Section titled “Типові помилки розуміння”«Сторінковий виняток — це помилка». Це штатний механізм. Тисячі винятків за секунду означають нормальну роботу, а проблемою є лише жорсткі винятки, які йдуть на диск.
«Вимкну swap, система стане швидшою». Без swap ядро не може витіснити жодної анонімної сторінки, тож рідковживані дані витісняють із пам’яті корисний кеш сторінок. Результат нерідко протилежний очікуваному, а замість плавного сповільнення система одразу зустрічає OOM killer.
«malloc повернув адресу, значить пам’ять виділено». Виділено адреси.
Фізичні сторінки з’являться під час першого запису, і от тоді може
з’ясуватися, що їх немає.
«Процес убито, значить у програмі баг». Спершу подивіться
journalctl -k на предмет OOM killer. Процес міг бути цілком коректним
і просто виявитися найбільшим на момент, коли пам’ять закінчилася.
«LRU використовується в операційних системах». Використовуються його наближення. Точний LRU вимагав би оновлювати позначку при кожному зверненні до пам’яті, чого не робить жодна система.
«Трешинг — це коли не вистачає пам’яті». Трешинг настає, коли не вміщаються робочі набори. Вільна пам’ять при цьому може бути, просто не там, де потрібно, і система витрачає весь час на переміщення сторінок.
Перевір себе
Лабораторна
Section titled “Лабораторна”A5 — симулятор заміщення сторінок. FIFO, LRU, годинниковий і оптимальний на реальному трасі звернень. Обов’язкова частина — відтворити аномалію Беладі на FIFO і переконатися, що на LRU вона не відтворюється.
B3 — cgroups і OOM. Обмежити процесу пам’ять через cgroups v2, довести його до межі й простежити в журналі, як спрацьовує OOM killer.
Джерела
Section titled “Джерела”- OSTEP: Beyond Physical Memory: Mechanisms, Policies
- Silberschatz, Operating System Concepts, розділ 10
man 2 mmap,man 5 proc,man 8 zramctl- Documentation/admin-guide/mm/concepts.rst
- Pressure Stall Information — як побачити нестачу пам’яті заздалегідь