středa 3. února 2021

Počítání paměti procesů v Ubuntu

Občas se chci podívat, kolik místa dohromady zabírá aplikace, která neběží pouze v jednom procesu. Zejména webové prohlížeče dnes v RAM zabírají kvanta místa, ale protože již většina běží v mnoha procesech, není pomocí základních nástrojů na první pohled vidět, kolik paměti žerou jako celek. Když si to budete chtít spočítat, narazíte na to, že si musíte ujasnit, jaká data k tomu použijete a kde je vezmete, příkaz ps to nebude. Chtěl jsem top žebříček obsazení paměti podle příkazů, tedy posčítat dohromady procesy spuštěné stejným spustitelným souborem. Začal jsem původně psát jednoduchý skript v Bashi, pak začal zkoumat aplikaci smem, která oproti běžným nástrojům typu ps nabízí přesnější čísla a mnohem víc, nakonec jsem začal dopisovat samotný smem a přidal nejen požadovanou funkci tam. Při tom jsem si samozřejmě pár věcí ujasnil a zanechal stopu na GitHubu. V tomto zápisku to bude trocha teorie a příkazový řádek, v dalším samostatně smem, a jeho použítí, i třeba v conky. ...

Pojmy a dojmy

Při počítání množství procesy konzumované paměti na linuxovém systému, je třeba se nejprve zorientovat v tom, co je co, a není od věci pochopit princip fungování paměti v systému. Běžně se operuje s RSS a VSZ/VSS počítadly, ale to nám pro přesnější náhled do paměti systému nestačí. Do kernelu časem přibyly doplňující metriky, které ovšem příkazem ps nedostanete.

VSZ (jinak i VSS, Virtual Set Size) představuje celý virtuální adresní prostor procesu, do kterého spadá jak program, data a zásobník vlastního procesu, tak použité knihovny a soubory, bez ohledu na to, zda se data nachází v RAM, nebo na disku. Každý proces má vlastní virtuální adresní prostor, jehož maximální velikost omezuje šířka paměťové sběrnice (takže na 64 bit systému je de facto neomezená (řády EiB, tedy exabajtů), na 32 bitech jsou to pouze 4 GiB). Pouze v tomto prostoru proces operuje, nemůže vidět do adresních prostorů jiných procesů, nemá přímý přístup do fyzické paměti.

Jak virtuální paměť funguje, se můžete podívat i s vizualizací na třeba na YouTube zde, nebo zde, počíst si můžete třeba zde: https://tldp.org/LDP/tlk/mm/memory.html. Trochu to zkusím shrnout a nezabřednout do příliš velkých detailů. Virtuální adresní prostor je v podstatě tabulka, které se říká paměťová mapa (Memory Map). Ta popisuje, na jakých adresách se co nachází a kde to najít, ať je to v RAM, na disku, ve VideoRAM, … . Popisovat každou adresu zvlášť by bylo velmi neefektivní, proto je paměť mapována po větších celcích, tzv. stránkách, jejichž velikost je na x86 systémech 4 kiB (současné CPU umožňují použití i větších stránek, v linuxovém slovníku označovaných Huge Pages s velikostí typicky 2 MiB).

Virtuální adresní prostor je rozdělen na uživatelskou část (user space), která začíná od spodních adres, ta je k dispozici procesu, a oblast patřící kernelu (kernel space) okupující adresní prostor z druhé strany, ten všechny procesy sdílí ale nemají tam přístup. Součástí paměťové mapy je i tabulka stránek pro překlad na fyzickou paměť - Page Table, ta je v části vyhrazené kernelu. Tabulku stránek obsluhuje kernel a její struktura je dána hardwarovým návrhem systému, na kterém běží. Na x86 systémech (a nejen na nich) je součástí CPU jednotka MMU (Memory Management Unit), což je HW mezi jádrem procesoru a řadičem paměti, který se stará o překlad adres virtuálních na adresy fyzické paměti (RAM). CPU si vyžádá instrukci z virtuální adresy, MMU, se koukne do tabulky stránek, kde se fyzicky nachází v RAM a CPU si odtud instrukci načte. Může se ale stát, že obsah požadované adresy není v RAM, ale někde na disku, pak MMU vyhlásí chybu a kernel musí tuto skutečnost napravit, než konkrétní proces může pokračovat v běhu. Kernel se podívá, kde konkrétně se požadovaná stránka nachází, načte ji do paměti, zapíše do tabulky a může proces pustit dál. V zájmu zefektivnění tohoto procesu využívají kernel, i CPU své cache, kam si načítají předem data, která pravděpodobně budou brzy potřeba, aby minimalizovali zpoždění při komunikaci s pomalejšími jednotkami. Součástí MMU je buffer zvaný TLB (Translation Lookaside Table), který si pamatuje omezené množství posledních překladů a je o moc rychlejší, než překlad přes kompletní mapu v RAM, kam se MMU kouká, až když nepochodí v TLB. Ve skutečnosti je to na aktuálních CPU trochu komplikovanější, hw optimalizující překlad adres je složitější, ale princip tu zůstává.

