Interacción entre kswapd y lmkd

En esta página, se explica la interacción entre kswapd y lmkd en Android, y se muestra cómo funcionan juntos para administrar la presión de memoria.

Fondo

Android usa dos mecanismos principales para recuperar memoria cuando el sistema se queda sin espacio:

  1. kswapd: Es un daemon estándar del kernel de Linux que recupera páginas expulsando páginas limpias respaldadas por archivos o intercambiando páginas anónimas a ZRAM.
  2. lmkd: Es el daemon de optimización de poca memoria del espacio de usuario de Android. Cuando kswapd no puede satisfacer las demandas de memoria y aumenta la presión del sistema (como lo indica la PSI), lmkd interviene para finalizar los procesos y liberar fragmentos de memoria más grandes.

kswapd y lmkd son dos sistemas complementarios de recuperación de memoria. Ambos trabajan para lograr el mismo objetivo (crear memoria libre), pero por diferentes medios. Para comparar y contrastar:

  • Recuperable frente a destructivo: kswapd descarta las páginas de memoria que se pueden volver a leer desde el almacenamiento o intercambia páginas de memoria con otras particiones de memoria o almacenamiento que se pueden volver a leer a pedido. lmkd finaliza los procesos, lo que destruye el estado no volátil.
  • Ancho de banda de recuperación: kswapd consume más CPU para recuperar una cantidad determinada de memoria libre que lmkd y tarda más en recuperar dicha memoria. lmkd puede recuperar grandes cantidades de memoria en ráfagas cortas de esfuerzo relativamente bajo. Ten en cuenta que, en los dispositivos móviles, el esfuerzo de la CPU se traduce no solo en latencia, sino también en energía de la batería.

Buckets de puntuación de OOM

Android asigna un ajuste de puntuación de falta de memoria (OOM) (oom_score_adj) a los procesos según su importancia para el usuario. Los procesos con puntuaciones más altas son los primeros en cerrarse.

Entre los buckets comunes, se incluyen los siguientes:

  • En caché (puntuación >= 900): Son procesos que no se necesitan en el momento y se conservan para un reinicio más rápido.
  • Servicio (puntuación de 500): Son procesos que alojan servicios iniciados.
  • Servicio en primer plano (puntuación de 200): Son los procesos que alojan servicios en primer plano.
  • Primer plano (puntuación 0): Es la app con la que el usuario interactúa en ese momento.

Análisis detallado: Activación de PSI y LMKD

¿Qué es PSI?

La información de bloqueo por presión (PSI) es una función del kernel de Linux que proporciona una forma canónica de medir la escasez de recursos. Realiza un seguimiento del tiempo que se retrasan las tareas debido a la escasez de CPU, memoria y E/S.

PSI de memoria: "some" frente a "full"

La PSI de memoria hace un seguimiento específico de los retrasos causados por la falta de memoria (p.ej., la espera de la recuperación de páginas o los errores de página). Proporciona dos métricas:

  • some: Es el porcentaje de tiempo durante el cual se detuvo al menos una tarea en la memoria. Esto indica que la falta de memoria afecta el rendimiento, pero que aún se está realizando trabajo.
  • full: Es el porcentaje de tiempo durante el cual todas las tareas no inactivas se detuvieron de forma simultánea. Esto indica un estado crítico en el que el sistema está completamente bloqueado esperando memoria.

Cómo PSI activa LMKD

El daemon lmkd del espacio del usuario se registra con el kernel para recibir notificaciones cuando se superan los umbrales de PSI (mediante la supervisión de /proc/pressure/memory). Cuando el sistema experimenta una alta presión de memoria (p.ej., una detención de full que supera una duración configurada), el kernel activa lmkd. Luego, lmkd evalúa el estado del sistema y decide si debe cerrar procesos y cuáles, según sus puntuaciones de OOM y su tamaño.

Ajuste de LMKD

El comportamiento de LMKD se puede ajustar con las propiedades del sistema.

Cómo encontrar valores de ajuste en un dispositivo

Para encontrar los valores de ajuste actuales de LMKD en un dispositivo conectado, ejecuta lo siguiente:

adb shell getprop | grep lmk

Propiedades comunes de LMKD

Estas son algunas de las propiedades clave que se usan para ajustar el LMKD (consulta la documentación de Android para obtener la lista completa):

  • ro.lmk.use_psi: Se establece en true para usar los monitores de PSI para la detección de presión de memoria (predeterminado en Android 10 y versiones posteriores).
  • ro.lmk.psi_partial_stall_ms: Es el umbral de duración de la detención (parcial) de some en milisegundos para activar una finalización (el valor predeterminado es 70 ms en dispositivos normales y 200 ms en dispositivos con poca RAM).
  • ro.lmk.psi_complete_stall_ms: Es el umbral de duración de la detención de full en milisegundos para activar una finalización (el valor predeterminado es 700 ms).
  • ro.lmk.kill_heaviest_task: Si es true, LMKD finaliza el proceso que usa la mayor cantidad de memoria entre los candidatos del mismo bucket de OOM.

