Interazione tra kswapd e lmkd

Questa pagina illustra l'interazione tra kswapd e lmkd su Android, mostrando come collaborano per gestire la pressione della memoria.

Sfondo

Android utilizza due meccanismi principali per recuperare memoria quando il sistema è in esaurimento:

  1. kswapd: un daemon del kernel Linux standard che recupera le pagine rimuovendo le pagine pulite supportate da file o scambiando le pagine anonime con ZRAM.
  2. lmkd: il daemon Low Memory Killer dello spazio utente di Android. Quando kswapd non riesce a soddisfare le richieste di memoria e la pressione sul sistema aumenta (come segnalato da PSI), lmkd interviene per terminare i processi e liberare blocchi di memoria più grandi.

kswapd e lmkd sono due sistemi di recupero della memoria complementari. Entrambi lavorano per raggiungere lo stesso obiettivo (creare memoria libera), ma con mezzi diversi. Per confrontare e contrapporre:

  • Recuperabile e distruttivo: kswapd elimina le pagine di memoria che possono essere lette dallo spazio di archiviazione o scambia le pagine di memoria con altre partizioni di memoria o con uno spazio di archiviazione che può essere letto on demand. lmkd termina i processi, distruggendo lo stato non volatile.
  • Larghezza di banda di recupero: kswapd utilizza più CPU per recuperare una determinata quantità di memoria libera rispetto a lmkd e impiega più tempo per recuperare la memoria. lmkd può recuperare grandi quantità di memoria in brevi periodi di tempo con un impegno relativamente ridotto. Tieni presente che sui dispositivi mobili, l'utilizzo della CPU si traduce non solo in latenza, ma anche in consumo della batteria.

Bucket di punteggi OOM

Android assegna un aggiustamento del punteggio Out-Of-Memory (OOM) (oom_score_adj) ai processi in base alla loro importanza per l'utente. I processi con punteggi più alti vengono terminati per primi.

I bucket più comuni includono:

  • Memorizzati nella cache (punteggio >= 900): processi non necessari al momento, conservati per un riavvio più rapido.
  • Servizio (punteggio 500): processi che ospitano i servizi avviati.
  • Servizio in primo piano (punteggio 200): processi che ospitano servizi in primo piano.
  • In primo piano (punteggio 0): l'app con cui l'utente sta interagendo al momento.

Approfondimento: attivazione di PSI e LMKD

Che cos'è PSI?

Pressure Stall Information (PSI) è una funzionalità del kernel Linux che fornisce un modo canonico per misurare le carenze di risorse. Tiene traccia del tempo di ritardo delle attività a causa di carenze di CPU, memoria e I/O.

PSI della memoria: "some" e "full"

Il PSI della memoria monitora in modo specifico i ritardi causati da carenze di memoria (ad es. attesa di recupero o errori di pagina). Fornisce due metriche:

  • some: la percentuale di tempo durante la quale almeno un'attività è stata bloccata nella memoria. Ciò indica che la carenza di memoria sta influendo sulle prestazioni, ma alcune attività sono ancora in corso.
  • full: la percentuale di tempo durante la quale tutte le attività non inattive sono state bloccate contemporaneamente. Questo indica uno stato critico in cui il sistema è completamente bloccato in attesa di memoria.

In che modo PSI attiva LMKD

Il daemon userspace lmkd si registra con il kernel per ricevere notifiche quando vengono superate le soglie PSI (monitorando /proc/pressure/memory). Quando il sistema subisce un'elevata pressione della memoria (ad es. un blocco di full che supera una durata configurata), il kernel attiva lmkd. lmkd valuta quindi lo stato del sistema e decide se terminare i processi e quali, in base ai punteggi OOM e alle dimensioni.

Ottimizzazione LMKD

Il comportamento di LMKD può essere ottimizzato utilizzando le proprietà di sistema.

Trovare i valori di ottimizzazione su un dispositivo

Puoi trovare i valori di ottimizzazione LMKD correnti su un dispositivo connesso eseguendo:

adb shell getprop | grep lmk