Celková virtuální paměť přidělená procesům tedy může výrazně převyšovat velikost RAM a přitom se tam ve skutečnosti může všechno krásně vejít, díky sdílení a alokaci fyzické paměti pouze využívanými stránkami. Procesy mohou využívat stejnou knihovnu, ve svém virtuálním prostoru ji mohou mít mapovanou na různých adresách, ale do fyzické paměti budou adresy překládány na stejné místo. Je-li sdílená paměťová stránka označena jako zapisovatelná, ve chvíli, kdy se ji snaží nějaký proces modifikovat, dojde na fyzické straně ke zkopírování původního obsahu na jiné místo a přemapování (technika copy on write). Pro proces je tento akt neviditelný, jen se přístup do paměti zdrží o tuto operaci.

RSS (Resident Set Size) počítá pouze ty stránky z virtuálního adresního prostoru procesu, které jsou v danou chvíli mapovány na fyzickou paměť RAM. Nijak neřeší sdílení, takže v případě, že jednu stránku fyzické paměti sdílí 30 procesů (mají ji mapovánu ve svém virtuálním adresním prostoru), tedy v paměti je fyzicky jen jednou, RSS každého procesu ji bude celou počítat, tudíž ji ve výsledném součtu dostanete 29x navíc. To je velmi zavádějící a hodnota RSS je tudíž zajímavá pouze z hlediska posuzování jednoho konkrétního procesu.

Při alokování fyzické paměti v uživatelském prostoru je kernel velmi “zdrženlivý” - virtuální stránku na fyzickou paměť mapuje až ve chvíli, kdy je k ní přistupováno, když CPU vrátí chybu, že požadovanou adresu v mapě nenašel. Říká se tomu lazy allocation. Načítání paměťových stránek z disku po jedné, by bylo dosti neekonomické, proto kernel používá algoritmy, které čtení optimalizují a snaží se do cache ve fyzické paměti předem, asynchronně, načíst stránky, které by mohly být potřeba v nejbližší budoucnosti. Nakešované stránky může v případě potřeby jednoduše namapovat do virtuálního prostoru procesu, nebo naopak kdykoli zahodit a paměť uvolnit pro aktuálně potřebnější stránky. Tímto lazy přístupem je logicky přistupováno i ke stránkám již v paměti sdíleným, proto fakt, že je nějaká stránka už v paměti mapována pro jiný proces neznamená, že bude automaticky mapována všemi procesy, jenž mají stránku ve svém virtuálním prostoru, ale doposud ji nepoužily. Proto, když se podíváte na RSS hodnoty konkrétní sdílené knihovny v jednotlivých procesech, nedostanete stejná čísla, protože různé procesy mohou využívat různé stránky (různé funkce a data) knihovny.

VSZ a RSS hodnoty jsou sice teoreticky zajímavé, když víte, co přesně představují, ale víceméně jen v kontextu jednoho procesu, nedají se z nich dělat prakticky použitelné závěry ohledně většího celku. Linux proto s verzí 2.6.27 přišel se sofistikovanější metrikou pojmenovanou PSS (Proportional Set Size). Hodnota PSS se skládá z unikátních stránek paměti obsazených procesem (private clean, private dirty), plus sdílených stránek (shared) rozpočítaných na všechny procesy, které je aktuálně sdílejí. Sčítáním PSS jednotlivých procesů byste se již měli dostat blízko jejich reálnému otisku v RAM.

Matt Mackall, který PSS do Linuxu přinesl, přišel ještě s jednou užitečnou veličinou - USS (Unique Set Size), což je ta část obsazené RAM, která je unikátní pro daný proces a bude skutečně uvolněna po jeho ukončení v danou chvíli. Jen RAM, swap se nepočítá. Tudíž i když PSS pro daný proces ukazuje např. 746 kiB, jeho ukončením můžete získat pouze 296 kiB, protože hojně využívá částí knihoven, které sdílí nejméně jeden další proces. USS kernel nereportuje přímo, je to ale součet hodnot Private_Clean a Private_Dirty stránek, které ano. Zde by mělo platit, že pokud je sdílená (shared) stránka sdílena pouze jedním procesem, je považována za soukromou (private), tedy počítanou do USS.

V případě, že fyzická paměť přeci jen dojde, podívá se kernel po stránkách, ke kterým nebylo nejdéle přistupováno a uklidí je na disk. Jde-li o stránky, které byly po načtení do paměti modifikovány (tzv. dirty pages), zapíše je do Swapu (pokud je nějaký k dispozici), což je samostatný oddíl na disku, nebo jen soubor. Nemodifikované stránky (clean pges), které může kdykoli zpět načíst z původního souboru, jednoduše smaže, což je řádově rychlejší operace. Na uvolněné místo namapuje aktuálně požadované stránky a ty staré přemapuje na disk. Ve chvíli, kdy proces požaduje přístup k odklizeným stránkám, dojde opět k popsanému procesu, dříve odklizené stránky se vrátí do RAM a proces s výrazným zdržením pokračuje.

Linux umožňuje nastavit míru své tendence ke swapování (swappiness), čím vyšší má hodnotu, tím víc bude kernel odkládat méně frekventovaná data. To na jednu stranu může výkonově pomoct při náhlé potřebě velkého množství RAM pro nová data (je odloženo dopředu, nemusí se to řešit v tu chvíli), ale zpomaluje to samozřejmě přístup k těm odloženým.

