Śledzenie zużycia pamięci

 
Obecnie programiści dużo rzadziej zwracają uwagę na to, ile pamięci zużywa aplikacja, nad którą pracują. Komputery osobiste mają gigantyczne zasoby RAMu, spokojnie wystarczające do obsługi zasobochłonnych aplikacji, głównie gier. Pamięć wirtualna pozwala z kolei uwolnić się od zmartwień o pamięć fizyczną, ot najwyżej dysk będzie trochę chrobotał od czasu do czasu. Programiści z kolei mają dodatkowo do dyspozycji języki programowania, w których praktycznie wcale nie muszą się martwić o zarządzanie pamięcią. Problem poprawnej alokacji i dealokacji załatwiają przeróżne garbage collectory.
Są jednak takie obszary, w których programiści muszą się liczyć z ograniczonymi zasobami. Jednym z takich obszarów jest oprogramowanie serwerowe, szczególnie takie, które w założeniu musi być wysokowydajne, na przykład takie, które relizuje strumieniowanie multimediów. Aplikacja musi utrzymywać po swojej stronie znaczne bufory na ramki wideo dla poszczególnych plików/kanałów. Zbyt wielu użytkowników naraz i nagle pamięć fizyczna ulega wyczerpaniu, co doprowadza do swap’owania aktualnie nieużywanych stron pamięci na dysk. To z kolei powoduje olbrzymi spadek wydajności aplikacji i może doprowadzić do problemów z funkcjonowaniem usług.
Drugim obszarem są systemy wbudowane (embedded). Z reguły zasoby takich komputerów są mocno ograniczone i o rzędy wielkości mniejsze, niż zasoby komputerów osobistych. Mimo, że w wielu architekturach jednostka zarządzania pamięcią (MMU – Memory Management Unit) jest dostępna i wykorzystywana przez system, to może się okazać, że nie ma co liczyć na swap. Ale nierzadko MMU zwyczajnie nie ma, a pamięć fizyczna jest adresowana wprost. Wtedy zarządzanie pamięcią staje się jednym z kluczowych zadań – programista nagle zaczyna zwracać uwagę na takie subtelności, jak wielkość pliku wykonywalnego, wielkości bibliotek, rozmiary stosów, alokacje statyczne podczas inicjalizacji.
Niezależnie od charakteru tworzonej aplikacji, zawsze warto wiedzieć, jak sprawdzić zużycie pamięci przez proces, stąd kilka rad.

1. ps u

Dlaczego nie skorzystać z ps-a? Wyświetla sporo informacji o procesach, a z argumentem 'u’ dodatkowo podaje pewne informacje na temat zużycia pamięci.

x:~$ ps u
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
gs       15867  0.0  0.0  27908  3852 pts/0    Ss   kwi23   0:00 bash
gs       16506  0.0  0.0  27848  3772 pts/2    Ss   kwi23   0:00 bash
gs       23768  0.1  0.0 193460  3876 pts/0    Sl+  00:21   0:00 ./myApp config.xml
gs       23787  0.0  0.0  23864  1324 pts/2    R+   00:24   0:00 ps u

VSZ jest tutaj całkowitym zaalokowanym rozmiarem pamięci wirtualnej. Wielkości podawane są w kilobajtach. Widać, że myApp ma dość kosmiczne wymagania, a to głównie dlatego, że linkuje się z kilkunastoma wielkimi bibliotekami do przetwarzania multimediów. RSS określa tutaj zestaw roboczy stron, czyli wielkość fizycznej pamięci, zajmowanej aktualnie przez proces. VSZ można by rozumieć jako deklarację procesu odnośnie tego, ile miejsca mógłby zająć, gdyby chciał się zmieścić w RAMie w całości. RSS mówi ile pamięci fizycznej jest aktualnie w użyciu.
Po obciążeniu aplikacji myApp masą roboty uzyskamy całkiem inny obraz:

x:~$ ps u
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
gs       15867  0.0  0.0  27908  3852 pts/0    Ss   kwi23   0:00 bash
gs       16506  0.0  0.0  27848  3772 pts/2    Ss   kwi23   0:00 bash
gs       23768  4.4  1.2 796512 97512 pts/0    Sl+  00:21   0:17 ./myApp config.xml
gs       23912  0.0  0.0  27904  3840 pts/3    Ss   00:27   0:00 bash
gs       24058  0.0  0.0  23864  1328 pts/2    R+   00:28   0:00 ps u

VSZ podskoczył do prawie 800 MB, a RSS aż do 100 MB. Teraz zastanawiam się, czy mój program cały czas zużywał 100MB RAMu, czy może bywał bardziej zachłanny w trakcie swojego działania.

2. top

Co powie mi top? W zasadzie to samo, VIRT to inaczej VSZ, RES to RSS, a SHR to pamięć współdzielona z innymi procesami. Niewątpliwie zaletą jest tutaj stały podgląd zużycia zasobów, procesora i pamięci na tle innych procesów. Jeśli w aplikacji znalazłby się znaczny wyciek pamięci, to dałoby się to zaobserwować statytykach top-a.

top - 00:44:18 up 1 day, 12:01,  5 users,  load average: 0,52, 0,39, 0,40
Tasks: 266 total,   3 running, 263 sleeping,   0 stopped,   0 zombie
%Cpu(s):  7,9 us,  0,9 sy,  3,9 ni, 86,4 id,  0,8 wa,  0,0 hi,  0,0 si,  0,0 st
KiB Mem:   8058880 total,  7890664 used,   168216 free,    28060 buffers
KiB Swap:  8270844 total,    21360 used,  8249484 free,  5094536 cached

  PID USER      PR  NI  VIRT  RES  SHR S  %CPU %MEM    TIME+  COMMAND                     
24217 gs        20   0  777m  95m 4560 S  46,3  1,2   0:12.68 myApp                       
13357 gs        20   0 2251m 1,2g  47m S  12,6 15,8 107:58.76 firefox                     
 2253 gs        20   0 1539m 104m  33m S   9,3  1,3  25:26.43 compiz                      
21505 gs        20   0  755m  99m  29m R   9,3  1,3  27:04.70 plugin-containe             
16588 gs        20   0 1505m  88m  35m S   9,0  1,1   0:15.45 vlc                         
 1086 root      20   0  836m 310m 271m S   8,7  3,9  40:17.83 Xorg                        
15857 gs        20   0  645m  22m  12m S   4,7  0,3   0:09.70 gnome-terminal              
 1930 gs        20   0  430m  18m 2856 S   1,0  0,2   0:56.48 ibus-daemon                 
 1999 gs        20   0  638m  67m  11m S   0,7  0,9   8:05.16 unity-panel-ser             
 8137 gs        20   0 1237m  73m  26m S   0,7  0,9   8:31.72 codeblocks                  
23347 gs        20   0  756m  94m  55m S   0,7  1,2   0:28.35 spotify                     
23405 gs        20   0  887m  25m  13m S   0,7  0,3   0:12.06 SpotifyHelper               
   17 root      20   0     0    0    0 R   0,3  0,0   0:17.89 rcu_sched                   
   20 root      20   0     0    0    0 S   0,3  0,0   0:04.27 rcuos/2                     
  971 root      20   0  4376  708  532 S   0,3  0,0   0:57.07 acpid                       
 1957 gs        20   0  373m  33m 4968 S   0,3  0,4   1:00.42 hud-service

 3. /proc/<pid>/status

Naprawde przydatne informacje odnośnie pamięci kryją się w /proc/<pid>/status:

