Dans les sections précédentes, nous avons vu comment le noyau gère la mémoire à l'échelle du système. Dans ce chapitre, nous allons examiner plus en détail les groupes de contrôle de la mémoire (memcg), le mécanisme qu'Android utilise pour partitionner et contrôler l'utilisation de la mémoire pour les applications individuelles.
Il est essentiel de comprendre les memcg, car c'est à ce niveau que le système peut cibler des applications individuelles pour la récupération ou définir des limites de mémoire, ce qui permet une gestion proactive de l'empreinte mémoire des applications.
Groupes de contrôle de la mémoire (memcg)
Les groupes de contrôle (cgroups) sont une fonctionnalité du noyau Linux qui permet d'organiser les processus en groupes hiérarchiques et de répartir les ressources système (telles que le processeur, la mémoire et les E/S) entre eux. memcg est le contrôleur cgroup spécifiquement destiné à la mémoire.
Concepts clés des memcg
- Charge memcg : lorsqu'un processus dans un memcg alloue une page de mémoire (anonyme ou sauvegardée dans un fichier), cette page est "facturée" au memcg. La charge totale d'un memcg correspond à la somme de toutes les pages utilisées par tous les processus qu'il contient.
- Comptabilité hiérarchique : l'utilisation de la mémoire est comptabilisée dans l'arborescence. Une charge dans un cgroup enfant est également comptabilisée dans l'utilisation de son parent.
- Limites de mémoire : chaque memcg peut avoir des limites (telles que
memory.maxoumemory.high) qui déclenchent la récupération, voire le killer OOM si elles sont dépassées, indépendamment de la mémoire système globale. - Récupération par memcg : lorsqu'un memcg dépasse sa limite ou lorsque le système a besoin de mémoire, le noyau peut cibler un memcg spécifique pour la récupération. Cela signifie qu'il faut évincer ses pages de fichiers ou échanger ses pages anonymes avec ZRAM.
Hiérarchie des memcg Android
Android utilise une hiérarchie spécifique pour gérer les processus d'application. Cette structure permet au système d'appliquer différentes stratégies à différents types d'applications (par exemple, au premier plan ou en arrière-plan).