Kernel počítá velikost Swap stejným způsobem, jako RSS, započítává všechny odložené stránky a neřeší sdílení. Vzhledem k tomu, že do swapu jdou pouze stránky, které nemají zdroj v souborovém systému a sdílených těchto anonymních stránek běžně nebývá tak masivní, nebývá také součet těchto hodnot všech procesů tak daleko od skutečnosti, jako RSS. Relativně nedávno se do kernelu dostala další metrika a to SwapPss, která případné sdílené stránky rozpočítá na zúčastněné procesy. Commit patche s touto novinkou jsem našel z roku 2015, nejsem si jist, ve které oficiální verzi kernelu se poprvé objevila.

Když už jsem u swapu, jeho obsah zahrnuje i cache, kterou kernel reportuje jako SwapCached. Pokud kernel něco odswapuje a následně to vrátí zpět do RAM, nemaže to hned, protože pokud se ten kus paměti nezmění a bude potřeba ho opět odswapovat, má už práci hotovou a může pouze smazat příslušnou část RAM, což operaci velmi zefektivní.

Kam pro data

Kernel dává informace o využití paměti skrze pseudo filesystém /proc, tak se tam podíváme.

Přehled obsazení paměti celého systému:

$ cat /proc/meminfo

MemTotal:        8081136 kB
MemFree:          480980 kB
MemAvailable:    1572560 kB
Buffers:          211416 kB
Cached:          1344772 kB
SwapCached:       184320 kB
Active:          5459316 kB
Inactive:        1176880 kB
Active(anon):    4628268 kB
Inactive(anon):   785236 kB
Active(file):     831048 kB
Inactive(file):   391644 kB
Unevictable:         164 kB
Mlocked:             144 kB
SwapTotal:      13468668 kB
SwapFree:       11335064 kB
Dirty:              2256 kB
Writeback:             0 kB
AnonPages:       5062140 kB
Mapped:           545300 kB
Shmem:            333992 kB
KReclaimable:     172536 kB
Slab:             336728 kB
SReclaimable:     172536 kB
SUnreclaim:       164192 kB
KernelStack:       18152 kB
PageTables:        50668 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    17509236 kB
Committed_AS:   15747952 kB
VmallocTotal:   34359738367 kB
VmallocUsed:       48684 kB
VmallocChunk:          0 kB
Percpu:             4288 kB
HardwareCorrupted:     0 kB
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
FileHugePages:         0 kB
FilePmdMapped:         0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB
DirectMap4k:     2057036 kB
DirectMap2M:     6262784 kB

Hned na začátku je třeba zmínit, že ačkoli jsou ve výpisu kB, jde ve skutečnosti o binární variantu kiB, vývojáři to nechali v původním stavu kvůli tomu, aby se nerozbily aplikace, které třeba parsují výstup s využitím tohoto řetězce. To platí pro všechny další zmíněné soubory.

