Recuperación de memoria: expulsión y espacio de intercambio

En las secciones anteriores, vimos cómo el kernel administra la memoria en todo el sistema. En este capítulo, profundizaremos en los grupos de control de memoria (memcg), el mecanismo que usa Android para particionar y controlar el uso de memoria de apps individuales.

Comprender memcg es fundamental porque es el nivel en el que el sistema puede segmentar apps individuales para recuperarlas o establecer límites de memoria en ellas, lo que permite la administración proactiva de las huellas de memoria de las apps.

Grupos de control de memoria (memcg)

Los grupos de control (cgroups) son una función del kernel de Linux que permite organizar procesos en grupos jerárquicos y distribuir recursos del sistema (como CPU, memoria y E/S) entre ellos. memcg es el controlador de cgroup específicamente para la memoria.

Conceptos clave de memcg

  • Carga de Memcg: Cuando un proceso en un memcg asigna una página de memoria (ya sea anónima o respaldada por un archivo), esa página se "carga" al memcg. La carga total de un memcg es la suma de todas las páginas que usan todos los procesos dentro de él.
  • Contabilidad jerárquica: El uso de memoria se contabiliza en el árbol. Una carga en un cgroup secundario también cuenta para el uso de su elemento superior.
  • Límites de memoria: Cada memcg puede tener límites (como memory.max o memory.high) que activan la recuperación o incluso el eliminador OOM si se superan, independientemente de la memoria global del sistema.
  • Recuperación por memcg: Cuando un memcg supera su límite o cuando el sistema necesita memoria, el kernel puede segmentar un memcg específico para la recuperación. Esto significa expulsar sus páginas de archivos o intercambiar sus páginas anónimas a ZRAM.

La jerarquía de memcg de Android

Android usa una jerarquía específica para administrar los procesos de la app. Esta estructura permite que el sistema aplique diferentes políticas a diferentes tipos de apps (p.ej., en primer plano vs. en segundo plano).

Jerarquía de memcg de Android

  • /sys/fs/cgroup/apps/: Es la raíz de todas las aplicaciones de Android.
  • uid_<UID>/: Es un directorio para el ID de usuario de cada app. Todos los procesos que pertenecen al mismo paquete de app comparten este grupo.
  • pid_<PID>/: Es un directorio para cada proceso individual y cualquier elemento secundario que se bifurque de él. Esto permite un control y una contabilidad detallados para las apps con varios procesos.

Ejercicio práctico: Explora memcg

En este ejercicio, encontrarás el directorio memcg de MemoryLab y observarás su carga de memoria en tiempo real.

1. Inicia MemoryLab

Asegúrate de que MemoryLab se esté ejecutando en tu dispositivo.

2. Busca el memcg de MemoryLab

Primero, obtén el PID de la app de MemoryLab en ejecución:

adb shell pidof com.android.memorylab
# Example output: 11672

Ahora, ubica su directorio cgroup. Puedes encontrarlo en /proc/<PID>/cgroup:

adb shell cat /proc/11672/cgroup
# Example output: 0::/apps/uid_10274/pid_11672

En cgroup v2, la ruta de acceso después de 0:: representa la jerarquía de memcg en relación con /sys/fs/cgroup. Por lo tanto, la ruta de acceso completa es /sys/fs/cgroup/apps/uid_10274/pid_11672/.

3. Lee las estadísticas de memcg

Ingresa a ese directorio y observa los archivos clave:

# Current memory usage (in bytes)
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current
# Example output:
# 1591324672

memory.current muestra la cantidad total de memoria (en bytes) que se carga actualmente a este 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 proporciona un desglose: * anon: Cantidad de memoria anónima (montones, pilas). * file: Cantidad de memoria respaldada por archivos (caché de páginas) * swap: Cantidad de memoria intercambiada a ZRAM

4. Observa los cambios

  1. Abre MemoryLab.
  2. Toma nota del valor de memory.current.
  3. Presiona Allocate Java Memory (10MB) varias veces.
  4. Vuelve a leer memory.current. Deberías ver que aumenta en aproximadamente 10 MB por cada presión.
  5. Presiona Allocate Bitmaps. Verifica memory.stat para ver que anon aumente.

Recuperación proactiva con memory.reclaim

Una función potente de memcg (v2) es el archivo memory.reclaim. Escribir un valor en este archivo le indica al kernel que intente recuperar de inmediato esa cantidad de memoria de este memcg y cualquier memcg que esté debajo de él.

Cómo usa Android memory.reclaim

El CachedAppOptimizer de Android usa esta función para recuperar al máximo la memoria de las apps después de que se congelan. El congelador de apps de Android garantiza que las apps almacenadas en caché consuman la menor cantidad posible de RAM mientras no se ejecutan. Cuando una app pasa a segundo plano y se congela, el sistema escribe el uso de memoria actual de la app en su archivo memory.reclaim. Esto obliga al kernel a expulsar todas las páginas de archivos posibles y a intercambiar todas las páginas anónimas a ZRAM, lo que minimiza la huella residente de la app.

Puedes lograr la misma "recuperación máxima" de forma manual leyendo memory.current y volviéndola a escribir en 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"

Ejercicio: Fuerza la recuperación y el registro

Ahora obligaremos al kernel a recuperar memoria de MemoryLab y capturaremos la actividad en un registro de Perfetto.

  1. Prepara: Asegúrate de que MemoryLab tenga algunas asignaciones (Java y mapas de bits).
  2. Inicia el registro: Usa esta configuración de registro intercalada para capturar eventos de programación, recuperación y caché de páginas.

    # 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
            }
        }
    }
    EOF
    
  3. Activa la presión y la recuperación:

    • En MemoryLab, presiona Allocate Native Memory (1GB). Esto activará la presión en todo el sistema.
    • En una terminal independiente, recupera 200 MB:

      adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
      
  4. Vuelve a ingresar: Vuelve a MemoryLab. Presiona Thrash Pagecache (Refault test).

  5. Detén el registro: Presiona Ctrl+C en la terminal de registro.

Analiza la recuperación en Perfetto

Cuando abras el registro, podrás ver la recuperación en todo el sistema y por app en acción:

Perfetto muestra la recuperación directa y de memcg

Busca lo siguiente en el registro:

  • kswapd: Busca kswapd0 en Kernel threads (en la captura de pantalla anterior, se fijó manualmente en la parte superior). Verás que se activa y se ejecuta (segmentos verdes) mientras el sistema intenta encontrar páginas libres.
  • Recuperación directa: Observa los subprocesos del proceso com.android.memorylab. Verás segmentos de eventos ftrace morados (como mm_vmscan_direct_reclaim_begin) que aparecen directamente en el seguimiento de programación del subproceso. Esto indica que el subproceso de la app está detenido a la espera de que el kernel libere páginas.
  • Contadores RSS e intercambio:
    • mem.rss.anon: Aumenta a medida que presionas los botones de asignación.
    • mem.swap: Aumenta de manera constante a medida que kswapd y los subprocesos propios de la app (sujetos a recuperación directa) comprimen esas páginas anónimas en ZRAM.
  • Recuperación de memcg: Si acercas el momento en que activaste la recuperación manual, verás una disminución abrupta en rss.anon y rss.file, acompañada de eventos mm_vmscan_memcg_reclaim.

← En todo el sistema | ↑ Arriba | Interacción de kswapd y lmkd →