Valores de ajuste relevantes en el dispositivo de prueba (Google Pixel 8 Pro)

(Nota: Estos son los valores predeterminados típicos para los dispositivos de alta gama que ejecutan Android 15)

  • ro.lmk.use_psi: true
  • ro.lmk.psi_partial_stall_ms: 70 (típico)
  • ro.lmk.psi_complete_stall_ms: 700 (típico)
  • ro.lmk.kill_heaviest_task: true

Verificación de ZRAM y swap

La capacidad de ZRAM afecta en gran medida la rapidez con la que se llena el intercambio y activa las finalizaciones de LMKD. Puedes verificar el estado de ZRAM y el intercambio en el dispositivo con estos comandos:

# Check ZRAM sizing and usage
adb shell free -m

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

Ejercicio práctico: Activa kswapd y lmkd

En este ejercicio, usarás la app de ejemplo KswapdLmkdLab para crear procesos en diferentes buckets de OOM y, luego, consumir memoria en el proceso principal para activar kswapd y lmkd.

La app está configurada para generar subprocesos automáticamente y asignar memoria para que sean objetivos atractivos para LMKD.

1. Crea subprocesos

La app genera automáticamente los siguientes subprocesos cuando se inicia:

  • Proceso almacenado en caché (oom_score_adj >= 900)
  • Proceso de servicio (oom_score_adj 500)
  • Proceso de servicio en primer plano (oom_score_adj 200)

Verifica los procesos y sus puntuaciones de OOM con ADB:

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

Fragmento de resultado de muestra:

    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)

Las abreviaturas de los estados del proceso corresponden a lo siguiente:

  • fg: En primer plano (puntuación de OOM de 0)
  • prcp: Perceptible (servicio en primer plano, puntuación de OOM de 200)
  • svc: Servicio iniciado (puntuación de OOM de 500)
  • cch: Proceso almacenado en caché (puntuación de OOM de 900)

2. Activa la presión de la memoria

La app comenzará a asignar memoria automáticamente en el proceso principal. Para que la prueba sea eficaz en dispositivos con mucha RAM, la app también les ordena a los subprocesos que asignen memoria (p.ej., 500 MB cada uno) para que sean mejores objetivos para el LMKD.

Supervisa la finalización de subprocesos

La app supervisa la finalización de estos subprocesos con el mecanismo IBinder.linkToDeath:

  1. El proceso principal se vincula a un servicio en el proceso de destino con bindService.
  2. Cuando se conecta, obtiene el objeto IBinder que representa el servicio.
  3. Registra una implementación de IBinder.DeathRecipient llamando a linkToDeath en el binder.
  4. Cuando el proceso de destino finaliza (por ejemplo, cuando lo finaliza LMKD), el sistema llama al método binderDied(), lo que permite que la app registre el evento de finalización de forma asíncrona.

Para evitar que esta vinculación eleve la prioridad de los procesos de destino (lo que anularía el propósito del ejercicio), la app usa la marca Context.BIND_WAIVE_PRIORITY.

App de ejemplo en acción

Esta es una captura de pantalla de la app después de que se completó una ejecución de prueba en un Google Pixel 8 Pro (que tiene aproximadamente 12 GB de RAM total).

Captura de pantalla de la app de ejemplo después de una ejecución de prueba completada.
Figura 1: App de ejemplo después de una ejecución de prueba completada en un Pixel 8 Pro.

Análisis de seguimiento de Perfetto

Para capturar la interacción, usa la siguiente configuración de Perfetto para capturar la programación, los contadores de memoria, las finalizaciones de LMK y la información de detención de presión (PSI).

Configuración de Perfetto

Captura la interacción ejecutando Perfetto con programación, contadores de memoria, finalizaciones de LMK y 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

Revisa el seguimiento

La siguiente captura de pantalla se toma de un registro que muestra el período poco antes de que se finalice el proceso :cached (más propenso a finalizarse) hasta que se finalice el proceso :foreground (menos propenso a finalizarse).

Captura de pantalla de la IU de Perfetto que muestra la actividad de kswapd y lmkd.
Figura 2: Registro de Perfetto que muestra la recuperación de kswapd y las finalizaciones de lmkd

Puedes ver que MemFree ya es bastante bajo y está bajo presión constante. kswapd trabaja constantemente y alivia la presión descartando páginas de mem.rss.file y moviendo mem.rss.anon a mem.swap de todos los procesos de kswapdlmkdlab (el proceso principal y los procesos secundarios) y, probablemente, de otros procesos que no se muestran en esta captura de pantalla. Al mismo tiempo, la app de kswapdlmkdlab sigue asignando memoria, lo que aumenta la presión más rápido de lo que kswapd puede recuperar.