V dokumentaci /proc fs: https://www.kernel.org/doc/html/latest/filesystems/proc.html je vysvětlen význam jednotlivých položek. Zajímavý rozbor je i na Pythianu. Pro refernci zdrojový kód meminfo.c.
Hned na začátku výpisu vidíte základní informace, které dostanete příkazem free, nebo vmstat.

  • MemTotal: je celkové množství RAM, které zbylo po alokaci paměti hardwarem (např. i integrované grafiky) a natažení kernelu po bootu. Rozdíl mezi celkovou velikostí RAM a MemTotal nikdy nebude z uživatelského prostoru dostupný, proto je zbytečné ho do celkové hodnoty počítat. Na mém systému mi z 8 GiB zbývá 7,7 GiB
  • MemFree: je RAM, která je momentálně neobsazená
  • MemoryAvailable: odhad RAM dostupné pro nové procesy, než dojde ke swapování (od Linuxu 3.14). Může bý větší, než MemFree, protože kernel může zahodit něco z aktuální cache, ale naopak z toho je odečtena hodnota z /proc/sys/vm/min_free_kbytes určující minimální množství paměti, které se kernel snaží udržet volné pro potřebu další alokace. Je to o tom, že kernel může v případě potřeby alokovat paměť hned a pak teprve, asynchronně, řešit, co kam odloží, aby tu paměť uvolnil.
  • Buffers: je cache disku, obsahuje aktivní ukazatele na data v souborovém systému
  • Cached: je cache stránek souborů a to všech, se kterými pracují procesy, nebo jsou v RAM z jiného důvodu. Nejsou to pouze stránky momentálně opuštěné, ale i ty, které jsou namapované procesy a pracuje se s nimi. Spadají do toho i soubory v tmpfs, (dočasný souborový systém v RAM) a ramfs (ramdisky), tudíž je (poměrně rozšířený) omyl, myslet si, že když přikážete kernelu, ať zahodí cache (echo 1 > /proc/sys/vm/drop_caches), *Cached* v MemInfu klesne k nule. Nemůže zahodit např. právě soubory těchto dočasných fs, pokud je nesmažete. S ramfs soubory nehne, ty žádnou keší nejsou, tmpfs může odswapovat. Mapped, tmpfs/ramfs a Shmem by měly být podmnožinou Cached. Zajímavá diskuze vývojářů na téma Cached, kde je vidět kousek zdrojáku, který ukazuje výpočet. V diskuzi šlo o patch, který by stávající Cached nahradil součtem hodnot Active/Inactive(file) s odečtením Buffers. Což neprošlo kvůli obavě o rozhození algoritmů aplikací v userspace, které s touto hodnotou kalkulují a přizpůsobily se tomu, že neobsahuje pouze potenciálně postradatelné stránky. Výňatek ze zrojáku meminfo.c:
    cached = global_node_page_state(NR_FILE_PAGES)
            - total_swapcache_pages() - i.bufferram;
    if (cached < 0)
            cached = 0;
  • Active/Inactive: Active je paměť, která byla nedávno použita a uvolňována bude jen v případě nejvyšší nouze, kdežto Inactive nebyla nějakou dobu použita a půjde první na odstřel/přesun, pokud bude třeba uvolnit místo. Součet těchto dvou položek dává celkovou velikost paměti spadající do správy algoritmu pro výměnu stránek mezi diskem a RAM.
  • Active/Inactive (annon/file) specifikují, jaká část z předchozích jsou anonymní stránky a jaká soubory. Active(file)+Inactive(file) zhruba odpovídají Cached-Shmem a jak jsem zmínil, byla tu snaha nahradit Cached touto sumou.
  • AnonPages: paměť mapována procesy z uživatelského prostoru, která není podložena soubory. Spolu s Mapped dává celkovou, procesy obsazenou paměť.
  • Mapped: paměť zabraná stránkami souborů (jako např. knihoven) mapovanými z uživatelského prostoru. Po troše pátrání jsem si ujasnil, že Mapped by měla být podmnožinou Cached, protože má jít pouze o stránky, které jsou mapované na fyzickou paměť, a soubor se nejprve musí dostat do té paměti, tedy nakešovat, aby mohl být takto namapován. A pak jsem si spustil VirtualBox a koukám, že Mapped je najednou 5x větší, než Cached! Dalším průzkumem jsem zjistil, že proces VirtualBoxu má v paměti ten hledaný rozdíl mapovaný z /dev/zero. Je celkem zřejmé, že tohle není třeba kešovat. VBox používá mapování z /dev/zero k alokaci a okamžité aktivaci paměti pro hostovaný systém (paměť je rovnou zapsána nulami a tudíž alokována ve fyzické paměti). Alokuje to po částech, proto má /dev/zero/ otevřený mnohokrát, v řádech stovek až spíš tisíců. Pointa je v tom, že takto mapovaná paměť by měla být počítána do AnonPages, tedy nepodložených souborem, protože /dev/zero žádným souborem ani ve skutečnosti není. Je to problém, pokoušíte-li se vyčíslit část Cached, která může být okamžitě uvolněna (reclaimable). Někde jsem našel mistifikaci, že do Mapped spadají všechny mapované stránky, včetně anonymních, tak tady je výňatek ze zdrojového kódu meminfo.c: 
    show_val_kb(m, "AnonPages: ", global_node_page_state(NR_ANON_MAPPED));
    show_val_kb(m, "Mapped: ", global_node_page_state(NR_FILE_MAPPED));

  • Shmem: celková velikost sdílených stránek a tmpfs. SysV ipc část se dá prozkoumat příkazem ipcs, tmpfs příkazem df -t tmpfs, zbytek jsou sdílené soubory procesů, které se dají odhalit skriptem, který jsem našel na StackExchange a dal jsm si ho na svůj GitHub(“Shmem” includes only data, not metadata, nor memory currently swapped out. But it includes tmpfs memory, SysV shared memory (from ipc/shm.c),
    POSIX shared memory (under /dev/shm (což je tmpfs)), and shared anonymous mappings
    (from mmap of /dev/zero with MAP_SHARED: see call to shmem_zero_setup()
    from drivers/char/mem.c): whatever allocates pages through mm/shmem.c.)
    (zdroj), Nějaká okumentace.
  • SwapCached: paměť, která již byla jednou odswapována a následně vrácena do RAM, ale zústává i ve swapu pro urychlení dalšího případného swapování.
  • SwapTotal:/SwapFree: celková dostupná velikost / neobsazená část swapu, včetně SwapCache. Porci, kterou z toho zabírají aktuálně odswapované stránky, si musíte spočítat SwapTotal - SwapFree - SwapCached
  • Unevictable: Celková paměť, kterou kód pageout funkce shledal jako neodsunutelnou z paměti. Spadá sem vše z Mlocked, ale i třeba obsah ramdisků.
  • Mlocked: Neodsunutelná paměť, která byla zamknuta uživatelskými procesy.
  • Dirty: paměť, která potřebuje zapsat na disk, typicky vzroste při kopírování souborů mezi disky, kdy se načítají soubory do cache a pak postupně zapisují na cílový disk.
  • Writeback: paměť právě zapisovaná na disk
  • Slab: kontinuální části paměti v kernel space využívající slab alokaci a používaná jako cache pro velké množství malých objektů stejné velikosti, které se neustále alokují a zase odalokovávají. Tyto objekty vytváří kernel a jeho moduly při obsluze procesů. Podrobnější data o obsahu slabu najdete v adresáři /proc/slabinfo, ale sofistikovanější výstup vám nabídne aplikace slabtop, kde aktivitu slabů můžete sledovat v čase. O slab alokaci si můžete základy přečíst třeba na Wiki, o implementaci zde.
  • SReclaimable: část slabu, zahrnující cache, která může být uvolněna při nedostatku paměti. Slab se kvůli vyššímu výkonu nezaobírá odalokací objektů, naopak využívá toho, že když budou znovu potřeba, má hotovo. Umí ale podle potřeby tuto paměť uvolnit.
  • SUnreclaim: část slabu, která nemůže být uvolněna při nedostatku paměti, kernel ji nutně potřebuje k zajištění chodu systému
  • KReclaimable: celková paměť alokovaná kernelem, kterou by v případě velké nouze uvolnil. Spadá sem i SReclaimable
  • PageTables: paměť obsazená nejnižší úrovní tabulek paměťových stránek
  • KernelStack: zásobník kernelu
  • Commited_AS: součet všech alokací provedených běžícími procesy ve virtuálním adresním prostoru, tedy to, co si naporoučely, nikoliv nutně to, co je aktuálně rezervováno v RAM/Swapu.

