Тема, на которой спотыкаются почти все: linux память процесса считается совсем не так наивно, как кажется. Цифра RSS в top врет, и сейчас ты поймешь почему.
Почему RSS врет и что вообще такое память процесса в linux
Когда ты смотришь на колонку RES в top или RSS в ps, ты видишь Resident Set Size - сколько физической RAM сейчас занимает процесс. Звучит честно, но есть подвох: сюда входит память, которую процесс делит с другими.
Представь библиотеку libc. Ее загрузили в RAM один раз, а пользуются ей сотни процессов. Но RSS каждого из них включает полный размер этих общих (shared) страниц. Если у тебя сотня процессов nginx или php-fpm и ты сложишь их RSS, получится цифра больше, чем вся RAM в системе. Это не магия - просто общую память посчитали много раз.
Поэтому для поиска пожирателей вводят более честные метрики:
- RSS - вся резидентная память, включая общие страницы целиком. Завышает.
- USS (Unique Set Size) - только то, что уникально процессу. Ровно столько RAM освободится, если процесс убить. Технически это Private_Clean + Private_Dirty из smaps. Занижает (игнорирует общее).
- PSS (Proportional Set Size) - золотая середина. Уникальная память плюс "справедливая доля" общей: если страницу делят 4 процесса, каждому засчитывается четверть. Сумма PSS всех процессов как раз сходится с реально занятой RAM.

