Quando Linux non riesce più a trovare memoria sufficiente per soddisfare una richiesta, si susseguono una serie di operazioni. Prima di arrivare a una situazione critica, il kernel prova a recuperare pagine inutilizzate, riduce le cache recuperabili, scrive eventualmente dati nell’area di swap e tenta diversi meccanismi di recupero della memoria.
Se anche questi meccanismi falliscono, però, continuare ad aspettare potrebbe bloccare l’intero sistema. Linux dispone quindi di una soluzione estrema: terminare uno o più processi per liberare memoria. Il componente che prende questa decisione è conosciuto come OOM Killer, dove OOM significa Out Of Memory.
Il kernel attribuisce ai processi un punteggio, valuta il tipo di memoria disponibile, considera eventuali limiti imposti tramite cgroup (gruppi di controllo che danno modo di assegnare e limitare le risorse disponibili ai processi) e permette perfino all’amministratore di indicare quali applicazioni debbano risultare più o meno sacrificabili.
Prima dell’OOM Killer, Linux prova a recuperare memoria
OOM Killer rappresenta l’ultima linea di difesa, non il primo intervento eseguito quando la memoria disponibile diminuisce. Linux utilizza normalmente gran parte della RAM non occupata direttamente dai programmi come page cache, cache dei filesystem e altre strutture utili.
In presenza di “pressione” sulla memoria, Linux prova innanzi tutto a liberare pagine recuperabili. Le pagine contenenti dati già presenti su disco possono essere eliminate dalla RAM e ricaricate successivamente; quelle anonime, ad esempio appartenenti all’heap dei processi, possono essere trasferite nello swap, se configurato e se le condizioni lo consentono.
Il kernel dispone inoltre di meccanismi come il direct reclaim, attraverso il quale lo stesso processo che sta tentando di ottenere memoria può essere costretto a partecipare al recupero delle pagine.
Solo quando un’allocazione continua a fallire e il kernel ritiene di non avere realisticamente altre risorse recuperabili, scatta la gestione OOM. Lo stesso codice sorgente di Linux descrive OOM Killer come un meccanismo utilizzato quando il sistema è “seriously out of memory“. La scarsità di memoria può interessare l’intera macchina oppure soltanto una determinata porzione delle risorse.
Come Linux sceglie il processo da terminare
Quando deve scegliere una “vittima sacrificale”, Linux utilizza una funzione chiamata oom_badness(): il punteggio più elevato è attribuito a tutti quei processi la cui eliminazione permetterebbe di recuperare una quantità significativa di memoria.
Il codice considera principalmente
- RSS del processo, cioè le pagine effettivamente residenti in memoria;
- pagine presenti nella swap associate al processo;
- memoria occupata dalle page table necessarie per gestirne lo spazio di indirizzamento.
Linux espone alcune delle informazioni utilizzate dall’OOM Killer attraverso il percorso /proc. Per un processo con PID 1234 è possibile utilizzare, ad esempio, questi due comandi:
cat /proc/1234/oom_score
cat /proc/1234/oom_score_adj
Il primo valore fornisce un’indicazione del punteggio attribuito al processo; il secondo rappresenta invece una correzione amministrativa applicata al calcolo. Un valore positivo rende il processo progressivamente più appetibile per l’OOM Killer; un valore negativo tende invece a proteggerlo.
Il programma la cui richiesta di memoria fa scattare OOM Killer su Linux non coincide necessariamente con il processo che sarà arrestato forzosamente.
Cosa succede dopo la scelta del processo da chiudere
Individuare il processo da eliminare non significa che tutta la sua memoria torni istantaneamente disponibile. Linux invia “alla vittima” un SIGKILL, ovvero un segnale che il processo non può intercettare né ignorare. Il processo deve tuttavia attraversare le procedure necessarie alla terminazione e il kernel deve smantellarne le strutture di memoria.
In una situazione di fortissima pressione, aspettare che tutto avvenga attraverso il normale percorso di uscita potrebbe essere troppo lento: per questo motivo Linux utilizza anche OOM Reaper.
Il suo compito consiste nel tentare di recuperare rapidamente parte dello spazio di indirizzamento del processo in fase di chiusura mentre questo completa la terminazione. Il codice del kernel cerca in particolare di liberare mapping anonimi o privati che possono essere rimossi senza operazioni complesse.
Nei log può quindi comparire una riga simile a:
oom_reaper: reaped process 1234 (programma)
Il meccanismo riduce il rischio che il sistema rimanga bloccato in attesa che il processo prescelto liberi spontaneamente una quantità sufficiente di memoria.
Come riconoscere un OOM Killer nei log
Quando Linux termina un processo per mancanza di memoria, lascia normalmente informazioni molto dettagliate nel kernel log. Di seguito due modi per cercarle tra la moltitudine di informazioni annotate durante l’esecuzione del sistema operativo:
journalctl -k | grep -i oom
dmesg | grep -i -E 'oom|killed process'
Un messaggio tipico può contenere informazioni simili a: Killed process 8421 (java) total-vm:… anon-rss:… file-rss:… pgtables:… oom_score_adj:…
Per impostazione predefinita, Linux può inoltre stampare lo stato degli altri processi candidati. Il parametro /proc/sys/vm/oom_dump_tasks vale normalmente 1 e permette di ottenere PID, UID, RSS, memoria virtuale, swap, page table e oom_score_adj degli elementi valutati ai fini della terminazione forzata.
Conclusioni
Imbattersi, nei log, in una riga come “Out of memory: Killed process” può sembrare il segnale di un malfunzionamento del kernel. In realtà OOM Killer rappresenta spesso il componente che impedisce una situazione ancora peggiore.
Quando non esistono più pagine recuperabili sufficienti e una nuova allocazione non può essere soddisfatta, Linux deve scegliere tra alternative poco piacevoli: rimanere bloccato indefinitamente, innescare un errore kernel panic oppure eliminare parte del carico (leggasi processi in memoria) per continuare a funzionare.
La politica predefinita privilegia la sopravvivenza della macchina ma, allo stesso tempo, OOM Killer non può e non deve diventare una normale strategia di gestione della memoria. Se interviene frequentemente, c’è un problema di fondo che va cercato in memory leak, limiti cgroup troppo bassi, assenza o dimensionamento inadeguato dello swap, workload sottodimensionati oppure applicazioni prive di limiti propri.