Souhrn základních informací o konkrétním procesu - status

$ cat /proc/<pid>/status

Name:  firefox
Umask: 0002  
State: S (sleeping)  
Tgid:  2544588  
Ngid:  0  
Pid:   2544588  
PPid:  2967  
TracerPid: 0  
Uid:   1000 1000 1000 1000  
Gid:   1000 1000 1000 1000  
FDSize: 1024  
Groups: 4 24 27 30 46 107 120 131 132 137 1000  
NStgid: 2544588  
NSpid:  2544588  
NSpgid: 497091  
NSsid:  497091  
VmPeak:  19237280 kB  
VmSize:   5146164 kB  
VmLck:          0 kB  
VmPin:          0 kB  
VmHWM:    1608432 kB  
VmRSS:     706828 kB  
RssAnon:         552128 kB  
RssFile:          61560 kB  
RssShmem:         93140 kB  
VmData:   1403780 kB  
VmStk:        148 kB  
VmExe:        572 kB  
VmLib:     153604 kB  
VmPTE:       5548 kB  
VmSwap:    108140 kB  
HugetlbPages:         0 kB  
CoreDumping: 0  
THP_enabled: 1  
Threads:    84  
...

Zde najdete všechny základní informace o daném procesu, včetně jména, masky, stavu, počtu vláken, … . Co se paměti týká, dáte z toho dohromady pouze VSZ a RSS a hodnoty RssAnon/File/Shmem jsou to spíše orientační:

  • VmData - velikost datové části virtuální paměti
  • VmStk - velikost zásobníku procesu
  • VmExe - velikost textové části (kód programu)
  • VmLib - velikost kódu sdílených knihoven
  • VmPte - velikost tabulky stránek (Page Table Entries)
  • VmPeak - maximální dosažená velikost VSS
  • VmHWM: - maximální dosažená velikost RSS
  • VmRss: - aktuální velikost mapované fyzické paměti
  • RssAnon: - mapovaná paměť, nemající za sebou soubor (až od Linuxu 4.5, březen 2016)
  • RssFile: - v paměti mapované soubory (až od Linuxu 4.5)
  • RssShmem: - sdílená paměť - anonymní stránky a sdílené soubory (až od Linuxu 4.5)

Zmíněné dva soubory jsou v Ubuntu přístupné z celého systému, nepotřebujete k přístupu zvýšená práva, proto můžete jako obyčejný uživatel získat základní informace i o procesech roota. Status využívá například příkaz ps.

Detailní výpis procesem mapované paměti - smaps

# cat /proc/<pid>/smaps  

...  
7f36a1694000-7f36a16a5000 r--p 00797000 08:12 1843109 /usr/lib/x86_64-linux-gnu/libgtk-3.so.0.2404.16
Size:             68 kB
KernelPageSize:    4 kB  
MMUPageSize:       4 kB  
Rss:              40 kB  
Pss:              40 kB
Shared_Clean:      0 kB
Shared_Dirty:      0 kB
Private_Clean:    40 kB
Private_Dirty:     0 kB
Referenced:       16 kB  
Anonymous:        40 kB 
LazyFree:          0 kB
AnonHugePages:     0 kB
ShmemPmdMapped:    0 kB
FilePmdMapped:     0 kB
Shared_Hugetlb:    0 kB
Private_Hugetlb:   0 kB
Swap:             28 kB
SwapPss:          28 kB
Locked:            0 kB
THPeligible: 0
VmFlags: rd mr mw me ac sd
...

Soubor smaps je rozšířením nad /proc/<pid>/maps a najdete zde souhrny všech stránek procesu mapovaných na fyzickou paměť. Právě zde najdete hodnoty Pss, nověji i SwapPSS a sumarizace čistých (Clean) a špinavých (Dirty), soukromých (Private), i sdílených (Shared) stránek, ze kterých spočítáte USS. Výpis se skládá z mnoha kusů paměti, jednotlivé mapované soubory jsou dělené i podle nastavení práv ke konkrétním stránkám, takže pokud z toho chcete dostat nějaké souhrnné informace, musíte parsovat a počítat. smaps soubory jsou už ale dostupné pouze vlastníkovi procesu, nebo rootovi.

Souhrnný výpis procesem mapované paměti - smaps_rollup

Kernel od verze 4.14 (teprve listopad 2017) nabízí soubor smaps_rollup, který sumarizuje data z smaps v jednom bloku označeném jménem [rollup]. Pokud potřebujete celkové sumy, je to daleko rychlejší možnost, než počítat obsah smaps. Patch, který rollup přináší, si prosadili vývojáři Androidu, kteří podle PSS vyhodnocují chování aplikací a optimalizují práci s pamětí - zpracování smaps_rollup (podle mých měření) je cca 7-11x rychlejší, než smaps. Výstup zachovává stejnou strukturu, jako smaps, aby se dal zpracovávat stejným způsobem, nejsou tu však všechny položky a jednou z nich je Size: představující velikost virtuální paměti VSZ. V případě potřeby si můžete hodnotu doplnit z výše zmíněného souboru status, kde je značena jako VmSize:.

