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 las apps individuales.

Comprender memcg es fundamental porque es el nivel en el que el sistema puede orientar apps individuales para recuperar o establecer límites de memoria en ellas, lo que permite la administración proactiva de los espacios en 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 los procesos en grupos jerárquicos y distribuir los recursos del sistema (como la CPU, la memoria y la E/S) entre ellos. memcg es el controlador de cgroup específico 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 archivos), esa página se "carga" en el memcg. La carga total de un memcg es la suma de todas las páginas que usan todos los procesos que contiene.
  • Contabilidad jerárquica: El uso de memoria se contabiliza en el árbol. Un cargo en un cgroup secundario también se cuenta para el uso de su elemento superior.
  • Límites de memoria: Cada memcg admite un umbral de recuperación flexible a través de memory.high, que los presupuestos de memoria de la app usan para activar la recuperación orientada y la limitación de asignación sin invocar el eliminador de OOM. También admite un límite estricto a través de memory.max, que los límites de memoria del sistema usan para finalizar los procesos descontrolados cuando la recuperación no puede mantener el uso por debajo del límite.
  • Recuperación por memcg: Cuando un memcg supera su límite o cuando el sistema necesita memoria, el kernel puede orientar un memcg específico para la recuperación. Esto significa que se expulsan sus páginas de archivos o se intercambian 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 las apps. Esta estructura permite que el sistema aplique diferentes políticas a distintos tipos de apps (p. ej., en primer plano o en segundo plano).

Jerarquía de memcg de Android

  • /sys/fs/cgroup/apps/: Es la raíz de todas las aplicaciones para 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 detallado y una contabilidad para las apps con varios procesos.

Ejercicio práctico: Explora memcg

En este ejercicio, encontrarás el directorio memcg para 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. Encuentra 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, busca su directorio de cgroup. Puedes encontrar esta información 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

Ve a ese directorio y observa los archivos de claves:

# 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 cobra 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: Es la cantidad de memoria respaldada por archivos (caché de páginas). * swap: Es la cantidad de memoria que se intercambió 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 aproximadamente 10 MB por cada presión.
  5. Presiona Allocate Bitmaps. Revisa memory.stat para ver el aumento de anon.

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 de cualquier memcg que esté debajo de él.

Cómo Android usa memory.reclaim

CachedAppOptimizer de Android usa esta función para recuperar la mayor cantidad de memoria posible de las apps después de que se congelan. El App Freezer 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 escribiéndolo de nuevo 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 seguimiento

Ahora, forzaremos al kernel a recuperar memoria de MemoryLab y capturar la actividad en un registro de Perfetto.

  1. Preparación: Asegúrate de que MemoryLab tenga algunas asignaciones (Java y mapas de bits).
  2. Iniciar seguimiento: Usa esta configuración de seguimiento intercalado para capturar eventos de programación, recuperación y almacenamiento en 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. Presión de activación y recuperación:

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

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

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

Cómo analizar la recuperación en Perfetto

Cuando abres el registro, puedes ver la recuperación en todo el sistema y por app en acción:

Perfetto mostrando la recuperación directa y de memcg

Busca lo siguiente en el seguimiento:

  • 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 (porciones verdes) mientras el sistema intenta encontrar páginas libres.
  • Direct Reclaim: Observa los subprocesos de com.android.memorylab. Verás porciones de eventos de ftrace de color púrpura (como mm_vmscan_direct_reclaim_begin) que aparecen directamente debajo de la pista 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 de RSS y de 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 propios subprocesos de la app (sujetos a la 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 caída pronunciada tanto en rss.anon como en rss.file, acompañada de eventos mm_vmscan_memcg_reclaim.

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