Proprietà LMKD comuni

Ecco alcune delle proprietà principali utilizzate per ottimizzare LMKD (consulta la documentazione di Android per l'elenco completo):

  • ro.lmk.use_psi: imposta su true per utilizzare i monitor PSI per il rilevamento della pressione della memoria (impostazione predefinita su Android 10 e versioni successive).
  • ro.lmk.psi_partial_stall_ms: la soglia per la durata del blocco (parziale) di some in millisecondi per attivare un'interruzione (valore predefinito 70 ms sui dispositivi normali, 200 ms sui dispositivi con poca RAM).
  • ro.lmk.psi_complete_stall_ms: la soglia per la durata del blocco di full in millisecondi per attivare un'interruzione (valore predefinito 700 ms).
  • ro.lmk.kill_heaviest_task: se true, LMKD termina il processo che utilizza più memoria tra i candidati nello stesso bucket OOM.

Valori di ottimizzazione pertinenti sul dispositivo di test (Google Pixel 8 Pro)

(Nota: valori predefiniti tipici per i dispositivi di fascia alta con Android 15)

  • ro.lmk.use_psi: true
  • ro.lmk.psi_partial_stall_ms: 70 (valore tipico)
  • ro.lmk.psi_complete_stall_ms: 700 (valore tipico)
  • ro.lmk.kill_heaviest_task: true

Verifica di ZRAM e swap

La capacità di ZRAM influisce notevolmente sulla velocità di riempimento dello swap e sull'attivazione delle terminazioni di LMKD. Puoi controllare lo stato di ZRAM e swap sul dispositivo utilizzando questi comandi:

# Check ZRAM sizing and usage
adb shell free -m

# Check swap partition details
adb shell cat /proc/swaps

Esercitazione pratica: attivazione di kswapd e lmkd

In questo esercizio utilizzerai l'app di esempio KswapdLmkdLab per creare processi in diversi bucket OOM e poi consumare memoria nel processo principale per attivare kswapd e lmkd.

L'app è configurata per generare automaticamente sottoprocessi e allocare memoria per renderli target interessanti per LMKD.

1. Crea sottoprocessi

All'avvio, l'app genera automaticamente i seguenti processi secondari:

  • Processo memorizzato nella cache (oom_score_adj >= 900)
  • Processo di servizio (oom_score_adj 500)
  • Processo di servizio in primo piano (oom_score_adj 200)

Verifica i processi e i relativi punteggi OOM utilizzando ADB:

adb shell dumpsys activity processes | grep com.android.kswapdlmkdlab

Snippet di output di esempio:

    Proc #  0: fg       T/A/TOP  LCMNFUATI  t: 0 18959:com.android.kswapdlmkdlab/u0a358 (top-activity)
    Proc #  1: prcp     b/ /FGS  -------TI  t: 0 19050:com.android.kswapdlmkdlab:foreground/u0a358 (fg-service)
    Proc #  2: svc      b/ /SVC  -------TI  t: 0 19077:com.android.kswapdlmkdlab:service/u0a358 (started-services)
    Proc #  3: cch      b/ /CACC -------TI  t: 0 19014:com.android.kswapdlmkdlab:cached/u0a358 (cch-client-act)

Le abbreviazioni dello stato del processo corrispondono a:

  • fg: primo piano (punteggio OOM 0)
  • prcp: Perceptible (servizio in primo piano, punteggio OOM 200)
  • svc: servizio avviato (punteggio OOM 500)
  • cch: processo memorizzato nella cache (punteggio OOM 900)

2. Attiva la pressione della memoria

L'app inizierà automaticamente ad allocare memoria nel processo principale. Per rendere il test efficace sui dispositivi con RAM elevata, l'app ordina anche ai sottoprocessi di allocare memoria (ad esempio 500 MB ciascuno) per renderli target migliori per LMKD.

Monitora l'interruzione del sottoprocesso

L'app monitora la terminazione di questi sottoprocessi utilizzando il meccanismo IBinder.linkToDeath:

  1. Il processo principale si associa a un servizio nel processo di destinazione utilizzando bindService.
  2. Al momento della connessione, ottiene l'oggetto IBinder che rappresenta il servizio.
  3. Registra un'implementazione di IBinder.DeathRecipient chiamando linkToDeath sul binder.
  4. Quando il processo di destinazione termina (ad esempio, quando viene terminato da LMKD), il sistema chiama il metodo binderDied(), consentendo all'app di registrare l'evento di terminazione in modo asincrono.

Per evitare che questa associazione aumenti la priorità dei processi di destinazione (il che vanificherebbe lo scopo dell'esercizio), l'app utilizza il flag Context.BIND_WAIVE_PRIORITY.

App di esempio in azione

Ecco uno screenshot dell'app dopo il completamento di un'esecuzione di test su un Google Pixel 8 Pro (che ha circa 12 GB di RAM totale).

Screenshot dell'app di esempio dopo il completamento di un'esecuzione di test.
Figura 1. App di esempio dopo l'esecuzione di un test completato su un Pixel 8 Pro.

Analisi della traccia di Perfetto

Per acquisire l'interazione, utilizza la seguente configurazione di Perfetto per acquisire la pianificazione, i contatori di memoria, i kill di LMK e le informazioni sulla pressione (PSI).

Configurazione di Perfetto

Acquisisci l'interazione eseguendo Perfetto con pianificazione, contatori di memoria, terminazioni LMK e informazioni sulla pressione di stallo (PSI):

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/kswapd_lmkd.perfetto-trace <<EOF
buffers: { size_kb: 131072 }
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "lowmemorykiller/lowmemory_kill"
            ftrace_events: "vmscan/mm_vmscan_kswapd_wake"
            ftrace_events: "vmscan/mm_vmscan_kswapd_sleep"
            ftrace_events: "psi/psi_event"
        }
    }
}
duration_ms: 180000
EOF