# cat /proc/<pid>/smaps_rollup 

2a1c00000-7fffbc970000 ---p 00000000 00:00 0 [rollup]  
Rss:               573680 kB  
Pss:               478843 kB  
Pss_Anon:          421280 kB  
Pss_File:           14926 kB  
Pss_Shmem:          42636 kB  
Shared_Clean:       60192 kB  
Shared_Dirty:       85704 kB  
Private_Clean:      41496 kB  
Private_Dirty:     386288 kB  
Referenced:        515536 kB  
Anonymous:         421280 kB  
LazyFree:               0 kB  
AnonHugePages:          0 kB  
ShmemPmdMapped:         0 kB  
FilePmdMapped:          0 kB  
Shared_Hugetlb:         0 kB  
Private_Hugetlb:        0 kB  
Swap:              137172 kB  
SwapPss:           106460 kB  
Locked:                 0 kB

V novějších kernelech (od 4.5 cca) k PSS přibyly i detaily, jako u RSS, tedy PssAnon, PssFile a PssShmem, význam viz výše, jen rozpočítané.

Výpis všech procesem mapovaných souborů

Jak jsem zmínil, v smaps najdete všechny procesem mapované soubory, jsou rozdělené na bloky podle nastavených flagů, které určují, o jaká data jde a jaký je k nim povolen přístup (čtení/zápis/spustitelnost) takže je třeba to profiltrovat, takto se např. dostanete k seznamu cest k mapovaným souborům:

# awk '/\/[^:]+$/{print $6}' /proc/<pid>/smaps|sort|uniq

/dev/dri/card0  
/home/gdh/.cache/fontconfig/a41116dafaf8b233ac2c61cb73f2ea5f-le64.cache-7  
/home/gdh/.cache/fontconfig/fb6e8f9b-6f0e-4b7f-bd48-a7661c04e6d1-le64.cache-7  
/home/gdh/.local/share/mime/mime.cache  
/home/gdh/.mozilla/firefox/uvpbrpq2.default-1513020500694/extensions/firefox@ghostery.com.xpi  
/tmp/Temp-3cb70754-fbc3-4a68-a50f-4cd170afdf62/mesa_shader_cache/index  
/usr/lib/firefox/browser/omni.ja  
/usr/lib/firefox/firefox  
/usr/lib/firefox/fonts/TwemojiMozilla.ttf  
/usr/lib/firefox/libfreeblpriv3.so  
/usr/lib/firefox/liblgpllibs.so  
...  

Výsledkem je pouze seznam mapovaných souborů, pokud byste chtěli podrobnosti, včetně nastavení příznaků a adresy jednotlivých bloků, máte to nejjednodušší pomocí příkazu:

pmap <pid>

Výpis potřebných knihoven neběžícího programu

Když už jsem u souborů, mezi kterými je většina knihovna, chcete-li zjistit, na jakých je závislý ještě nespuštěný kompilovaný program, můžete použít příkaz ldd, např.:

$ ldd /usr/bin/grep

linux-vdso.so.1 (0x00007ffd4654a000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f6d380f9000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f6d380f3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6d37f01000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f6d37ede000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6d381bb000)

Knihovny jsou vypsány i s aktuálními cestami v systému, základní konfigurace cest, kde se dynamické knihovny hledají, je v /etc/ld.so.conf resp. souborech v /etc/ld.so.conf.d/. Nastavit jiné priority lze přes systémovou proměnnou LD_LIBRARY_PATH. A když už jsme u toho, další proměnnou LD_PRELOAD můžete před spuštěním programu nastavit cestu k jedné konkrétní knihovně, která nahradí tu systémovou.

Příkaz, kterým byl proces spuštěn

V souboru status sice najdete i jméno procesu (najdete ho i samostatně v souboru /proc/<pid>/comm), výchozím je jméno spustitelného souboru, ale proces si ho může libovolně změnit (do 15 znaků), například Firefox má jeden proces jménem firefox, dalších x Web Content a zakončuje to Web Extensions. Pokud si budete chtít změnit jméno svého bash skriptu, stačí v něm na začátku zapsat do souboru comm a místo <pid> použít kouzelné slovíčko self:

echo jméno > /proc/self/comm

To jsem ale odbočil, příkaz, kterým byl proces spuštěn, získáte takto:

$ cat /proc/<pid>/cmdline|tr '\0' ' '

Jednotlivé argumenty příkazu jsou odděleny nulou, proto je nahrazuji mezerami pomocí příkazu tr.

Vyfiltrování PID procesů řetězcem z příkazu

Pokud budete potřebovat získat PID procesu vyhledáváním řetězce v příkazech, můžete na to jít třeba takto

$ ls /proc|egrep '[0-9]+'|while read pid; do [[ "$(cat /proc/$pid/cmdline)" =~ "hledaný_řetězec" ]] && echo $pid; done 2>/dev/null

Jako cvičení pěkné, ale nejefektivnější to není, na to přeci máme specializované nástroje:

$ pgrep -f firefox  