gs@gs-X750JB:~$ cat /proc/24217/status 
Name:	myApp
State:	S (sleeping)
Tgid:	24217
Pid:	24217
PPid:	15867
TracerPid:	0
Uid:	1000	1000	1000	1000
Gid:	1000	1000	1000	1000
FDSize:	256
Groups:	4 24 27 30 46 112 124 1000 
VmPeak:	  862048 kB
VmSize:	  792992 kB
VmLck:	       0 kB
VmPin:	       0 kB
VmHWM:	  102468 kB
VmRSS:	  100196 kB
VmData:	  696340 kB
VmStk:	     136 kB
VmExe:	     184 kB
VmLib:	   19896 kB
VmPTE:	     508 kB
VmSwap:	       0 kB
Threads:	9
SigQ:	0/62560
SigPnd:	0000000000000000
ShdPnd:	0000000000000000
SigBlk:	0000000000000000
SigIgn:	0000000000000000
SigCgt:	0000000180000000
CapInh:	0000000000000000
CapPrm:	0000000000000000
CapEff:	0000000000000000
CapBnd:	0000001fffffffff
Seccomp:	0
Cpus_allowed:	ff
Cpus_allowed_list:	0-7
Mems_allowed:	00000000,00000001
Mems_allowed_list:	0
voluntary_ctxt_switches:	12
nonvoluntary_ctxt_switches:	54

 

VmSize  jest znaną skądinąd wielkością pamięci wirtualnej zaalokowanej przez proces ale już VmPeak podaje szczytową wartość zaalkoowanej pamięci wirtualnej, co jest dużo przydatniejszą informacją. Podobnie sprawa wygląda z wielkością pamięci fizycznej wykorzystywanej przez proces, czyli VmRSS.  VmHWM jest szczytowym zużyciem pamięci fizycznej przez proces (HWM to skrót od high water mark co dosłownie oznacza najwyższy odczyt na wodowskazie, analogia dość celna). Mamy też dość bezużyteczne pola jak VmData i VmStk. VmStk podaje wielkość stosu wątku głównego, dość bezużyteczna informacja w przypadku aplikacji wielowątkowych. Z kolei VmData zmienia się proporcjonalnie do VmSize, ponieważ VmData jest sumą rozmiarów segmentów kodu (text), segmentów danych statycznych (bss) i segmentu danych (data) – zawierającego stertę oraz rozmiaru stosu. Wartość jest zawsze niższa niż VmSize ale bardziej wiarygodna, ponieważ VmSize podaje również wielkość stron mapowanych w pamięci wirtualnej ale jednocześnie nieużywalnych (PROT_NONE). Tak więc VmSize w zasadzie daje nam informację o całkowitym stopniu zużycia przestrzeni adresowej, podczas gdy VmData podaje ile z tej przestrzeni adresowej w ogóle może być użyte przez proces, ponieważ zawiera kod lub dane. Tutaj jest to dośc wyczerpująco opisane.
Jest jeszcze VmSwap, które podaje wielkośc pamięci wyswapowanej na dysk. Niestety nie ma tutaj informacji o szczytowej wielkości swap’u. Jednak gdyby zdarzyło nam się zaobserwować VmSwap > 0, to znaczy, że nie mieścimy się w fizycznej pamięci, co może być alarmujące.

4. /proc/<pid>/smaps

Tutaj znajdziemy mapę pamięci naszego procesu – wylistowane wszystkie obszary pamięci wirtualnej, jakie alokuje nasz proces. Do tych obszarów zaliczają się też te zaalokowane w bibliotekach współdzielonych. W moim przypadku każda z bibliotek, a jest ich kilkanaście, alokuje dodatkowe 2MB pamięci wirtualnej której nie używa i nigdy nie użyje (patrz VmSize wyżej) – jest to pewien mechanizm linkera, wyjaśniony tutaj, który ma zapewnić poprawne współdzielenie bibliotek między procesami. To niestety powoduje, że nawet małe zgrabne aplikacyjki, ale wykorzystujące bardzo wiele bibliotek dynamicznych, podczas działania pożerają gigantyczną częśc przestrzeni adresowej, mimo, że faktycznie nie zużywają nawet promila tego, co pokazuje VmSize.

