Disclaimer. Заявления о том, что утечки памяти следует контролировать исключительно по значению VSZ, а RSS не возвращается в систему после удаления объектов, не верны или, по крайней мере, часто не верны. Вы не сможете контролировать утечки в случае, если в программе работает кастомный аллокатор, выделяющий память из заранее выделенного пула. В случае анализа RSS, пример в статье оказался банально некорректным, потому что для выделения физической памяти мало выделить ее функцией malloc(), нужно с ней еще что-нибудь сделать (например, вызвать memset(), спасибо моим коллегам за подсказку): в этом случае RSS будет реально выделяться и возвращаться в систему после удаления объектов. Я не буду удалять или редактировать эту статью, поскольку она демонстрирует технику работы с программой pidstat и опциями malloc и, кроме этого, является хорошим уроком мне самому, почему порой не следует делать слишком громких заявлений.
Я приведу небольшой пример, в котором будет показано различие в природе этих значений и возможные способы управления ими (по крайней мере, значением VSZ). Как ни странно, несмотря на очевидное смысловое различие между виртуальной памятью процесса (VSZ) и резидентной (или физической) памятью процесса (RSS), находятся люди, которые судят об утечке памяти в программе на основании роста (а иногда неосвобождения) последней. Обычно программист может контролировать только виртуальную память (собственно, адресное пространство, доступное в программе относится к виртуальной памяти). Управлением физической памятью занимается ядро операционной системы, и ее рост и неосвобождение могут быть связаны с особенностями реализации в нем (ядре) управления страницами физической памяти. Соответственно, рост RSS может лишь косвенно свидетельствовать о возможной утечке виртуальной памяти, а ее неосвобождение так и вообще ни о чем таком не свидетельствует, поскольку ядро может возвращать в систему аллоцированные процессу страницы когда ему вздумается. Единственный относительно надежный способ судить об утечке памяти в программе — это следить за величиной VSZ. Почему относительно надежный: ну, во-первых, VSZ включает адресные пространства разделяемых библиотек, которые тоже могут, в принципе, течь, а во-вторых …
И тут мы переходим к анонсированному примеру.
#include <unistd.h>
#include <iostream>
int main( void )
{
const size_t ncycles( 100 );
for ( int i( ncycles ); i > 0; --i )
{
int * a( new int[ 1024 * i ] );
std::cout << "Cycle " << ncycles - i + 1 <<
", addr: " << a << std::endl;
usleep( 500000 );
delete[] a;
}
return 0;
}
Простая программа на C++. В цикле выделяем большие куски памяти: сначала самые большие, затем все меньше и меньше. Внутри цикла устанавливаем задержку в пол-секунды для того, чтобы можно было удобно мониторить программу с помощью утилиты pidstat. Скомпилируем программу и дадим ей название malloc_test.
g++ -o malloc_test main.cc
Запустим программу и мониторинг в разных терминалах. Мы ожидаем, что VSZ будет постепенно уменьшаться, предположений о поведении RSS пока выдвигать не будем. А вот что показал pidstat на самом деле:
pidstat -r -p $(pgrep -x malloc_test) 2 30
Linux 3.14.5-200.fc20.x86_64 (localhost.localdomain) 10.06.2014 _x86_64_ (4 CPU)
18:53:44 UID PID minflt/s majflt/s VSZ RSS %MEM Command
18:53:46 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:53:48 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:53:50 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:53:52 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:53:54 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:53:56 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:53:58 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:54:00 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:54:02 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:54:04 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:54:06 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:54:08 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:54:10 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:54:12 1000 20532 2,00 0,00 13032 1152 0,01 malloc_test
18:54:14 1000 20532 2,00 0,00 13032 1412 0,02 malloc_test
18:54:16 1000 20532 2,00 0,00 13032 1412 0,02 malloc_test
18:54:18 1000 20532 2,00 0,00 13032 1412 0,02 malloc_test
18:54:20 1000 20532 2,00 0,00 13032 1412 0,02 malloc_test
18:54:22 1000 20532 2,00 0,00 13032 1412 0,02 malloc_test
18:54:24 1000 20532 2,00 0,00 13032 1412 0,02 malloc_test
18:54:26 1000 20532 2,00 0,00 13032 1412 0,02 malloc_test
18:54:28 1000 20532 2,00 0,00 13032 1412 0,02 malloc_test
18:54:30 1000 20532 2,00 0,00 13032 1412 0,02 malloc_test
18:54:32 1000 20532 2,00 0,00 13032 1412 0,02 malloc_test
Значение VSZ не падает, а RSS вообще растет. О, ужас! Неужели утечка памяти? Но программа-то вроде простая, явных ошибок в ней нет. Открываем man mallopt
и читаем о способах управления параметрами malloc. Комбинация M_TOP_PAD и M_TRIM_THRESHOLD выглядит заманчиво, листаем чуть ниже и узнаем, что их можно задать через переменные окружения MALLOC_TOP_PAD_ и MALLOC_TRIM_THRESHOLD_ без перекомпиляции программы! Зануляем эти значения и запускаем malloc_test снова.
MALLOC_TRIM_THRESHOLD_=0 MALLOC_TOP_PAD_=0 ./malloc_test
На этот раз вывод pidstat выглядит так:
19:07:18 UID PID minflt/s majflt/s VSZ RSS %MEM Command
19:07:20 1000 32224 2,00 0,00 12876 1164 0,01 malloc_test
19:07:22 1000 32224 2,00 0,00 12860 1164 0,01 malloc_test
19:07:24 1000 32224 2,00 0,00 12844 1164 0,01 malloc_test
19:07:26 1000 32224 2,00 0,00 12828 1164 0,01 malloc_test
19:07:28 1000 32224 2,00 0,00 12812 1164 0,01 malloc_test
19:07:30 1000 32224 2,00 0,00 12796 1164 0,01 malloc_test
19:07:32 1000 32224 2,00 0,00 12780 1164 0,01 malloc_test
19:07:34 1000 32224 1,99 0,00 12764 1164 0,01 malloc_test
19:07:36 1000 32224 2,00 0,00 12748 1164 0,01 malloc_test
19:07:38 1000 32224 2,00 0,00 12732 1164 0,01 malloc_test
19:07:40 1000 32224 2,00 0,00 12716 1164 0,01 malloc_test
19:07:42 1000 32224 2,00 0,00 12700 1164 0,01 malloc_test
19:07:44 1000 32224 2,00 0,00 12684 1164 0,01 malloc_test
19:07:46 1000 32224 2,00 0,00 12668 1164 0,01 malloc_test
19:07:48 1000 32224 2,00 0,00 12652 1164 0,01 malloc_test
19:07:50 1000 32224 2,00 0,00 12636 1164 0,01 malloc_test
19:07:52 1000 32224 4,00 0,00 12620 1180 0,01 malloc_test
19:07:54 1000 32224 2,00 0,00 12604 1180 0,01 malloc_test
19:07:56 1000 32224 2,00 0,00 12588 1180 0,01 malloc_test
19:07:58 1000 32224 2,00 0,00 12572 1180 0,01 malloc_test
19:08:00 1000 32224 2,00 0,00 12564 1180 0,01 malloc_test
19:08:02 1000 32224 2,00 0,00 12564 1180 0,01 malloc_test
19:08:04 1000 32224 2,00 0,00 12564 1180 0,01 malloc_test
Круто, объем виртуальной памяти уменьшается! Но RSS по-прежнему увеличивается скачком под конец работы программы. Давайте посмотрим на вывод malloc_test:
Cycle 1, addr: 0x7fbbeda72010
Cycle 2, addr: 0x7fbbeda73010
Cycle 3, addr: 0x7fbbeda74010
Cycle 4, addr: 0x7fbbeda75010
Cycle 5, addr: 0x7fbbeda76010
Cycle 6, addr: 0x7fbbeda77010
...
Cycle 65, addr: 0x7fbbedaea010
Cycle 66, addr: 0x7fbbedaeb010
Cycle 67, addr: 0x7fbbedaec010
Cycle 68, addr: 0x7fbbedaed010
Cycle 69, addr: 0x7fbbedaee010
Cycle 70, addr: 0x10e3010
Cycle 71, addr: 0x10e3010
Cycle 72, addr: 0x10e3010
Cycle 73, addr: 0x10e3010
...
Cycle 96, addr: 0x10e3010
Cycle 97, addr: 0x10e3010
Cycle 98, addr: 0x10e3010
Cycle 99, addr: 0x10e3010
Cycle 100, addr: 0x10e3010
Заметили качественный скачок в значениях адресов выделенной памяти на 70-ом шаге, то есть когда размер выделяемой виртуальной памяти стал меньше примерно 1024 x 30 x sizeof(int)
, то есть 128 килобайт? Давайте заново запустим программу и проследим карту ее памяти до и после скачка.
До:
cat /proc/$(pgrep -x malloc_test)/maps
00400000-00401000 r-xp 00000000 08:02 5907699 /home/lyokha/tmp/28/malloc_test
00600000-00601000 r--p 00000000 08:02 5907699 /home/lyokha/tmp/28/malloc_test
00601000-00602000 rw-p 00001000 08:02 5907699 /home/lyokha/tmp/28/malloc_test
3748800000-3748820000 r-xp 00000000 08:01 353673 /usr/lib64/ld-2.18.so
3748a1f000-3748a20000 r--p 0001f000 08:01 353673 /usr/lib64/ld-2.18.so
3748a20000-3748a21000 rw-p 00020000 08:01 353673 /usr/lib64/ld-2.18.so
3748a21000-3748a22000 rw-p 00000000 00:00 0
3748c00000-3748db4000 r-xp 00000000 08:01 353674 /usr/lib64/libc-2.18.so
3748db4000-3748fb4000 ---p 001b4000 08:01 353674 /usr/lib64/libc-2.18.so
3748fb4000-3748fb8000 r--p 001b4000 08:01 353674 /usr/lib64/libc-2.18.so
3748fb8000-3748fba000 rw-p 001b8000 08:01 353674 /usr/lib64/libc-2.18.so
3748fba000-3748fbf000 rw-p 00000000 00:00 0
3749800000-3749905000 r-xp 00000000 08:01 353677 /usr/lib64/libm-2.18.so
3749905000-3749b05000 ---p 00105000 08:01 353677 /usr/lib64/libm-2.18.so
3749b05000-3749b06000 r--p 00105000 08:01 353677 /usr/lib64/libm-2.18.so
3749b06000-3749b07000 rw-p 00106000 08:01 353677 /usr/lib64/libm-2.18.so
374a000000-374a015000 r-xp 00000000 08:01 353678 /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a015000-374a214000 ---p 00015000 08:01 353678 /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a214000-374a215000 r--p 00014000 08:01 353678 /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a215000-374a216000 rw-p 00015000 08:01 353678 /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374d400000-374d4e9000 r-xp 00000000 08:01 353679 /usr/lib64/libstdc++.so.6.0.19
374d4e9000-374d6e9000 ---p 000e9000 08:01 353679 /usr/lib64/libstdc++.so.6.0.19
374d6e9000-374d6f1000 r--p 000e9000 08:01 353679 /usr/lib64/libstdc++.so.6.0.19
374d6f1000-374d6f3000 rw-p 000f1000 08:01 353679 /usr/lib64/libstdc++.so.6.0.19
374d6f3000-374d708000 rw-p 00000000 00:00 0
7fad31adf000-7fad31b44000 rw-p 00000000 00:00 0
7fad31b77000-7fad31b79000 rw-p 00000000 00:00 0
7fff3eddb000-7fff3edfd000 rw-p 00000000 00:00 0 [stack]
7fff3edfe000-7fff3ee00000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
После:
00400000-00401000 r-xp 00000000 08:02 5907699 /home/lyokha/tmp/28/malloc_test
00600000-00601000 r--p 00000000 08:02 5907699 /home/lyokha/tmp/28/malloc_test
00601000-00602000 rw-p 00001000 08:02 5907699 /home/lyokha/tmp/28/malloc_test
01ba4000-01bbd000 rw-p 00000000 00:00 0 [heap]
3748800000-3748820000 r-xp 00000000 08:01 353673 /usr/lib64/ld-2.18.so
3748a1f000-3748a20000 r--p 0001f000 08:01 353673 /usr/lib64/ld-2.18.so
3748a20000-3748a21000 rw-p 00020000 08:01 353673 /usr/lib64/ld-2.18.so
3748a21000-3748a22000 rw-p 00000000 00:00 0
3748c00000-3748db4000 r-xp 00000000 08:01 353674 /usr/lib64/libc-2.18.so
3748db4000-3748fb4000 ---p 001b4000 08:01 353674 /usr/lib64/libc-2.18.so
3748fb4000-3748fb8000 r--p 001b4000 08:01 353674 /usr/lib64/libc-2.18.so
3748fb8000-3748fba000 rw-p 001b8000 08:01 353674 /usr/lib64/libc-2.18.so
3748fba000-3748fbf000 rw-p 00000000 00:00 0
3749800000-3749905000 r-xp 00000000 08:01 353677 /usr/lib64/libm-2.18.so
3749905000-3749b05000 ---p 00105000 08:01 353677 /usr/lib64/libm-2.18.so
3749b05000-3749b06000 r--p 00105000 08:01 353677 /usr/lib64/libm-2.18.so
3749b06000-3749b07000 rw-p 00106000 08:01 353677 /usr/lib64/libm-2.18.so
374a000000-374a015000 r-xp 00000000 08:01 353678 /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a015000-374a214000 ---p 00015000 08:01 353678 /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a214000-374a215000 r--p 00014000 08:01 353678 /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374a215000-374a216000 rw-p 00015000 08:01 353678 /usr/lib64/libgcc_s-4.8.2-20131212.so.1
374d400000-374d4e9000 r-xp 00000000 08:01 353679 /usr/lib64/libstdc++.so.6.0.19
374d4e9000-374d6e9000 ---p 000e9000 08:01 353679 /usr/lib64/libstdc++.so.6.0.19
374d6e9000-374d6f1000 r--p 000e9000 08:01 353679 /usr/lib64/libstdc++.so.6.0.19
374d6f1000-374d6f3000 rw-p 000f1000 08:01 353679 /usr/lib64/libstdc++.so.6.0.19
374d6f3000-374d708000 rw-p 00000000 00:00 0
7fad31b3f000-7fad31b44000 rw-p 00000000 00:00 0
7fad31b77000-7fad31b79000 rw-p 00000000 00:00 0
7fff3eddb000-7fff3edfd000 rw-p 00000000 00:00 0 [stack]
7fff3edfe000-7fff3ee00000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Видите в чем разница? После скачка появилась именованная секция [heap], в которой находится диапазон адресов, соответствующий адресам, выделенным программой начиная с 70-ого шага итерации (в данных выводах диапазоны немного отличаются, поскольку я запустил программу второй раз). В то же время адреса, которые выделялись до 70-ого шага, находятся в одной (а может быть в обеих) из двух неименованных секций перед секцией [stack]. Совершенно очевидно, что ядру пришлось выделить дополнительную страницу физической памяти для создания секции [heap] начиная с 70-ого шага: этим и объясняется скачок RSS.
Но почему же поменялась стратегия выделения памяти? Открываем man malloc
и читаем:
Обычно, malloc() распределяет память из кучи и подгоняет размер кучи
соответствующим образом с помощью sbrk(2). Если распределяемый блок памяти
больше чем MMAP_THRESHOLD байт, то реализация malloc() в glibc распределяет
память с помощью mmap(2) в виде частного анонимного отображения. По умолчанию,
значение MMAP_THRESHOLD равно 128 КБ, но его можно изменить с помощью
mallopt(3). На распределения, выполняемые с помощью mmap(2), не влияет
ограничитель ресурса RLIMIT_DATA (смотрите getrlimit(2)).
Вот и объяснение, откуда взялось значение 128 килобайт. И, кстати, значением MMAP_THRESHOLD тоже можно управлять через переменную окружения MALLOC_MMAP_THRESHOLD_, а значит в данном случае мы сможем остановить скачок RSS. Проверим.
MALLOC_TRIM_THRESHOLD_=0 MALLOC_TOP_PAD_=0 MALLOC_MMAP_THRESHOLD_=0 ./malloc_test
Вывод pidstat:
19:40:13 UID PID minflt/s majflt/s VSZ RSS %MEM Command
19:40:15 1000 28448 2,00 0,00 12876 1164 0,01 malloc_test
19:40:17 1000 28448 2,00 0,00 12860 1164 0,01 malloc_test
19:40:19 1000 28448 2,00 0,00 12844 1164 0,01 malloc_test
19:40:21 1000 28448 2,00 0,00 12828 1164 0,01 malloc_test
19:40:23 1000 28448 2,00 0,00 12812 1164 0,01 malloc_test
19:40:25 1000 28448 2,00 0,00 12796 1164 0,01 malloc_test
19:40:27 1000 28448 2,00 0,00 12780 1164 0,01 malloc_test
19:40:29 1000 28448 2,00 0,00 12764 1164 0,01 malloc_test
19:40:31 1000 28448 2,00 0,00 12748 1164 0,01 malloc_test
19:40:33 1000 28448 2,00 0,00 12732 1164 0,01 malloc_test
19:40:35 1000 28448 2,00 0,00 12716 1164 0,01 malloc_test
19:40:37 1000 28448 2,00 0,00 12700 1164 0,01 malloc_test
19:40:39 1000 28448 2,00 0,00 12684 1164 0,01 malloc_test
19:40:41 1000 28448 2,00 0,00 12668 1164 0,01 malloc_test
19:40:43 1000 28448 2,00 0,00 12652 1164 0,01 malloc_test
19:40:45 1000 28448 2,00 0,00 12636 1164 0,01 malloc_test
19:40:47 1000 28448 2,00 0,00 12620 1164 0,01 malloc_test
19:40:49 1000 28448 2,00 0,00 12604 1164 0,01 malloc_test
19:40:51 1000 28448 2,00 0,00 12588 1164 0,01 malloc_test
19:40:53 1000 28448 2,00 0,00 12572 1164 0,01 malloc_test
19:40:55 1000 28448 2,00 0,00 12556 1164 0,01 malloc_test
19:40:57 1000 28448 2,00 0,00 12540 1164 0,01 malloc_test
19:40:59 1000 28448 2,00 0,00 12524 1164 0,01 malloc_test
Работает! Объем RSS не увеличился, как мы и предположили. Этот последний пример хорошо демонстрирует природу RSS. Однако то, что нам удалось программно повлиять на динамику ее изменения — скорее исключение из правила, нежели правило. Программист не обязан контролировать величину RSS, поскольку ею управляет ядро операционной системы по своему усмотрению. Задача программиста — следить за изменением величины VSZ, однако и тут полно нюансов из-за вклада разделяемых библиотек и латентности при освобождении памяти в случае настройки malloc по умолчанию.