Esamina la traccia

Lo screenshot seguente è tratto da una traccia che mostra il periodo di tempo che intercorre tra poco prima dell'interruzione del processo :cached (più terminabile) e l'interruzione del processo :foreground (meno terminabile).

Screenshot della UI di Perfetto che mostra l'attività di kswapd e lmkd.
Figura 2. Traccia di Perfetto che mostra il recupero di kswapd e le interruzioni di lmkd.

Puoi vedere che MemFree è già piuttosto basso ed è sotto pressione costante. kswapd è costantemente in funzione e riduce la pressione eliminando mem.rss.file pagine e spostando mem.rss.anon in mem.swap da tutti i kswapdlmkdlab processi (il processo principale e i sottoprocessi) e probabilmente da altri processi non mostrati in questo screenshot. Allo stesso tempo, l'app kswapdlmkdlab continua ad allocare memoria, aumentando la pressione più velocemente di quanto kswapd possa recuperare.

Quando la pressione diventa troppo elevata, come si vede quando psi.mem è alto per un periodo di tempo sufficiente, lmkd si attiva e termina alcuni processi. Puoi vedere che lmkd rispetta i punteggi OOM, terminando prima kswapdlmkdlab:cached, poi :service e infine :foreground.

Esamina la sequenza temporale degli eventi e rifletti su quanto segue:

  • Come kswapd e lmkd si completano a vicenda: kswapd è il primo a rispondere alla pressione della memoria, ma lmkd aiuta quando la larghezza di banda di recupero di kswapd non soddisfa la domanda di allocazione.
  • La frequenza di liberazione della memoria: osserva il tempo di CPU impiegato in kswapd e la memoria recuperata eliminando mem.rss.file o scambiando mem.rss.anon con mem.swap. Confronta questo dato con le brevi ondate di attività in lmkd che comportano il rilascio di grandi quantità di memoria privata del processo.

Query

Esegui queste query nell'interfaccia utente di Perfetto (Query SQL) per estrarre i dati dalla traccia.

1. Attività kswapd

Per scoprire quando il daemon di swap del kernel era attivo e in esecuzione, esegui:

SELECT ts, dur, state
FROM thread_state
JOIN thread USING (utid)
WHERE thread.name LIKE 'kswapd%'
  AND state = 'Running'
LIMIT 5;

Esempio di output:

ts dur stato
30317633052969 159180 In esecuzione
30317633398347 3999186 In esecuzione
30317638448111 1847941 In esecuzione
30317641455354 1031006 In esecuzione
30317643272615 888387 In esecuzione

2. Kill LMKD

LMKD scrive le interruzioni per memoria insufficiente nella tabella instant. Per eseguire una query su tutti i kill di LMKD:

SELECT ts, name
FROM instant
WHERE name LIKE 'lmk,%'
ORDER BY ts ASC;

Il formato del nome dell'evento è: lmk,<pid>,<kill_reason>,<oom_score_adj>,<min_oom_score>,<max_thrashing>.

  • kill_reason: un numero intero che indica il motivo per cui lmkd ha deciso di terminare il processo (definito in system/memory/lmkd/statslog.h):
    • 0: PRESSURE_AFTER_KILL
    • 1: NOT_RESPONDING
    • 2: LOW_SWAP_AND_THRASHING (lo spazio di swap ZRAM è estremamente ridotto e si è verificato un thrashing)
    • 3: LOW_MEM_AND_SWAP (il sistema ha poca memoria libera e poco spazio di scambio ZRAM)
    • 4: LOW_MEM_AND_THRASHING
    • 5: DIRECT_RECL_AND_THRASHING
    • 6: LOW_MEM_AND_SWAP_UTIL
    • 7: LOW_FILECACHE_AFTER_THRASHING
    • 8: LOW_MEM
    • 9: DIRECT_RECL_STUCK
  • max_thrashing: la percentuale massima di thrashing della pagina al momento dell'interruzione.

Esempio di output:

ts nome
30326148679967 lmk,2308,3,900,201,0
30330439763710 lmk,2309,3,500,201,0
30334985999837 lmk,2310,2,200,0,307

Visualizzazione

Per visualizzare i dati, puoi utilizzare il seguente script gnuplot per tracciare la memoria libera del sistema, l'attività kswapd, i kill lmkd e il PSI su una singola serie temporale. Lo script presuppone che venga eseguito dalla directory principale della build Android.

Esempio di grafico di output

Il grafico seguente è stato generato utilizzando lo script precedente con dati campionati rappresentativi dell'esecuzione del test.

Grafico della memoria libera, dell'attività kswapd, del PSI e delle interruzioni lmkd.
Figura 3. Grafico della pressione della memoria che mostra la memoria libera, kswapd, PSI e le interruzioni di lmkd.

Spiegazione della trama:

Questo grafico illustra l'interazione dinamica tra i diversi componenti di gestione della memoria durante lo stress test:

  1. Calo della memoria libera: man mano che l'app principale alloca memoria, la memoria libera del sistema (linea viola) diminuisce costantemente.
  2. Attivazione di kswapd: quando la memoria libera scende al di sotto di determinati watermark, kswapd si attiva (impulsi blu) per recuperare memoria eseguendo lo swap su ZRAM. Questo è visibile come picchi intermittenti.
  3. Picchi di PSI: poiché kswapd fatica a tenere il passo con l'elevata frequenza di allocazione, i thread iniziano a bloccarsi sull'allocazione della memoria, causando un picco di PSI (Pressure Stall Information) (linea verde).
  4. Interruzioni di LMKD: quando il PSI raggiunge livelli critici (ad esempio, superando concettualmente la soglia di ro.lmk.psi_complete_stall_ms) e la memoria libera è estremamente bassa, lmkd interviene interrompendo i processi con priorità bassa (impulsi arancioni al minuto 30 e 45) per liberare grandi blocchi di memoria. Noterai che, subito dopo un kill, la memoria libera aumenta e il PSI diminuisce, a dimostrazione dell'efficacia del kill nell'alleviare la pressione. ________________________________________________________________________________

← Recupero della memoria: espulsione e swap | ↑ Su | Restituisci le pagine di memoria al sistema operativo →