Виртуальная память: stack, heap и mmap
Виртуальная память даёт каждому процессу собственное непрерывное виртуальное адресное пространство. Адрес в указателе — не физический адрес DRAM: MMU переводит виртуальную страницу в физический фрейм по таблицам страниц. Поэтому два процесса могут использовать одинаковый виртуальный адрес, не видя память друг друга; ядро также может выделить большой диапазон адресов без немедленного занятия такого же объёма RAM.
Зачем это на интервью
Тема связывает «высокий RSS», ENOMEM, OOM-killer, скорость аллокаций, утечки и поведение Go runtime. На интервью важно не просто назвать stack и heap, а объяснить, почему резерв виртуального адреса не равен потреблению памяти, когда mmap предпочтительнее read, и по каким данным в Linux подтверждать гипотезу.
Минимум для E4
- Различать virtual address space, физическую RAM, страницу и RSS: виртуальный диапазон может быть зарезервирован, но не resident.
- Объяснять назначение stack, heap и
mmap, не утверждая, что любой объект «всегда на стеке» или «всегда на куче». - Знать, что стек потока растёт в заданном направлении и ограничен guard page/лимитом; переполнение обычно заканчивается аварией процесса.
- Понимать, что
mallocвозвращает память из пользовательского allocator, который может брать страницы у ядра черезbrkи/илиmmap; это не обязательно один syscall на объект. - Для Linux-сервиса уметь начать диагностику с
/proc/<pid>/status,/proc/<pid>/smapsиpmap, сопоставив virtual size, RSS и anonymous/file-backed mappings.
Углубление для E5/Senior
Что именно учитывает память
- VSS/VSZ — размер отображённого виртуального адресного пространства. Он включает не тронутые страницы, разделяемые библиотеки и зарезервированные области; сам по себе не доказывает дефицит RAM.
- RSS — страницы mapping, которые сейчас находятся в RAM. RSS разделяет общие физические страницы между процессами не так, как это удобно для charge в контейнере.
- PSS делит стоимость shared page между потребителями, а
Private_Dirtyизsmapsполезен для поиска реально уникальной изменённой памяти. - Committed memory — обещание ядра обеспечить будущую запись; при политике overcommit резерв и гарантия различаются. Проверка
vm.overcommit_memoryбез контекста не заменяет анализа cgroup-лимита.
Trade-offs отображений
mmap file-backed отображает файл в адресное пространство, а page cache держит прочитанные страницы. Это может убрать лишнее копирование в пользовательский буфер и позволяет лениво подгружать участки файла. Но случайный доступ вызывает page faults, доступ к повреждённому или усечённому файлу может дать SIGBUS, а dirty shared mapping требует осмысленной политики синхронизации (msync там, где она нужна).
MAP_PRIVATE реализует частное представление: запись не меняет исходный файл и отделяет страницу через copy-on-write. MAP_SHARED видит изменения между mapping и файлом/другими процессами, что удобно для IPC и файлов, но усложняет согласованность. Анонимный mmap подходит allocator-ам и большим буферам. Решение выбирать по профилю доступа, лимитам и жизненному циклу, а не по мифу «mmap всегда быстрее read».
Связь с Go и контейнерами
Go управляет кучей и аренами runtime поверх виртуальных mapping; GC возвращает не каждую освобождённую байту ОС сразу. Escape analysis решает, может ли значение жить вне стека вызывающей функции, но компилятор вправе менять размещение. Goroutine имеет маленький растущий стек, который не равен фиксированному stack OS thread; переполнение goroutine stack также фатально. Профили go tool pprof, runtime.MemStats и метрики процесса нужно читать вместе с RSS и memory.current cgroup: heap profile не включает page cache, mmap сторонней библиотеки и весь native overhead.
Ключевые понятия
| Понятие | Суть | Практический вывод |
|---|---|---|
| Виртуальная страница | Единица отображения MMU, обычно 4 KiB на x86-64 Linux, но размер архитектурно зависим | Стоимость fault и locality считают страницами, не только байтами |
| Stack | Кадры вызовов, локальные данные и метаданные выполнения | Глубокая рекурсия и крупные локальные массивы опасны; размер ограничен |
| Heap | Объекты с произвольным временем жизни, управляемые allocator/GC | Фрагментация и время жизни важнее единичной цены malloc |
mmap | VMA: диапазон виртуальных адресов, связанный с файлом или anonymous memory | Смотрите permissions, shared/private и file-backed признак в smaps |
| Page cache | Кэш файловых страниц ядра | Чтение файла может поднять memory pressure без роста Go heap |
| Guard page | Неприступная страница у границы стека | Превращает выход за границу в fault вместо тихой порчи памяти |
Полезная ментальная модель: адресное пространство состоит из VMA (virtual memory areas): код и данные executable, shared libraries, stack, heap, anonymous mapping и file-backed mapping. Каждая VMA имеет права rwx, тип sharing и диапазон; фактическая физическая страница появляется или подключается при обращении.
Типовые вопросы
- Почему процесс с VSZ 20 GiB может потреблять лишь 800 MiB RAM?
- VSZ включает все mapping и резерв; физические фреймы требуются лишь resident страницам. Нужно проверить RSS/PSS,
smapsи политики commit/cgroup, а не объявлять VSZ утечкой.
- VSZ включает все mapping и резерв; физические фреймы требуются лишь resident страницам. Нужно проверить RSS/PSS,
- Чем stack отличается от heap?
- Stack организован по вызовам и обычно освобождается при возврате из функции; heap хранит объекты с более долгой или неструктурной жизнью. Это модель времени жизни, а не гарантия размещения каждого локального значения.
- Когда использовать
mmapдля файла?- Для произвольного/lazy доступа к крупному файлу или совместного отображения, когда fault и обработка ошибок приемлемы. Для последовательного потокового I/O
readс буфером часто проще и предсказуемее.
- Для произвольного/lazy доступа к крупному файлу или совместного отображения, когда fault и обработка ошибок приемлемы. Для последовательного потокового I/O
- Почему
freeне всегда снижает RSS?- Allocator может оставить свободные span/арены для повторного использования, а ядро не обязано немедленно отобрать или выгрузить страницы. Сначала отличают нормальный high-water mark от продолжающегося роста live memory.
- Как отличить leak Go heap от file-backed memory?
- Сравнить heap profile и
runtime.MemStatsсsmaps/pmap; рост anonymous private mapping согласуется с кучей, а file-backed RSS — с mapping/page cache. Затем подтвердить серией снимков и нагрузкой.
- Сравнить heap profile и
- Что происходит при доступе к неотображённому адресу?
- CPU генерирует page fault; ядро проверяет VMA. При отсутствии допустимого mapping процесс получает
SIGSEGV, а при допустимой, но отсутствующей resident странице ядро может подгрузить/выделить её.
- CPU генерирует page fault; ядро проверяет VMA. При отсутствии допустимого mapping процесс получает
Практика
- Инвентаризация mapping процесса. Запустите небольшой Go-процесс, снимите
pmap -x <pid>и/proc/<pid>/smaps_rollupдо и после выделения 256 MiB и касания каждой страницы.- Критерии готовности: в отчёте есть VSZ, RSS/PSS и объяснение, почему выделение без касания отличается от записи; команды и версия окружения воспроизводимы.
- Сравнение buffered I/O и
mmap. На тестовом файле реализуйте последовательное чтение черезos.File.Readи случайный выбор блоков через memory-mapped API либо минимальную C-программу; измерьте время и major/minor faults.- Критерии готовности: нагрузка одинаковая по объёму и паттерну, приведены измерения и вывод с ограничениями; не используйте
mmapв production только по микро-бенчмарку.
- Критерии готовности: нагрузка одинаковая по объёму и паттерну, приведены измерения и вывод с ограничениями; не используйте
- Разбор памяти Go-сервиса. Соберите heap profile и
smaps_rollupпод стабильной нагрузкой; сформулируйте, что относится к Go heap, стекам, shared libraries и file-backed pages.- Критерии готовности: есть два снимка во времени, гипотеза о самом большом компоненте и способ её опровергнуть.
Частые ошибки и ловушки
- Считать VSZ потреблённой RAM или считать RSS точным charge процесса в cgroup.
- Называть
mmap«загрузкой файла в память»: mapping создаёт адресный диапазон; чтение страниц обычно ленивое. - Игнорировать права mapping: попытка записи в read-only mapping — не «медленная запись», а fault.
- Принимать рост RSS после нагрузки за leak без анализа plateau, PSS, heap profile и allocator cache.
- Использовать
unsafe/mmapв Go без явного владения временем жизни: slice не должен пережитьUnmap, а доступ после него может аварийно завершить процесс. - Смешивать память процесса и page cache в контейнере: конкретный accounting зависит от версии ядра, cgroup и типа mapping.
Связанные темы
Модуль Foundations · Страничная адресация, page fault и copy-on-write · Go runtime, GC и профилирование · Linux: RSS, OOM и диагностика · Наблюдаемость и расследование деградаций · System Design: ресурсы и ограничения
Источники
- Linux kernel documentation: Memory Management.
- Linux manual pages:
mmap(2)иproc_pid_smaps(5). - Go documentation: A Guide to the Go Garbage Collector.
- Исходный код Go runtime:
runtime/HACKING.md.