Interação entre kswapd e lmkd

Esta página aborda a interação entre o kswapd e o lmkd no Android, demonstrando como eles funcionam juntos para gerenciar a pressão de memória.

Contexto

O Android usa dois mecanismos principais para recuperar memória quando o sistema está com pouca:

  1. kswapd: um daemon padrão do kernel do Linux que recupera páginas removendo páginas limpas com suporte de arquivos ou trocando páginas anônimas para ZRAM.
  2. lmkd: daemon do Low Memory Killer do espaço do usuário do Android. Quando o kswapd não consegue acompanhar as demandas de memória e a pressão do sistema aumenta (conforme sinalizado pelo PSI), o lmkd intervém para encerrar processos e liberar blocos maiores de memória.

kswapd e lmkd são dois sistemas complementares de recuperação de memória. Eles trabalham para alcançar o mesmo objetivo (criar memória livre), mas por meios diferentes. Para comparar:

  • Recuperável x destrutivo: o kswapd descarta páginas de memória que podem ser lidas novamente do armazenamento ou troca páginas de memória para outras partições de memória ou armazenamento que podem ser lidas novamente sob demanda. O lmkd encerra processos, o que destrói o estado não volátil.
  • Largura de banda de recuperação: o kswapd gasta mais CPU para recuperar uma determinada quantidade de memória livre do que o lmkd e leva mais tempo para recuperar essa memória. O lmkd pode recuperar grandes quantidades de memória em curtos períodos de esforço relativamente pequeno. Em dispositivos móveis, o esforço da CPU se traduz não apenas em latência, mas também em consumo de bateria.

Buckets de pontuação de OOM

O Android atribui um ajuste de pontuação de falta de memória (OOM, na sigla em inglês) (oom_score_adj) aos processos com base na importância deles para o usuário. Os processos com pontuações mais altas são encerrados primeiro.

Os buckets comuns incluem:

  • Em cache (pontuação >= 900): processos que não são necessários no momento, mantidos para uma reinicialização mais rápida.
  • Serviço (pontuação 500): processos que hospedam serviços iniciados.
  • Serviço em primeiro plano (pontuação 200): processos que hospedam serviços em primeiro plano.
  • Primeiro plano (pontuação 0): o app com que o usuário está interagindo no momento.

Investigação: acionamento de PSI e LMKD

O que é o PSI?

As informações de parada de pressão (PSI, na sigla em inglês) são um recurso do kernel do Linux que fornece uma maneira canônica de medir a escassez de recursos. Ele rastreia o tempo em que as tarefas são atrasadas devido à falta de CPU, memória e E/S.

PSI de memória: "some" versus "full"

O PSI de memória rastreia especificamente atrasos causados por falta de memória (por exemplo, espera por recuperação de página ou novas falhas). Ele fornece duas métricas:

  • some: a porcentagem de tempo em que pelo menos uma tarefa foi interrompida na memória. Isso indica que a falta de memória está afetando o desempenho, mas algum trabalho ainda está em andamento.
  • full: a porcentagem de tempo em que todas as tarefas não ociosas foram interrompidas simultaneamente. Isso indica um estado crítico em que o sistema está completamente bloqueado aguardando memória.

Como o PSI aciona o LMKD

O daemon lmkd do espaço do usuário se registra no kernel para receber notificações quando os limites de PSI são excedidos (monitorando /proc/pressure/memory). Quando o sistema passa por uma alta pressão de memória (por exemplo, uma parada de full que excede uma duração configurada), o kernel ativa o lmkd. Em seguida, o lmkd avalia o estado do sistema e decide se vai encerrar processos e quais deles, com base nas pontuações de OOM e no tamanho.

Ajuste do LMKD

O comportamento do LMKD pode ser ajustado usando propriedades do sistema.

Como encontrar valores de ajuste em um dispositivo

Para encontrar os valores de ajuste atuais do LMKD em um dispositivo conectado, execute:

adb shell getprop | grep lmk

Propriedades comuns do LMKD

Confira algumas das principais propriedades usadas para ajustar o LMKD (consulte a documentação do Android para ver a lista completa):

  • ro.lmk.use_psi: defina como true para usar monitores de PSI na detecção de pressão de memória (padrão no Android 10+).
  • ro.lmk.psi_partial_stall_ms: o limite para a duração da parada (parcial) some em milissegundos para acionar um encerramento (padrão de 70 ms em dispositivos normais, 200 ms em dispositivos com pouca RAM).
  • ro.lmk.psi_complete_stall_ms: o limite de duração da interrupção de full em milissegundos para acionar um encerramento (padrão de 700 ms).
  • ro.lmk.kill_heaviest_task: se for true, o LMKD encerra o processo que usa mais memória entre os candidatos no mesmo bucket de OOM.

