Nelle sezioni precedenti abbiamo visto come il kernel gestisce la memoria a livello di sistema. In questo capitolo approfondiremo i gruppi di controllo della memoria (memcg), il meccanismo utilizzato da Android per partizionare e controllare l'utilizzo della memoria per le singole app.
Comprendere memcg è fondamentale perché è il livello in cui il sistema può scegliere come target le singole app per il recupero o impostare limiti di memoria, consentendo la gestione proattiva delle impronte di memoria delle app.
Gruppi di controllo della memoria (memcg)
I gruppi di controllo (cgroup) sono una funzionalità del kernel Linux che consente di organizzare i processi in gruppi gerarchici e di distribuire le risorse di sistema (come CPU, memoria e I/O) tra di essi. memcg è il controller cgroup specifico per la memoria.
Concetti chiave di memcg
- Addebito memcg: quando un processo in un memcg alloca una pagina di memoria (anonima o supportata da file), questa pagina viene "addebitata" al memcg. L'addebito totale di un memcg è la somma di tutte le pagine utilizzate da tutti i processi al suo interno.
- Contabilità gerarchica: la memoria utilizzata viene contabilizzata nella struttura ad albero. Un addebito in un cgroup secondario viene conteggiato anche per l'utilizzo del relativo cgroup principale.
- Limiti di memoria: ogni memcg può avere limiti (come
memory.maxomemory.high) che attivano il recupero o persino l'eliminazione per esaurimento della memoria (OOM) se superati, indipendentemente dalla memoria di sistema globale. - Recupero per memcg: quando un memcg supera il limite o quando il sistema ha bisogno di memoria, il kernel può scegliere come target un memcg specifico per il recupero. Ciò significa eliminare le pagine dei file o spostare le pagine anonime su ZRAM.
La gerarchia memcg di Android
Android utilizza una gerarchia specifica per gestire i processi delle app. Questa struttura consente al sistema di applicare criteri diversi a tipi diversi di app (ad es. in primo piano e in background).

/sys/fs/cgroup/apps/: la radice di tutte le applicazioni Android.uid_<UID>/: una directory per l'ID utente di ogni app. Tutti i processi appartenenti allo stesso pacchetto applicativo condividono questo gruppo.pid_<PID>/: una directory per ogni singolo processo e per tutti i processi secondari da esso derivati. Ciò consente un controllo e una contabilità granulari per le app con più processi.
Esercizio pratico: esplorare memcg
In questo esercizio, troverai la directory memcg di MemoryLab e osserverai l'addebito di memoria in tempo reale.
1. Avvia MemoryLab
Assicurati che MemoryLab sia in esecuzione sul tuo dispositivo.
2. Trova il memcg di MemoryLab
Innanzitutto, recupera il PID dell'app MemoryLab in esecuzione:
adb shell pidof com.android.memorylab
# Example output: 11672
Ora individua la directory cgroup. Puoi trovarla in /proc/<PID>/cgroup:
adb shell cat /proc/11672/cgroup
# Example output: 0::/apps/uid_10274/pid_11672
In cgroup v2, il percorso dopo 0:: rappresenta la gerarchia memcg relativa a /sys/fs/cgroup. Il percorso completo è /sys/fs/cgroup/apps/uid_10274/pid_11672/.
3. Leggi le statistiche memcg
Vai alla directory e guarda i file chiave:
# Current memory usage (in bytes)
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current
# Example output:
# 1591324672
memory.current mostra la quantità totale di memoria (in byte) attualmente addebitata a questo cgroup.
# Detailed statistics
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.stat | head -n 10
# Example output:
# anon 1585098752
# file 741376
# kernel 5484544
# kernel_stack 393216
# pagetables 4583424
# sec_pagetables 0
# percpu 216
# sock 0
# vmalloc 4096
# shmem 20480
memory.stat fornisce una suddivisione: * anon: quantità di memoria anonima
(heap, stack). * file: quantità di memoria supportata da file (cache di pagine). *
swap: quantità di memoria spostata su ZRAM.
4. Osserva le modifiche
- Apri MemoryLab.
- Prendi nota del valore di
memory.current. - Tocca Allocate Java Memory (10MB) più volte.
- Leggi di nuovo
memory.current. Dovresti vedere un aumento di circa 10 MB per ogni tocco. - Tocca Allocate Bitmaps. Controlla
memory.statper vedere l'aumento dianon.
Recupero proattivo con memory.reclaim
Una funzionalità potente di memcg (v2) è il file memory.reclaim. La scrittura di un valore in questo file indica al kernel di tentare immediatamente di recuperare la quantità di memoria da questo memcg e da tutti i memcg sottostanti.
Come Android utilizza memory.reclaim
CachedAppOptimizer di Android utilizza questa funzionalità per recuperare la massima quantità di memoria dalle app dopo che sono state congelate. App Freezer di Android assicura che le app memorizzate nella cache consumino la minor quantità possibile di RAM quando non sono in esecuzione. Quando un'app passa in background e viene congelata, il sistema scrive la memoria utilizzata corrente dell'app nel relativo file memory.reclaim. In questo modo, il kernel è costretto a eliminare tutte le pagine dei file possibili e a spostare tutte le pagine anonime su ZRAM, riducendo al minimo l'impronta residente dell'app.
Puoi ottenere lo stesso "recupero massimo" manualmente leggendo memory.current e riscrivendolo in memory.reclaim:
# Force reclaim of everything
adb shell "cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
Esercizio: forzare il recupero e la traccia
Ora forzeremo il kernel a recuperare la memoria da MemoryLab e acquisiremo l'attività in una traccia Perfetto.
- Preparazione: assicurati che MemoryLab abbia alcune allocazioni (Java e bitmap).
Avvia la traccia: utilizza questa configurazione di traccia in linea per acquisire gli eventi di pianificazione, recupero e cache di pagine.
# Use a config that captures scheduling, reclaim and page cache events adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/reclaim.perfetto-trace <<EOF buffers: { size_kb: 131072 fill_policy: RING_BUFFER } data_sources: { config { name: "linux.ftrace" ftrace_config { ftrace_events: "sched/sched_switch" ftrace_events: "sched/sched_wakeup" ftrace_events: "kmem/rss_stat" ftrace_events: "mm_filemap_add_to_page_cache" ftrace_events: "mm_filemap_delete_from_page_cache" ftrace_events: "vmscan/mm_vmscan_direct_reclaim_begin" ftrace_events: "vmscan/mm_vmscan_direct_reclaim_end" ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_begin" ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_end" symbolize_ksyms: true } } } data_sources: { config { name: "linux.process_stats" process_stats_config { scan_all_processes_on_start: true } } } EOFAttiva la pressione e il recupero:
- In MemoryLab, tocca Allocate Native Memory (1GB). In questo modo si attiverà la pressione a livello di sistema.
In un terminale separato, recupera 200 MB:
adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
Ripristina: torna a MemoryLab. Tocca Thrash Pagecache (Refault test).
Interrompi la traccia: premi Ctrl+C nel terminale di traccia.
Analizzare il recupero in Perfetto
Quando apri la traccia, puoi vedere il recupero a livello di sistema e per app in azione:

Cerca quanto segue nella traccia:
- kswapd: cerca
kswapd0in Kernel threads (nello screenshot sopra è stato aggiunto manualmente in alto). Vedrai che si riattiva ed è in esecuzione (segmenti verdi) mentre il sistema fatica a trovare pagine libere. - Recupero diretto: esamina i thread del processo
com.android.memorylab. Vedrai segmenti di eventi ftrace viola (comemm_vmscan_direct_reclaim_begin) direttamente sotto la traccia di pianificazione del thread. Ciò indica che il thread dell'app è bloccato in attesa che il kernel liberi le pagine. - Contatori RSS e swap:
mem.rss.anon: aumenta quando tocchi i pulsanti di allocazione.mem.swap: aumenta costantemente man mano chekswapde i thread dell'app (soggetti a recupero diretto) comprimono le pagine anonime in ZRAM.
- Recupero memcg: se ingrandisci il momento in cui hai attivato il recupero manuale, vedrai un forte calo di
rss.anonerss.file, accompagnato da eventimm_vmscan_memcg_reclaim.
← A livello di sistema | ↑ Su | Interazione kswapd e lmkd →