Správa paměti
Správa paměti a adresní prostor procesu
Pustíš free -h na stroji s 16 GB paměti a ve sloupci free uvidíš 400 MB. První reakce bývá hledat, co paměť žere.
Není co. Volná paměť je z pohledu operačního systému promarněná paměť. Co zrovna nikdo nepotřebuje, si jádro zaplní vyrovnávací pamětí disku (buff/cache, blíž u vstupu a výstupu) a zahodí ji v okamžiku, kdy si o ni někdo řekne. Sloupec, na který se máš dívat, se jmenuje available, ne free.
Tahle stránka je o rozvržení adresního prostoru a o tom, jak se paměť přiděluje: halda, zásobník, co dělá malloc a jak číst čísla z ps. Překlad adres - tabulky stránek, TLB, výpadek stránky - je na virtuální paměti a stránkování a tady se neopakuje. Předpokládá to proces a systémová volání.
Adresní prostor není paměť, je to seznam slibů
Tohle je nejdůležitější věta na celé stránce: adresní prostor procesu není paměť. Je to seznam slibů.
Proces vidí souvislý prostor od nuly nahoru. Fyzická paměť za ním je roztroušená po stránkách 4 KiB kdekoli v RAM a velký kus těch slibů nemá za sebou nic, dokud na něj proces nesáhne.
Dva procesy proto můžou mít na adrese 0x400000 každý něco jiného, proces může mít „dva gigabajty paměti“ a zabírat čtyřicet megabajtů a malloc, který uspěl, nedokazuje vůbec nic.
Mapa adresního prostoru odshora dolů
Uživatelská část je na x86-64 spodních 128 TiB, od 0xffff800000000000 nahoru je prostor jádra. Tam se z uživatelského režimu nedostaneš, o to se stará ochrana režimů.
| Oblast | Co v ní je | Chování |
|---|---|---|
| kód (text) | instrukce programu | čtení a spuštění |
| data | inicializované globální proměnné | čtení i zápis |
| BSS | proměnné vynulované na startu | v souboru jen číslo |
| halda | to, co vrací malloc |
roste nahoru |
| mapované oblasti | knihovny, soubory, velké alokace | mezi haldou a zásobníkem |
| zásobník | lokální proměnné, návratové adresy | roste dolů |
| prostor jádra | data a tabulky jádra | nepřístupný |
Všimni si BSS: pole miliónu vynulovaných čísel zabere v souboru pár bajtů popisu, protože nuly vyrobí jádro až při dotyku.
Přečti si /proc/self/maps nahlas
Nejužitečnější příkaz na téhle stránce vypíše mapu procesu, který ho právě vykonává:
cat /proc/self/maps # mapa toho běžícího `cat`, ne shellu
57538aa88000-57538aa8d000 r-xp 00002000 103:02 2230078 /usr/bin/cat
57538aa90000-57538aa91000 rw-p 00009000 103:02 2230078 /usr/bin/cat
5753ab67d000-5753ab69e000 rw-p 00000000 00:00 0 [heap]
7fc8c1c28000-7fc8c1db0000 r-xp 00028000 103:02 2269286 /usr/lib/x86_64-linux-gnu/libc.so.6
7fc8c1ef2000-7fc8c1ef4000 r-xp 00000000 00:00 0 [vdso]
7ffe91c3c000-7ffe91c5d000 rw-p 00000000 00:00 0 [stack]
První sloupec je rozsah adres, druhý práva. r-xp je čtení a spuštění bez zápisu - kód. Že do něj nejde psát, je jediné, co brání přepsání instrukcí za běhu. rw-p je čtení a zápis bez spuštění - data. Písmeno p znamená soukromé mapování, tedy kopii při zápisu, s sdílené.
[heap] posouvá brk a má tu 132 KiB. [stack] je zásobník hlavního vlákna, taky 132 KiB, přestože strop je 8 MiB - roste podle potřeby. [vdso] jsou dvě stránky namapované jádrem, aby se gettimeofday obešlo bez systémového volání.
Co se dělalo dřív a proč to v učebnicích zůstalo
Před stránkováním dostal proces jeden souvislý kus fyzické paměti a dvojici registrů, bázi a limit. Jádro si drželo seznam volných děr a jednu z nich na každou žádost vybralo: první vhodná je první, do které se to vejde, nejlepší vhodná je nejtěsnější z celého seznamu, nejhorší vhodná naopak největší.
Vyhrála první vhodná. Nejlepší vhodná zní rozumně, ale za cenu průchodu seznamem zanechává drobty, do kterých se už nic nevejde.
Zůstaly dva druhy plýtvání, které se pletou dodnes. Vnější fragmentace znamená, že volné paměti je dost, ale ne v jednom kuse. Vnitřní fragmentace znamená, že přidělený blok je větší než žádost, protože se zaokrouhlilo nahoru.
Segmentace rozdělila proces na logické celky s vlastní bází a limitem, čímž vnitřní fragmentaci odstranila a vnější ne. Na x86-64 prakticky zmizela: cs, ds, es a ss mají bázi natvrdo nulovou a použitelnou si nechaly jen fs a gs, které slouží lokálním datům vlákna. Píše se o ní dál proto, že terminologii po ní zdědilo stránkování.
Jak se paměť doopravdy získá
Systémová volání jsou dvě. brk a sbrk posunou horní hranici haldy, mmap vytvoří novou oblast kdekoli v prostoru.
malloc mezi nimi není. Je to funkce knihovny, která si vezme paměť po velkých kusech a pak z nich rozdává. Jinak by nešlo obsloužit malloc(24): jádro umí vydat jen celé stránky po 4 KiB a jedno volání stojí zhruba 50-100 ns.
Od zhruba 128 KiB glibc přepne z haldy na mmap. Ta hranice je pohyblivá a při uvolňování takových bloků roste až ke 32 MiB. Program s alokacemi po 100 KiB se proto chová jinak než s alokacemi po 200 KiB.
strace -e trace=brk,mmap,munmap ./program 2>&1 | head -20 # kolikrát se šlo do jádra
Proč free paměť nevrátí systému
free vrátí blok do seznamu volných bloků alokátoru, ne jádru. Halda se zkracuje jen shora a jen když je souvislý volný konec větší než 128 KiB, takže jediný živý blok nahoře drží celý zbytek pod sebou. Bloky z mmap se naopak vracejí okamžitě.
Je to záměr: program, který v cyklu alokuje a uvolňuje, by jinak trávil čas v jádře místo v práci.
Anonymní a souborové mapování
mmap s příznakem MAP_ANONYMOUS vyrobí oblast, za kterou není nic a jejíž stránky vzniknou vynulované při prvním dotyku. To je paměť v běžném smyslu slova.
Bez toho příznaku mapuje soubor a čtení z oblasti je čtením souboru bez read. S MAP_SHARED ji vidí i ostatní procesy, které mapují týž soubor - nejrychlejší sdílená paměť, jakou můžeš mít.
Přeprodávání paměti a líné přidělování
malloc(1 GB) uspěje na stroji, kde volný gigabajt není. Nikdo nelže: alokace jen zapíše do seznamu oblastí další slib. Fyzická stránka se přidělí až při prvním dotyku, přes výpadek stránky.
cat /proc/sys/vm/overcommit_memory # režim přeprodávání
| Hodnota | Co dělá | Kdy ji chceš |
|---|---|---|
0 |
heuristika, odmítne nesmyslné žádosti | výchozí, nech to tak |
1 |
vyhoví vždycky | řídká pole, mapuje se víc, než se použije |
2 |
tvrdý strop CommitLimit |
má-li selhat malloc místo procesu |
Režim 2 počítá strop jako odkládací prostor plus overcommit_ratio procent RAM, a protože ten poměr je implicitně 50, na stroji bez swapu ti z 16 GB povolí zhruba osm. Nech výchozí nulu, ale věz, co znamená: tvůj program může být zabit, i když malloc uspěl. Kdo koho zabije, je u výměnných algoritmů.
Jak číst čísla o paměti procesu
| Metrika | Co doopravdy měří | Kdy je zavádějící |
|---|---|---|
| VSZ | součet velikostí všech mapování | skoro vždy, počítá i nedotčené sliby |
| RSS | stránky přítomné ve fyzické paměti | sdílené knihovny počítá celé každému |
| PSS | sdílená stránka dělená počtem uživatelů | jen na Linuxu a jen ze smaps |
| USS | jen soukromé stránky | řekne, co uvolní zabití procesu |
Odtud plyne, co každého zarazí: součet RSS všech procesů vyjde víc, než kolik je v počítači paměti. Knihovnu libc má namapovanou každý proces a každému se počítá celá, přestože fyzicky je v paměti jednou.
Poctivější číslo dá PSS. Sečte ho smem, a bez instalace čehokoli funguje /proc/PID/smaps_rollup, které je od jádra 4.14 a je mnohem levnější než celé smaps. Zbytek je u nástrojů a diagnostiky.
Zásobník a jeho meze
Zásobník hlavního vlákna má implicitně 8 MiB a ulimit -s to ukáže v kilobajtech jako 8192. Přeteče ze dvou příčin: rekurze, která se nezastaví, a lokální pole typu char buf[16 * 1024 * 1024]. Obojí končí signálem SIGSEGV, protože pod zásobníkem je stránka stráže, tedy nenamapovaná oblast. Velká pole patří na haldu.
ulimit -s # 8192 znamená 8192 KiB, tedy 8 MiB
ulimit -s 32768 # zvedne strop na 32 MiB pro tenhle shell a jeho potomky
Každé vlákno má vlastní zásobník a implicitní velikost bere z ulimit -s. Tisíc vláken tedy znamená 8 GiB virtuálního prostoru. Fyzicky se spotřebuje jen dotčená část, ale při overcommit_memory na 2 se započítá celý.
Stack Clash, 2017
V červnu 2017 zveřejnila firma Qualys třídu útoků Stack Clash. Stránka stráže byla tehdy jediná stránka o 4 KiB a program, který si naráz vyrobil dostatečně velký rámec, ji přeskočil a zapisoval rovnou do sousedního mapování. Přes setuid binárky to vedlo k místní eskalaci práv na Linuxu i na několika BSD; opravovalo se to jako CVE-2017-1000364 a jádro zvětšilo mezeru z jedné stránky na 1 MiB.
Poučení: stránka stráže chrání jen do té míry, do jaké je jisté, že do ní program šlápne.
Proč dlouho běžící proces roste, i když nikde neteče
Server obslouží ve špičce deset tisíc spojení po 20 KiB, pak jich má sto, a RSS zůstane nahoře. Paměť je uvolněná, ale sedí v seznamech alokátoru roztroušená mezi živými bloky, které brání zkrácení haldy. To je fragmentace haldy, ne únik.
Rozlišíš je podle tvaru křivky. Únik roste bez konce, fragmentace se ustálí na plató. Nech to běžet den; když se RSS zastaví, byť vysoko, žádný únik nehledej.
Druhá častá příčina je počet arén v glibc: ve vícevláknovém programu jich vyrobí až osminásobek počtu jader, aby se vlákna nepraly o zámek, a každá si drží vlastní zásobu. Na osmi jádrech je to až 64 arén.
MALLOC_ARENA_MAX=2 ./program # méně arén, méně roztříštěné paměti
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 ./program # balíček libjemalloc2
Jiný alokátor zkoušej, když má program hodně vláken a hodně malých alokací různých velikostí; jemalloc i tcmalloc mají třídy velikostí a vlákenné mezipaměti a fragmentují míň. Placebo je to u jednovláknového programu a všude, kde je příčinou skutečný únik.
Diagnostika
free -h # dívej se na available, ne na free
grep -E 'MemAvailable|Committed_AS' /proc/meminfo
pmap -x 1234 # oblasti procesu s RSS a Dirty na každou
grep -E '^(Rss|Pss|Private_Dirty)' /proc/1234/smaps_rollup
dmesg -T | grep -i 'out of memory' # zabil ho OOM killer?
valgrind --leak-check=full ./program # zpomalí 10-50x, na produkci ne
| Příznak | Kde je problém |
|---|---|
| RSS roste a nezastaví se | skutečný únik, na to valgrind |
| RSS vyskočil a drží se na plató | fragmentace haldy nebo neuvolněná špička |
| málo volné paměti, systém běží dobře | špatný sloupec, čti available |
SIGKILL a v dmesg zmínka o OOM |
přeprodaná paměť došla při dotyku |
malloc vrátil NULL |
RLIMIT_AS, limit cgroups nebo overcommit 2 |
| segfault na startu u velkého pole | lokální pole přes 8 MiB, přesuň ho na haldu |
V kontejneru ukáže free paměť celého stroje, ale zabije tě strop cgroup z /sys/fs/cgroup/memory.max.
Co si odnést
Adresní prostor je seznam slibů, ne paměť. Většina z nich nemá za sebou nic, dokud na ně nesáhneš.
Sloupec available, ne free. Vyrovnávací paměť disku se zahazuje na požádání.
/proc/PID/maps odpoví na většinu otázek. Práva, velikosti oblastí i mapované soubory.
malloc je knihovna, ne jádro. Do jádra chodí přes brk a mmap, a zřídka.
Úspěšný malloc není záruka. Stránka se přiděluje při dotyku a tehdy může přijít OOM killer.
RSS sečtené přes procesy nedává smysl, PSS ano. Sdílené stránky se jinak počítají mnohokrát.
Únik roste bez konce, fragmentace se ustálí. Podle toho poznáš, který nástroj vzít.
Kam dál
- Virtuální paměť a stránkování - jak se ze slibu stane fyzická stránka
- Výměnné algoritmy - co se děje, když paměť dojde, a jak vybírá OOM killer
- Vlákna - proč každé vlákno ukousne kus adresního prostoru
- Nástroje a diagnostika -
pmap,smem,/proca kdy který