Valores de ajuste relevantes no dispositivo de teste (Google Pixel 8 Pro)

(Observação: padrões típicos para dispositivos de última geração com o Android 15)

  • ro.lmk.use_psi: true
  • ro.lmk.psi_partial_stall_ms: 70 (normal)
  • ro.lmk.psi_complete_stall_ms: 700 (normal)
  • ro.lmk.kill_heaviest_task: true

Verificação de ZRAM e swap

A capacidade da ZRAM afeta muito a velocidade com que a troca é preenchida e aciona as eliminações do LMKD. É possível verificar o status da ZRAM e da troca no dispositivo usando estes comandos:

# Check ZRAM sizing and usage
adb shell free -m

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

Exercício prático: acionar kswapd e lmkd

Neste exercício, você vai usar o app de exemplo KswapdLmkdLab para criar processos em diferentes buckets de OOM e, em seguida, consumir memória no processo principal para acionar kswapd e lmkd.

O app é configurado para gerar subprocessos automaticamente e alocar memória para torná-los alvos atraentes para o LMKD.

1. Criar subprocessos

O app gera automaticamente os seguintes subprocessos ao ser iniciado:

  • Processo em cache (oom_score_adj >= 900)
  • Processo de serviço (oom_score_adj 500)
  • Processo de serviço em primeiro plano (oom_score_adj 200)

Verifique os processos e as pontuações de OOM usando o ADB:

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

Exemplo de snippet de saída:

    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)

As abreviações de estado do processo correspondem a:

  • fg: primeiro plano (pontuação de OOM 0)
  • prcp: perceptível (serviço em primeiro plano, pontuação de OOM 200)
  • svc: serviço iniciado (pontuação de OOM 500)
  • cch: processo em cache (pontuação de OOM 900)

2. Acionar pressão da memória

O app vai começar a alocar memória automaticamente no processo principal. Para que o teste seja eficaz em dispositivos com muita RAM, o app também comanda os subprocessos para alocar memória (por exemplo, 500 MB cada) para que eles sejam melhores alvos para o LMKD.

Monitorar a morte de subprocessos

O app monitora a morte desses subprocessos usando o mecanismo IBinder.linkToDeath:

  1. O processo principal se vincula a um serviço no processo de destino usando bindService.
  2. Após a conexão, ele recebe o objeto IBinder que representa o serviço.
  3. Ele registra uma implementação de IBinder.DeathRecipient chamando linkToDeath no binder.
  4. Quando o processo de destino é encerrado (por exemplo, quando é encerrado pelo LMKD), o sistema chama o método binderDied(), permitindo que o app registre o evento de encerramento de forma assíncrona.

Para evitar que essa vinculação eleve a prioridade dos processos de destino, o que anularia o propósito do exercício, o app usa a flag Context.BIND_WAIVE_PRIORITY.

App de exemplo em ação

Esta é uma captura de tela do app após a conclusão de um teste em um Google Pixel 8 Pro (que tem aproximadamente 12 GB de RAM total).

Captura de tela do app de exemplo após a conclusão de um teste.
Figura 1. App de exemplo após uma execução de teste concluída em um Pixel 8 Pro.

Análise de trace do Perfetto

Para capturar a interação, use a seguinte configuração do Perfetto para capturar programação, contadores de memória, encerramentos do LMK e informações de parada de pressão (PSI, na sigla em inglês).

Configuração do Perfetto

Capture a interação executando o Perfetto com programação, contadores de memória, encerramento de LMK e informações de parada de pressão (PSI, na sigla em inglês):

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

Analisar o trace

A captura de tela abaixo foi tirada de um rastro que mostra o período pouco antes de o processo :cached (mais eliminável) ser encerrado até o processo :foreground (menos eliminável) ser encerrado.

Captura de tela da interface do Perfetto mostrando a atividade do kswapd e do lmkd.
Figura 2. Trace do Perfetto mostrando a recuperação do kswapd e as finalizações do lmkd.