Cuando la presión es demasiado alta, como se ve en el valor alto de psi.mem durante un período suficiente, lmkd se activa y finaliza algunos procesos. Puedes ver que lmkd respeta las puntuaciones de OOM, ya que finaliza primero kswapdlmkdlab:cached, luego :service y, por último, :foreground.

Revisa la cronología de los eventos y reflexiona sobre lo siguiente:

  • Cómo se complementan kswapd y lmkd: kswapd es el primero en responder a la presión de memoria, pero lmkd ayuda cuando el ancho de banda de recuperación de kswapd no cumple con la demanda de asignación.
  • La tasa de memoria que se libera: observa el tiempo de CPU que se dedica a kswapd y la memoria que se recupera descartando mem.rss.file o intercambiando mem.rss.anon por mem.swap. Compara esto con las ráfagas breves de actividad en lmkd que generan la liberación de grandes cantidades de memoria privada del proceso.

Consultas

Ejecuta estas consultas en la IU de Perfetto (Query SQL) para extraer datos de tu registro.

1. Actividad de kswapd

Para saber cuándo estuvo activo y en ejecución el daemon de intercambio del kernel, ejecuta lo siguiente:

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

Resultado de muestra:

ts dur state
30317633052969 159180 En ejecución
30317633398347 3999186 En ejecución
30317638448111 1847941 En ejecución
30317641455354 1031006 En ejecución
30317643272615 888387 En ejecución

2. Eliminaciones de LMKD

LMKD escribe las finalizaciones por poca memoria en la tabla instant. Para consultar todas las finalizaciones de LMKD, haz lo siguiente:

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

El formato del nombre del evento es el siguiente: lmk,<pid>,<kill_reason>,<oom_score_adj>,<min_oom_score>,<max_thrashing>.

  • kill_reason: Es un número entero que indica por qué lmkd decidió finalizar el proceso (definido en system/memory/lmkd/statslog.h):
    • 0: PRESSURE_AFTER_KILL
    • 1: NOT_RESPONDING
    • 2: LOW_SWAP_AND_THRASHING (el espacio de intercambio de ZRAM es extremadamente bajo y se produjo una sobrecarga)
    • 3: LOW_MEM_AND_SWAP (el sistema tiene poca memoria libre y poco espacio de intercambio de 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: Es el porcentaje máximo de hiperpaginación en el momento de la finalización.

Resultado de muestra:

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

Visualización

Para visualizar los datos, puedes usar la siguiente secuencia de comandos de gnuplot para trazar la memoria libre del sistema, la actividad de kswapd, las finalizaciones de lmkd y el PSI en una sola serie temporal. La secuencia de comandos supone que se ejecuta desde el directorio superior de la compilación de Android.

Gráfico de resultados de muestra

El siguiente gráfico se generó con la secuencia de comandos anterior y datos de muestra representativos de la ejecución de prueba.

Gráfico de memoria libre, actividad de kswapd, PSI y cierres de lmkd.
Figura 3: Gráfico de presión de la memoria que muestra la memoria libre, kswapd, PSI y las finalizaciones de lmkd.

Explicación del gráfico:

En este gráfico, se ilustra la interacción dinámica entre los diferentes componentes de la administración de la memoria durante la prueba de esfuerzo:

  1. Caída de la memoria libre: A medida que la app principal asigna memoria, la memoria libre del sistema (línea morada) disminuye de forma constante.
  2. Activación de kswapd: Cuando la memoria libre cae por debajo de ciertos límites, kswapd se activa (impulsos azules) para recuperar memoria intercambiando a ZRAM. Esto se puede ver como picos intermitentes.
  3. Picos de PSI: A medida que kswapd tiene dificultades para mantenerse al día con la alta tasa de asignación, los subprocesos comienzan a detenerse en la asignación de memoria, lo que hace que la PSI (información de detención de presión) (línea verde) aumente.
  4. Finalizaciones de LMKD: Cuando la PSI alcanza niveles críticos (p.ej., supera el umbral de ro.lmk.psi_complete_stall_ms conceptualmente) y la memoria libre es extremadamente baja, lmkd interviene finalizando los procesos de baja prioridad (impulsos naranjas en los momentos 30 y 45) para liberar grandes fragmentos de memoria. Observa que, inmediatamente después de una finalización, la memoria libre aumenta y el PSI disminuye, lo que demuestra la eficacia de la finalización para aliviar la presión. ________________________________________________________________________________

← Recuperación de memoria: expulsión e intercambio | ↑ Arriba | Devuelve páginas de memoria al SO →