Nooo, elegantní to je, 150 ms je lepších, než předchozích 750, ale co to zkusit takto:

$ ps ax|awk '/[f]irefox/{print $1}'

Tento příklad dokazuje, že co je kratší zápisem, nemusí být rychlejší, filtrování ps přes rouru je s 20 ms 7,5x rychlejší, než pgrep samotný. Tak to jen takové zpestření, vždy hledám tu nejrychlejší variantu, proto jsem si také do promptu integroval časomíru (viz minulý blog), abych stále nemusel psát příkaz time. Ty hranaté závorky kolem f jsou regulárním výrazem pro výčet znaků, které se na tom místě mohou vyskytovat, dal jsem je tam proto, aby příkaz nenašel sám sebe.

Základní počty s pamětí

Pár úvah na úvod

Pokud počítáte sumu hodnot USS nějaké vybrané skupiny procesů, výsledek nemusí odpovídat realitě, neboť v ní nebude zahrnuta paměť sdílená pouze procesy té skupiny (pokud taková je), která také po ukončení těchto procesů bude uvolněna. Zde je třeba přinejmenším identifikovat knihovny unikátní pro tuto skupinu a ty odečíst celé, resp. jejich souhrnné PSS. Ale kdo by se s tím patlal…

Celkovou, skutečně využitou virtuální paměť, kterou proces okupuje, tedy RAM, i Swap, dostanete nejblíže součtem Pss a SwapPss. Nazval jsem to TPss, jako Total Pss.

Mezi procesy v /proc/ najdete i vlákna kernelu, ta se poznají i podle toho, že mají prázdné smaps soubory, i cmdline, ty nespočítáte.

Občas je rychlost kritická, například pokud budete psát nástroj typu top, potřebujete poměrně rychle vyhodnocovat aktuální stav. Zpracovat smaps a spočítat potřebné položky v celém systému je časově náročné, zvláště na slabších počítadlech. Do tohoto času je nutné počítat i samotné čtení dat, soubory v adresáři /proc nejsou statické položky, kernel při čtení z nich data posbírá a zpracuje, než vám je předá.
Nejjednodušší to má kernel s daty pro status, přiřazení obsahu do proměnné v Bashi na mém Core i5 3. generace jsou to cca 2 ms u procesu Firefoxu mapujícího 1,3 GB paměti.
Čtení smaps je v tomto případě řádově pomalejší především vzhledem k množství předávaných dat (93265 řádků, to jsou megabajty), ale něco udělá i zpracování. Přiřazení dat z smaps stejného procesu do proměnné již trvá kolem 35 ms.
Při čtení z smaps_rollup vygeneruje kernel data pro smaps, všechno sečte a vyplivne souhrn na pár řádků, přesto/proto je v tomto případě se 17 ms dvakrát rychlejší, než smaps a ještě vám ušetří čas zpracování.

Kolik a čím okupuje RAM kernel a user space

Vezmu výše zmíněné položky souboru MemInfo reportované kernelem a zkusím je dát dohromady do celkového obrazu RAM.

Userspace obsahuje:

  • AnnonPages
  • Mapped

Vzorec je jednoduchý (počítám pouze to, co je v RAM, swap a disk v tom nejsou) :
User space total = AnnonPages + Mapped
Pokud si posčítáte PSS všech uživatelských procesů, měli byste se na tuto hodnoru dostat, to mi sedí.

Kernel space:

Tady v podstatě stačí od MemTotal odečíst User space total, ale z čeho se vlastně skládá?
Samotný kód kernelu je nad MemTotal, ale pokud se budete snažit vyčíslit jeho otisk v RAM, je údajně třeba příkazem size zjistit velikost kódu rozbaleného kernelu. V Ubuntu se používá komprimát, což poznáte podle toho, že jeho jméno začíná vmlimuz (je v /boot adresáři) nekomprimovaný by se podle zvyklostí jmenoval vmlinux. Můj rozbalený Linux 5.8.0-40 má velikost kódu 33,4 MiB (pžíkaz size), na disku zabírá 43 MiB, a v dmesg povídá sám kernel:

$ grep Memory: /var/log/dmesg
[    0.049831] kernel: Memory: 7797912K/8319428K available (14339K kernel code, 2563K rwdata, 5356K rodata, 2636K init, 4900K bss, 521516K reserved, 0K cma-reserved)

Nemám v tom úplně jasno, abych pravdu řekl, nicméně je to v část nad MemTotal, což je oblast po bootu pevně daná. Jinak součástí každého balíku s kernelem je skript na jeho rozbalení, zkuste ho najít locate extract-vmlinux. Skriptu dáte jako argument cestu k zabalenému kernelu, on to rozbalí do /tmp/, načež to přes cat sype na stdout, takže stačí výstup přesměrovat do souboru, kam to chcete uložit.

Takže k tomu obsahu paměti kernelem dále okupovanému:

  • Moduly natažené kernelem - beru reportovanou velikost z /proc/modules na disku mají o poznání menší velikost, protože ty malé mrchy nemohou dostat méně, než stránku a nějakou další stránku prý dostanou navíc. U mě to třeba na 162 modulech dělá kolem 10 MiB velikost kódu a z /proc/modules kolem 15 MiB, nejmenší velikost modulu je totiž 4 stránky, tedy 16 kiB.
  • Slab
  • KernelStack
  • PageTables
  • Active(File)
  • Inactive(File)
  • ramfs 
  • Buffers
  • Shmem mínus sdílené stránky procesů