7f41b13ce000-7f41b14c5000 r-xp 00000000 08:02 54003358                   /usr/lib/x86_64-linux-
gnu/libavformat.so.53.21.1
Size:                988 kB
Rss:                 560 kB
Pss:                 560 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:       560 kB
Private_Dirty:         0 kB
Referenced:          560 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd ex mr mw me ?? 
7f41b14c5000-7f41b16c4000 ---p 000f7000 08:02 54003358                   /usr/lib/x86_64-linux-
gnu/libavformat.so.53.21.1
Size:               2044 kB
Rss:                   0 kB
Pss:                   0 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:            0 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: mr mw me ?? 
7f41b16c4000-7f41b16ca000 r--p 000f6000 08:02 54003358                   /usr/lib/x86_64-linux-gnu/libavformat.so.53.21.1
Size:                 24 kB
Rss:                  24 kB
Pss:                  24 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:        24 kB
Referenced:           24 kB
Anonymous:            24 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd mr mw me ac ?? 
7f41b16ca000-7f41b16d7000 rw-p 000fc000 08:02 54003358                   /usr/lib/x86_64-linux-gnu/libavformat.so.53.21.1
Size:                 52 kB
Rss:                  52 kB
Pss:                  52 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:        52 kB
Referenced:           52 kB
Anonymous:            52 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd wr mr mw me ac ??
021b4000-021d5000 rw-p 00000000 00:00 0                                  [heap]
Size:                132 kB
Rss:                 132 kB
Pss:                 132 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:       132 kB
Referenced:          132 kB
Anonymous:           132 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd wr mr mw me ac ??

„VmFlags: rd ex” oznacza obszar pamięci z kodem wykonywalnym,  „VmFlags: rd” to dane tylko do odczytu, natomiast „VmFlags: rd wr” to pamięć swobodnie dostępna, czyli np. stos lub sterta. Przy czym sterta niekoniecznie musi mieć w wydruku z /proc/xxx/smaps etykietę [heap]. Większe alokacje, powyżej MMAP_THRESHOLD na pewno nie będą jej miały, a dodatkowo biblioteki współdzielone mogą implementować własne zarządzanie pamięcią, pomijając malloc/new i korzystając z własnych mechanizmów a następnie z mmap().

Widać tutaj też tę, zagadkową do niedawna, 2MB „dziurę” (2044 kB) w przestrzeni adresowej między obszarem kodu a obszarem danych.

Dzięki smaps możemy przekonać się, jaka jest struktura pamięci wirtualnej wykorzystywanej przez proces i skąd bierze się wartość VmSize i VmRSS. Możemy się dowiedzieć również, które obszary są aktualnie wyswapowane na dysk. Jednak przydałaby się możliwość stałego monitorowania zużycia pamięci w czasie. Są do tego niezliczone skrypty, w większości korzystające z danych w /proc/ . Zamiast jednak obserwować proces z zewnątrz, można skorzystać z profilera pamięci, czyli w pewnym sensie obserwować zjawiska zachodzące wewnątrz procesu.

5. Valgrind Massif

Valgrind jest aktualnie najbardziej wszechstronnym profilerem do debugowania problemów z pamięcią. Głównie załatwia się przy jego pomocy wycieki pamięci i mazanie po stercie (heap corruption) – na problemy ze stertą, szczególnie w oprogramowaniu wielowątkowym, naprawdę nie ma zbyt wielu sposobów, oprócz Valgrinda jest jeszcze dmalloc lub Electric Fence. Jednak nie nadają się one do monitorowania zużycia pamięci. Do tych zadań Valgrind ma narzędzie o nazwie massif. Domyślnie rejestruje ono wszelkie alokacje i dealokacje wywołane za pomocą malloc()/free() i new/delete, jednak można skłonić to narzędzie do rejestrowania na niższym poziome, czyli alokacje poprzez mmap/mremap/brk (–pages-as-heap=yes), które operują na całych stronach (4kB).