É possível notar que o MemFree já está bem baixo e sob pressão constante. O kswapd está trabalhando constantemente e alivia a pressão descartando páginas mem.rss.file e movendo mem.rss.anon para mem.swap de todos os processos kswapdlmkdlab (o processo principal e os subprocessos) e provavelmente de outros processos não mostrados nesta captura de tela. Ao mesmo tempo, o app kswapdlmkdlab continua alocando memória, aumentando a pressão mais rápido do que o kswapd pode recuperar.

Quando a pressão fica muito alta, como visto em psi.mem sendo alta por um período suficiente, o lmkd é ativado e encerra alguns processos. É possível ver que lmkd respeita as pontuações de OOM, encerrando primeiro kswapdlmkdlab:cached, depois :service e, em seguida, :foreground.

Analise a linha do tempo dos eventos e reflita sobre o seguinte:

  • Como kswapd e lmkd se complementam: kswapd é o primeiro a responder à pressão de memória, mas lmkd ajuda quando a largura de banda de recuperação de kswapd não está atendendo à demanda de alocação.
  • A taxa de memória que está sendo liberada: observe o tempo de CPU gasto em kswapd e a memória que está sendo recuperada ao descartar mem.rss.file ou trocar mem.rss.anon por mem.swap. Compare isso com as curtas intermitências de atividade no lmkd, que resultam na liberação de grandes quantidades de memória privada do processo.

Consultas

Execute estas consultas na interface do Perfetto (Query SQL) para extrair dados do trace.

1. Atividade do kswapd

Para descobrir quando o daemon de troca do kernel estava ativo e em execução, execute:

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

Exemplo de resposta:

ts dur estado
30317633052969 159180 Em execução
30317633398347 3999186 Em execução
30317638448111 1847941 Em execução
30317641455354 1031006 Em execução
30317643272615 888387 Em execução

2. Encerramentos do LMKD

O LMKD grava as eliminações por falta de memória na tabela instant. Para consultar todas as eliminações do LMKD:

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

O formato do nome do evento é: lmk,<pid>,<kill_reason>,<oom_score_adj>,<min_oom_score>,<max_thrashing>.

  • kill_reason: um número inteiro que indica por que o lmkd decidiu encerrar o processo (definido em system/memory/lmkd/statslog.h):
    • 0: PRESSURE_AFTER_KILL
    • 1: NOT_RESPONDING
    • 2: LOW_SWAP_AND_THRASHING (o espaço de troca do ZRAM está extremamente baixo e ocorreu thrashing)
    • 3: LOW_MEM_AND_SWAP (o sistema está com pouca memória livre e troca 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: a porcentagem máxima de thrashing de página no momento do encerramento.

Exemplo de resposta:

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

Visualização

Para visualizar os dados, use o seguinte script gnuplot para traçar a memória livre do sistema, a atividade kswapd, as eliminações do lmkd e o PSI em uma única série temporal. O script pressupõe que ele é executado no diretório superior do build do Android.

Exemplo de gráfico de saída

O gráfico a seguir foi gerado usando o script anterior com dados de amostra representativos da execução de teste.

Gráfico de memória livre, atividade kswapd, PSI e encerramentos do lmkd.
Figura 3. Gráfico de pressão da memória mostrando memória livre, kswapd, PSI e encerramento de processos pelo lmkd.

Explicação do gráfico:

Este gráfico ilustra a interação dinâmica entre os diferentes componentes de gerenciamento de memória durante o teste de estresse:

  1. Queda na memória livre: à medida que o app principal aloca memória, a memória livre do sistema (linha roxa) diminui de forma constante.
  2. Ativação do kswapd: quando a memória livre fica abaixo de determinados limites, o kswapd é ativado (impulsos azuis) para recuperar memória ao trocar para a ZRAM. Isso é visível como picos intermitentes.
  3. Picos de PSI: como o kswapd tem dificuldade para acompanhar a alta taxa de alocação, as linhas de execução começam a travar na alocação de memória, fazendo com que o PSI (informações de travamento por pressão, na sigla em inglês) (linha verde) tenha um pico.
  4. Encerramentos do LMKD: quando o PSI atinge níveis críticos (por exemplo, excedendo o limite de ro.lmk.psi_complete_stall_ms conceitualmente) e a memória livre está extremamente baixa, o lmkd intervém encerrando processos de baixa prioridade (impulsos laranja nos tempos 30 e 45) para liberar grandes blocos de memória. Observe que, imediatamente após um encerramento, a memória livre aumenta e o PSI cai, mostrando a eficácia do encerramento em aliviar a pressão. ________________________________________________________________________________

← Recuperação de memória: remoção e troca | ↑ Voltar | Retornar páginas de memória ao SO →