smem: честный PSS по процессам - первый инструмент против утечки памяти linux
Утилита smem читает /proc/PID/smaps по всем процессам и считает PSS и USS. В большинстве дистрибутивов ставится так:
Код: Выделить всё
sudo apt install smem # Debian/Ubuntu/Astra Linux
sudo dnf install smem # Fedora/RHEL/RED OS
Код: Выделить всё
$ sudo smem -t -k -s pss -r
PID User Command Swap USS PSS RSS
2841 www-data php-fpm: pool www 0 312.4M 318.1M 402.7M
1190 mysql /usr/sbin/mariadbd 4.0M 180.2M 181.0M 206.3M
1503 root /usr/bin/dockerd 0 88.1M 91.4M 140.9M
-------------------------------------------------------------------------------
47 1 6.1M 640.5M 661.7M 1.1G
- USS - сколько освободится при kill. У php-fpm 312M уникальны, при перезапуске вернутся в систему.
- PSS - честная доля. 318M против RSS 402M: разница в 84M это та самая общая память (тот же libc, тот же opcache, разделяемый между воркерами).
- RSS - завышенная цифра, которую ты привык видеть в top.
- Swap - сколько ушло в своп. У mariadbd 4M в свопе - не криминал сам по себе, но звоночек, что памяти было тесно.
- Строка -t (total) внизу: сумма PSS 661M это и есть реально занятое процессами. RSS суммарно 1.1G - вот насколько обманчива простая сумма RSS.
Если smem ставить нельзя (минимальный образ, нет интернета) - тот же PSS суммарно по процессу можно взять без него, прямо из ядра, об этом ниже в smaps_rollup.
pmap и smaps: вскрываем процесс и локализуем, куда утекает память
Нашли PID-обжору. Теперь надо понять, куда именно ушла память: в кучу (heap), в файлы через mmap, в библиотеки. Здесь главный инструмент - pmap.
Код: Выделить всё
$ sudo pmap -x 2841
2841: php-fpm: pool www
Address Kbytes RSS Dirty Mode Mapping
000055b3c0a00000 18432 17200 17200 rw--- [ anon ]
00007f2a14000000 131072 128640 128640 rw--- [ anon ]
00007f2a3c1f0000 2048 1980 0 r-x-- libc.so.6
00007f2a3c400000 512 180 0 r---- locale-archive
...
---------------- ------- ------- -------
total kB 412800 402700 302100
- Address - начальный адрес региона памяти.
- Kbytes - размер виртуального региона (зарезервировано, это вклад в VSZ).
- RSS - сколько из региона реально лежит в RAM.
- Dirty - "грязные" страницы: изменены и не сброшены на диск. Это ключевая колонка для утечек. Большой anon-регион с огромным Dirty - это разросшаяся куча приложения, классическое место утечки. Чистые (clean) страницы кода ядро может выкинуть и перечитать с диска, грязные - нет, они держат RAM мертвой хваткой.
- Mode - права: rw--- (данные/куча, изменяемое), r-x-- (исполняемый код библиотек), r---- (только чтение, например locale-archive).
- Mapping - что за регионом: [ anon ] это анонимная память (куча, malloc, mmap без файла), [ stack ] стек, а имя файла - это mmap файла или библиотека.
Под капотом pmap и smem читают /proc/PID/smaps - там по каждому региону десятки полей. Читать его построчно руками тяжело, поэтому с ядра 4.14+ есть агрегат smaps_rollup, который суммирует все регионы в одну сводку:
Код: Выделить всё
$ sudo cat /proc/2841/smaps_rollup
55b3c0a00000-7ffffffff000 ---p 00000000 00:00 0 [rollup]
Rss: 402700 kB
Pss: 318100 kB
Shared_Clean: 62000 kB
Shared_Dirty: 0 kB
Private_Clean: 600 kB
Private_Dirty: 301500 kB
Pss_Dirty: 300800 kB
Referenced: 350000 kB
Anonymous: 298000 kB
Swap: 0 kB
- Pss - та самая честная цифра по всему процессу, без установки smem.
- Private_Dirty - приватные измененные страницы, которые никуда не денутся и ни с кем не делятся. Главный кандидат на утечку.
- Pss_Dirty (есть с ядра 5.18+, актуально на 2026) - доля грязного в PSS. Удобно: показывает, сколько честной памяти процесса именно изменяемое, а не код-страницы.
- Anonymous - память без файла за спиной (куча и стек). Это то, что нельзя сбросить на диск и перечитать.
- Referenced - сколько страниц процесс трогал недавно (горячий рабочий набор). Если Rss большой, а Referenced заметно меньше - часть памяти холодная, кандидат на своп.
- USS = Private_Clean + Private_Dirty - тут это примерно 302M.
Память процесса в динамике: ловим монотонный рост
Одна цифра ничего не доказывает. Утечка памяти linux - это не "много", а "монотонно растет и не падает". Процесс может честно занять 2G под кеш и стоять ровно - это не утечка. А может по 5M в минуту ползти вверх без остановки - вот это оно.
Снимай динамику через pidstat -r (пакет sysstat) - раз в 2 секунды, 5 раз:
Код: Выделить всё
$ pidstat -r -p 2841 2 5
12:00:01 UID PID minflt/s majflt/s VSZ RSS %MEM Command
12:00:03 33 2841 420.00 0.00 812000 402700 10.1 php-fpm
12:00:05 33 2841 455.00 0.00 814000 408900 10.2 php-fpm
12:00:07 33 2841 480.00 2.00 816000 415100 10.4 php-fpm
- minflt/s - малые page fault в секунду: страница нашлась в RAM (или подцепилась из page cache без чтения с диска), грузить с диска не пришлось. Нормальный фон, особенно у активного процесса.
- majflt/s - большие page fault: пришлось читать страницу с диска. Если majflt/s стабильно ненулевой и растет - процессу тесно, он свопится или дергает mmap-файлы, это сигнал давления на память.
- VSZ - виртуальный размер (Кбайт), сколько зарезервировано. Часто большой и не страшный сам по себе.
- RSS - резидентная память (Кбайт). Вот тут смотри тренд: 402700 -> 408900 -> 415100, плюс 6M за 4 секунды и не откатывается. Это и есть монотонный рост.
eBPF: трассируем утечку до стека вызовов (актуально на 2026)
smem и pmap отвечают "куда" (в какой регион), но не "кто" (какая строка кода аллоцировала и не освободила). Раньше для этого лезли в valgrind с дикими тормозами или в gdb. На 2026 зрелый ответ - eBPF: инструмент memleak из пакета bcc (bpfcc-tools). Он цепляется к malloc/calloc/realloc/free через uprobes, собирает стек каждой аллокации и показывает те стеки, чья память так и не была освобождена - прямо на живом проде, без перекомпиляции и почти без остановки сервиса.
Код: Выделить всё
sudo apt install bpfcc-tools linux-headers-$(uname -r) # Debian/Ubuntu/Astra
sudo dnf install bcc-tools # RHEL/RED OS
# трассируем процесс 2841, сводка outstanding-аллокаций каждые 5 сек
$ sudo memleak-bpfcc -p 2841 5
[12:30:05] Top 10 stacks with outstanding allocations:
81920 bytes in 20 allocations from stack
_emalloc+0x1a [php-fpm]
cache_put+0x44 [app.so]
handle_request+0x91 [app.so]
Где eBPF уже вытеснил старое (на 2026): для разовой диагностики аллокаций memleak практичнее valgrind massif, а связка smem (где) -> memleak (кто) заменяет долгие сессии в отладчике.
Типичные грабли и заблуждения
- Складывать RSS всех процессов. Получишь больше физической RAM и решишь, что "память кончилась из-за процессов". Считай PSS через smem -t.
- Путать file cache с утечкой. В free/htop "занятая" память часто это кеш файлов, который ядро мгновенно отдаст под нагрузку. Это buff/cache, не утечка. Смотри строку available в free -h: вот сколько реально можно занять без свопа. Утечка - это растущий Private_Dirty/Anonymous конкретного процесса.
- Большой VSZ это плохо. Нет. VSZ это виртуальный резерв, его можно нарезать гигабайтами и не трогать RAM (так делают JVM, Go-рантайм, аллокаторы с аренами). Смотри RSS и PSS.
- Один замер = диагноз. Память дышит. Утечка доказывается только трендом во времени, а не одним снимком.
- Винить swap. Немного свопа при наличии free RAM - это нормальная работа ядра, оно вытесняет холодные страницы. Тревога - это активный своппинг (si/so в vmstat ненулевые постоянно) плюс растущий majflt/s.
- Поставь smem и запусти sudo smem -t -k -s pss -r. Найди свой топ-3 по PSS. Сравни PSS и RSS у самого жирного - оцени, сколько у него общей памяти.
- Возьми PID браузера или сервера БД и выполни sudo pmap -x PID. Найди самый большой регион [ anon ] с большим Dirty - это его куча.
- Сравни sudo cat /proc/PID/smaps_rollup: посмотри Pss, Private_Dirty, Pss_Dirty, Anonymous. Прикинь USS = Private_Clean + Private_Dirty.
- Запусти pidstat -r -p PID 2 10 и последи за колонкой RSS - растет, падает или стоит ровно? Заодно глянь majflt/s.
- Если стоит bpfcc-tools - запусти sudo memleak-bpfcc -p PID 5 на пару минут и посмотри, есть ли стек, который растет от замера к замеру.
- Почему сумма RSS всех процессов может превысить объем физической RAM и какая метрика считает честно?
- Чем USS отличается от PSS, из каких полей smaps складывается USS и в каком случае какую метрику брать?
- В выводе pmap -x какая колонка и какое значение Mapping укажут на разросшуюся кучу приложения?
- Какой признак в динамике pidstat -r отличает реальную утечку от процесса, который просто занял память под кеш, и чем тут поможет memleak?
RSS врет из-за общих страниц - для честной картины бери PSS (smem -t или /proc/PID/smaps_rollup). Куда утекло, покажет pmap -x (ищи большие [ anon ] с Dirty) и smaps_rollup (Private_Dirty, Pss_Dirty, Anonymous). Сам факт утечки доказывается только динамикой: pidstat -r и монотонно растущий RSS, который не откатывается, плюс PSI и memory.current под cgroup v2. А чтобы выйти на конкретный стек кода - на 2026 это memleak из bcc прямо на проде. Кеш файлов утечкой не считается - его ядро отдаст под нагрузку, смотри available в free. Этого набора хватает, чтобы локализовать пожирателя памяти без отладчика и понять, в какую сторону копать дальше.