Po wywołaniu Valgrinda z Massifem dostajemy plik z próbkami, w zasadzie nieczytelny, stąd jak w przypadku innych profilerów mamy narzędzia do wizualizacji danych. Valgrind znacznie spowalnia naszą aplikację, w wielu przypadkach z aplikacji w ogóle nie da się korzystać podczas profilowania, co nie znaczy, że nie da się wyśledzić problemów i ich przyczyn.

x:~/y$ valgrind --tool=massif ./myApp config.xml 
==27213== Massif, a heap profiler
==27213== Copyright (C) 2003-2012, and GNU GPL'd, by Nicholas Nethercote
==27213== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==27213== Command: ./myApp config.xml
==27213== 
^C==27213== 
Killed

x:~/y$ ls -l mas*
-rw------- 1 gs gs 121053 kwi 23 11:19 massif.out.16475
-rw------- 1 gs gs 105778 kwi 23 11:20 massif.out.16490
-rw------- 1 gs gs  98409 kwi 24 11:42 massif.out.27213

x:~/y$ ms_print massif.out.27213 | less

--------------------------------------------------------------------------------
Command:            ./myApp config.xml
Massif arguments:   (none)
ms_print arguments: massif.out.27213
--------------------------------------------------------------------------------

    MB
109.7^                                                                      : 
     |     #:@::@@:::::::::::::::::@::::::::::::::::::::::::::::@::::::@::::::
     |     #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |   @@#:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |   @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |   @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |   @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |   @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |   @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     | @@@ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     | @ @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     | @ @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |@@ @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |@@ @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |@@ @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |@@ @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |@@ @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |@@ @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |@@ @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
     |@@ @ #:@::@ :: : ::::: ::::: @ : ::::: :::::: :::: : :::: @::::::@::::::
   0 +----------------------------------------------------------------------->Gi
     0                                                                   19.94

Number of snapshots: 68
 Detailed snapshots: [1, 2, 3, 4 (peak), 6, 9, 24, 49, 59]

--------------------------------------------------------------------------------
  n        time(i)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
--------------------------------------------------------------------------------
  0              0                0                0             0            0
  1    289,926,038       48,520,976       48,478,883        42,093            0
99.91% (48,478,883B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->79.26% (38,459,704B) 0x633CEB3: x264_malloc (in /usr/lib/x86_64-linux-gnu/libx264.so.123)
| ->16.55% (8,030,208B) 0x6332A78: x264_frame_pop_unused (in /usr/lib/x86_64-linux-gnu/libx264.so.123)
| | ->16.55% (8,030,208B) 0x63A9DB7: x264_encoder_open_123 (in /usr/lib/x86_64-linux-gnu/libx264.so.123)
(...)

Z narzędzia ms_print dostajemy nawet toporny ale czytelny wykresik, pokazujący zużycie pamięci w czasie, a niżej Valgrind próbuje wytknąć winowajców co większych alokacji. W pierwszej próbce za 79% zaalokowanej pamięci odpowiada enkoder x264, zresztą w kolejnych próbkach było podobnie. x264 po prostu jest żarłoczny.

A oto ładna wizualizacja wyników z massif’a, za pomocą massif-visualizer:

massif

Valgrind odpowie nam na wiele pytań, na które nie byłyby nam w stanie odpowiedzieć narzędzia z pkt. 1-4 – na przykłąd skąd się biorą poszczególne alokacje. Jednak korzystanie z niego jest dość skomplikowane, a do tego nie zawsze udaje się go zastsosować i pożera ogromne zasoby. Jednak zdecydowanie warto spróbować, nie tylko do szukania wycieków pamięci czy szkodliwego kodu pląsającego po stercie zanieczyszczając cudze bufory.

 

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *