Виртуальная память: 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
mmapVMA: диапазон виртуальных адресов, связанный с файлом или 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 и диапазон; фактическая физическая страница появляется или подключается при обращении.

Типовые вопросы

  1. Почему процесс с VSZ 20 GiB может потреблять лишь 800 MiB RAM?
    • VSZ включает все mapping и резерв; физические фреймы требуются лишь resident страницам. Нужно проверить RSS/PSS, smaps и политики commit/cgroup, а не объявлять VSZ утечкой.
  2. Чем stack отличается от heap?
    • Stack организован по вызовам и обычно освобождается при возврате из функции; heap хранит объекты с более долгой или неструктурной жизнью. Это модель времени жизни, а не гарантия размещения каждого локального значения.
  3. Когда использовать mmap для файла?
    • Для произвольного/lazy доступа к крупному файлу или совместного отображения, когда fault и обработка ошибок приемлемы. Для последовательного потокового I/O read с буфером часто проще и предсказуемее.
  4. Почему free не всегда снижает RSS?
    • Allocator может оставить свободные span/арены для повторного использования, а ядро не обязано немедленно отобрать или выгрузить страницы. Сначала отличают нормальный high-water mark от продолжающегося роста live memory.
  5. Как отличить leak Go heap от file-backed memory?
    • Сравнить heap profile и runtime.MemStats с smaps/pmap; рост anonymous private mapping согласуется с кучей, а file-backed RSS — с mapping/page cache. Затем подтвердить серией снимков и нагрузкой.
  6. Что происходит при доступе к неотображённому адресу?
    • CPU генерирует page fault; ядро проверяет VMA. При отсутствии допустимого mapping процесс получает SIGSEGV, а при допустимой, но отсутствующей resident странице ядро может подгрузить/выделить её.

Практика

  • Инвентаризация 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.