Bohužel se nedá jednoduše přesně určit, co s čím v MemInfo sčítat a odečítat, jednotlivé položky se různě prolínají, něco jsou ze strany kernelu víc odhady, než přesná čísla. Stále mi někde většinou zbývá pár stovek MiB, ve kterých jsem si zatím neudělal jasno.

Celková procesy fyzicky obsazená RAM - PSS:

# awk '/^Pss:/{sum += $2} END {print sum}' /proc/*/smaps

Nezapomeňte, že všechny procesy v systému spočítáte pouze s právy roota, jinak dostanete sumu pouze pro aktuálního uživatele (krom hlášek o nedostatečném oprávnění, které můžete přesměrovat do /dev/null). Pss v příkazu můžete vyměnit, za Rss, Swap, SwapPss, atd. Hvězdičku zas můžete vyměnit za konkrétní PID požadovaného procesu.

Tak kolik tedy žere ten Firefox celkem?

# ps ax | egrep '[f]irefox' | awk '{print $1}' | xargs -I '%' grep 'Pss:' /proc/%/smaps | awk '{sum += $2} END {print sum}'

Tento jednořádkový skript vyplivne aktuální součet PSS a SwapPSS všech procesů, které mají v příkaze, kterým se spouštěly, řetězec “firefox”. V kiB. Hranaté závorky jsou opět použity pro eliminaci vlastního procesu ze selekce, případně můžete řetězec upřesnit, např. na lib/firefox/[f]irefox (pokud to odpovídá vaší realitě). Máte-li aktuální kernel, vyměníte v příkazu smaps za smaps_rollup, čímž výsledek dostanete o poznání rychleji (na mém systému a 4 GB Firefoxu to bylo zhruba 200 ms vs 110 ms). Při filtrování hodnot Pss a SwapPss jsem využil toho, že obě položky obsahují “Pss:” a žádné další nežádoucí řádky do toho nespadají, jinak bych použil regulární výraz, kterým bych zajistil, že se řetězce budou hledat samostatně a pouze na začátku řádku '(^Pss: |^SwapPss:)'. Budete-li chtít pouze součet PSS, bude výraz vypadat takto: '^Pss:'.

USS procesu

# awk '/(^Private_Clean:|^Private_Dirty:)/{sum += $2} END {print sum}' /proc/<pid>/smaps

Zkrátka variace toho samého, opět vyměňte smaps za smaps_rollup, pokud můžete.

Žebříček největších žroutů podle spustitelných souborů

Tím mám na mysli, že procesy se stejnou cestou v cmdline budou počítány dohromady, což sečte firefoxy, chrome, ale stejně tak bashe, pythony, …

Zbastlil jsem cvičně takovýto hrozivý oneliner:


# ps axo stat,user,cmd|egrep -v '\[.+\]'|grep -v '^Z '|awk '{print $3}'|sort|uniq|while read cmd; do echo -e "$(ps axo pid,cmd | grep " $cmd" | awk '{print $1}' |xargs -I '%' grep 'Pss:' /proc/%/smaps_rollup | awk '{sum += $2} END {printf "%8.2f", sum/1024}') \t${cmd##*/}"; done 2>/dev/null|sort -rhk1|egrep "[0-9]+ "|less  

Vypíše v prvním sloupci posčítané velikosti PSS v MiB formátované na 8 znaků včetně 2 desetinných míst, a v druhém jména programů (spustitelných souborů bez cesty, tu z $cmd odstraní konstukce ${cmd##*/} ). Celé je to seřazeno podle velikosti od nevzšší hodnoty a egrep na konci odfiltruje objekty bez hodnoty PSS, které do výpisu nepatří. Výpis bývá dlouhý, proto less na konci pro pohodlnější prohlížení. Ideální to není, zjednodušovat to nebudu, jsou lepší jazyky na takové operace a v tuto chvíli už i hotová aplikace.

Závěr?

Je zřejmé, že sledovat pouze free RAM a neustále dropovat cache, aby to číslo bylo co nejvyšší, je nesmysl, protože i obsazená paměť může být dostupná a přitom užitečná. Linux se snaží dostupnou paměť využívat na maximum a co nejvíce tím snížit latence při čekání na paměťové stránky. Při hledání problémů s pamětí v systému je dobré si uvědomit, co které kernelem reportované metriky skutečně reprezentují a pokud je nedostupná paměť, co by ji mohlo blokovat.
Při studování obsahu RAM se ale stále nemůžu dopočítat celkové obsazené paměti systému, stále mi unikají stovky MiB, které zjevně nejsou součástí uvolnitelné cache a rostou proporcionálně s přibývající konzumací paměti procesy. Postupně rozklíčovávám některé souvislosti a provázanost jednotlivých položek MemInfo souboru, takže určitě sem ještě budu dopisovat, co objevím.
 
Další zdroje: 


Žádné komentáře:

Okomentovat

Zkuste prosím při komentováni používat místo volby Anonymní volbu Název/adresa URL, kde vyplníte nějakou přezdívku, adresu zadávat nemusíte. Vědět, které příspěvky jsou od jednoho člověka, je fajn. Díky.

Pokud by se vám náhodou odeslaný komentář na stránce nezobrazil, vytáhnu ho z koše hned jak si toho všimnu. I Google spam filter se občas sekne.