/sys/fs/cgroup/apps/: racine de toutes les applications Android.uid_<UID>/: répertoire pour l'ID utilisateur de chaque application. Tous les processus appartenant au même package d'application partagent ce groupe.pid_<PID>/: répertoire pour chaque processus individuel et tous les enfants qui en sont issus. Cela permet un contrôle précis et une comptabilité pour les applications comportant plusieurs processus.
Exercice pratique : explorer les memcg
Dans cet exercice, vous trouverez le répertoire memcg de MemoryLab et vous observerez sa charge mémoire en temps réel.
1. Lancer MemoryLab
Assurez-vous que MemoryLab est en cours d'exécution sur votre appareil.
2. Trouver le memcg de MemoryLab
Commencez par obtenir le PID de l'application MemoryLab en cours d'exécution :
adb shell pidof com.android.memorylab
# Example output: 11672
Localisez ensuite son répertoire cgroup. Vous le trouverez dans /proc/<PID>/cgroup :
adb shell cat /proc/11672/cgroup
# Example output: 0::/apps/uid_10274/pid_11672
Dans cgroup v2, le chemin d'accès après 0:: représente la hiérarchie memcg par rapport à /sys/fs/cgroup. Le chemin d'accès complet est donc /sys/fs/cgroup/apps/uid_10274/pid_11672/.
3. Lire les statistiques memcg
Accédez à ce répertoire et examinez les fichiers clés :
# Current memory usage (in bytes)
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current
# Example output:
# 1591324672
memory.current indique la quantité totale de mémoire (en octets) actuellement facturée à ce 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 fournit une répartition : * anon : quantité de mémoire anonyme
(tas, piles). * file: quantité de mémoire sauvegardée dans un fichier (cache de page). *
swap: quantité de mémoire échangée avec ZRAM.
4. Observer les modifications
- Ouvrez MemoryLab.
- Notez la valeur de
memory.current. - Appuyez plusieurs fois sur Allocate Java Memory (10MB) (Allouer de la mémoire Java (10 Mo)).
- Relisez
memory.current. Vous devriez voir une augmentation d'environ 10 Mo à chaque pression. - Appuyez sur Allocate Bitmaps (Allouer des bitmaps). Vérifiez que
anonaugmente dansmemory.stat.
Récupération proactive avec memory.reclaim
Une fonctionnalité puissante de memcg (v2) est le fichier memory.reclaim. L'écriture d'une valeur dans ce fichier indique au noyau de tenter immédiatement de récupérer cette quantité de mémoire à partir de ce memcg et de tous les memcg qui lui sont associés.
Comment Android utilise memory.reclaim
L'CachedAppOptimizer d'Android utilise cette fonctionnalité pour récupérer au maximum la mémoire des applications après leur gel. L'App Freezer d'Android veille à ce que les applications mises en cache consomment le moins de RAM possible lorsqu'elles ne sont pas en cours d'exécution. Lorsqu'une application passe en arrière-plan et est gelée, le système écrit son utilisation actuelle de la mémoire dans son fichier memory.reclaim. Cela force le noyau à évincer toutes les pages de fichiers possibles et à échanger toutes les pages anonymes avec ZRAM, ce qui minimise l'empreinte résidente de l'application.
Vous pouvez obtenir manuellement la même "récupération maximale" en lisant memory.current et en l'écrivant dans 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"
Exercice : forcer la récupération et le suivi
Nous allons maintenant forcer le noyau à récupérer de la mémoire à partir de MemoryLab et à capturer l'activité dans une trace Perfetto.
- Préparation : assurez-vous que MemoryLab dispose de certaines allocations (Java et bitmaps).
Démarrer le suivi : utilisez cette configuration de suivi en ligne pour capturer les événements de planification, de récupération et de cache de page.
# 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 } } } EOFDéclencher la pression et la récupération :
- Dans MemoryLab, appuyez sur Allocate Native Memory (1GB) (Allouer de la mémoire native (1 Go)). Cela déclenchera une pression à l'échelle du système.
Dans un terminal distinct, récupérez 200 Mo :
adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
Retour à la défaillance : revenez à MemoryLab. Appuyez sur Thrash Pagecache (Refault test) (Vider le cache de page (test de défaillance)).
Arrêter le suivi : appuyez sur Ctrl+C dans le terminal de suivi.
Analyser la récupération dans Perfetto
Lorsque vous ouvrez la trace, vous pouvez voir la récupération à l'échelle du système et par application en action :

Recherchez les éléments suivants dans la trace :
- kswapd : recherchez
kswapd0sous Kernel threads (Threads du noyau) (dans la capture d'écran ci-dessus, il a été épinglé manuellement en haut). Vous le verrez se réveiller et s'exécuter (tranches vertes) lorsque le système aura du mal à trouver des pages libres. - Récupération directe : examinez les threads de processus
com.android.memorylab. Vous verrez des tranches d'événements ftrace violettes (commemm_vmscan_direct_reclaim_begin) apparaître directement sous la piste de planification du thread. Cela indique que le thread de l'application lui-même est bloqué en attendant que le noyau libère des pages. - Compteurs RSS et d'échange:
mem.rss.anon: augmente lorsque vous appuyez sur les boutons d'allocation.mem.swap: augmente de manière constante à mesure quekswapdet les propres threads de l'application (soumis à une récupération directe) compressent ces pages anonymes dans ZRAM.
- Récupération memcg : si vous effectuez un zoom avant sur le moment où vous avez déclenché la récupération manuelle, vous verrez une forte baisse de
rss.anonet derss.file, accompagnée d'événementsmm_vmscan_memcg_reclaim.
← À l'échelle du système | ↑ Haut | Interaction entre kswapd et lmkd →