Post-mortem de um desktop travando: do futex lockup ao Cgroups v2 no Linux
Voltar para blog

Post-mortem de um desktop travando: do futex lockup ao Cgroups v2 no Linux

07/10/2026 · 5 min · Linux

O desktop congelou: quando um futex lockup vira caso de Cgroups v2#

Quem utiliza a estação de trabalho diariamente com múltiplos terminais, navegadores com dezenas de abas e containers sabe que a linha entre produtividade máxima e um desktop congelado pode ser tênue. Dias atrás, me deparei com uma lentidão extrema na minha estação (Linux Mint com Cinnamon): o ponteiro do mouse movia com atraso, as janelas pararam de responder e o sistema operava aos soluços.

Em vez de forçar um desligamento no botão de energia, abri um terminal tty, coletei métricas e investiguei a causa raiz até o nível do kernel.

Abaixo, compartilho o diagnóstico detalhado, as ferramentas utilizadas e as soluções de contenção aplicadas para blindar o ambiente.


1. O sintoma e o diagnóstico inicial via htop#

Ao abrir o htop, o cenário era crítico:

Dois problemas principais ficaram evidentes:

  1. Memory Pressure e Swap Thrashing: A paginação ativa em swap causava page faults constantes ao alternar entre janelas.
  2. Crash Loop no Window Manager: O Cinnamon estava preso em um loop de renderização aguardando recursos de I/O de GPU/Xorg.

2. Descendo ao kernel: strace e contenção de futex#

Para entender o que o processo principal do Cinnamon (cinnamon --replace) estava executando, fiz um tracing de chamadas de sistema:

strace -p <PID_CINNAMON> -c -- sleep 10

O resultado revelou uma patologia clássica de concorrência:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99,37    0,230989         106      2173       113 futex
  0,18    0,000414           3       120           write
  0,16    0,000372         372         1           execve
...
100,00    0,232457          98      2349       118 total

Mais de 99,3% do tempo de CPU estava preso na syscall futex (Fast Userspace Mutex), com mais de 100 erros do tipo ETIMEDOUT/EAGAIN. O ambiente gráfico estava em Busy Waiting, tentando adquirir travas de sincronização de threads que nunca eram liberadas.


3. Investigando o subsistema do kernel: vmstat e context switching#

Para avaliar o impacto geral no agendador de tarefas (scheduler), executei o vmstat:

vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
 r  b   swpd  livre   buff  cache   si   so    bi    bo   in   cs us sy id wa st gu
 8  0 1063936 2155216 133344 3756084   38  138   465   688 3614   17 34 12 52  2  0  0
10  1 1063936 2164608 133344 3749040    0    0     0     0 4249 12005 20 38 41  0  0  0
 9  0 1063936 2162456 133344 3746108    0    0     0     0 5491 12703 37 33 29  1  0  0
 8  0 1063936 2155848 133352 3746100    0    0     0   132 5590 13532 29 55 16  1  0  0

Os dados mostraram:

Quando o consumo de CPU em modo Kernel (sy) supera o espaço de usuário (us), o sistema operacional está gastando mais energia gerenciando a fila de processos e travas de sincronização do que executando tarefas reais.


4. OOM score e processos órfãos do chromium#

Ao tentar inspecionar as threads do Cinnamon via ps -Lp, o processo já havia sido terminado, mas a carga continuava alta. Para identificar quais processos estavam mais vulneráveis ao encerramento pelo kernel:

printf "PID\tOOM_SCORE\tCOMMAND\n"
for proc in $(ls /proc | grep -E '^[0-9]+'); do
    if [ -f /proc/$proc/oom_score ]; then
        printf "$proc\t$(cat /proc/$proc/oom_score)\t$(cat /proc/$proc/comm)\n"
    fi
done | sort -k2 -nr | head -n 10

Resultado:

PID     OOM_SCORE   COMMAND
3547404 876         brave
3547749 874         brave
3547682 874         brave
3547658 872         brave

Vários subprocessos do navegador estavam com oom_score acima de 870 (escala de 0 a 1000) e consumindo entre 30% e 40% de CPU cada. Com a queda do Window Manager, esses processos filhos perderam a comunicação com o servidor gráfico e entraram em spinlocks no espaço de usuário.


5. I/o stalls: o spike silencioso do flatpak#

Após finalizar os renderers travados (pkill -9) e zerar o swap, a memória normalizou (6 GB livres), mas o sistema ainda apresentava micro-travamentos.

Rodei o iotop em modo batch para verificar o tráfego de disco:

iotop -o -b -n 2
Total DISK WRITE: 29.84 M/s
    TID  PRIO  USER     DISK WRITE  COMMAND
3683027 be/4 sr00t3d     3.04 M/s  flatpak-update-worker
    640 be/3 root       54.11 K/s  [jbd2/sda7-8]

Um processo em background (flatpak-update-worker) começou a escrever quase 30 MB/s exatamente no momento em que o sistema tentava se reestruturar. O processo jbd2 (journaling do ext4) precisou realizar sucessivos commits de transações, competindo por I/O.

Inspecionando os indicadores de PSI (Pressure Stall Information):

cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 (Sem pressão de RAM)

cat /proc/pressure/io
# some avg10=2.25 avg60=3.64 avg300=3.16
# full avg10=1.38 avg60=2.44 avg300=2.17

A métrica full avg10=1.38 confirmou que durante 1.38% do tempo todas as tarefas ativas ficaram bloqueadas aguardando a liberação da fila de I/O do disco.


6. A armadilha do zramctl --reset#

Durante a tentativa de limpar os blocos em cache do swap comprimido, executei:

swapoff /dev/zram0
zramctl --reset /dev/zram0
swapon /dev/zram0 -p 100

O comando retornou o erro: swapon: não foi possível abrir /dev/zram0: Arquivo ou diretório inexistente

Causa Raiz: O comando zramctl --reset não apenas limpa a memória, mas destrói o dispositivo de bloco virtual no kernel. Para recriá-lo corretamente:

# 1. Aloca um novo bloco virtual zram de 4GB
zramctl --find --size 4G

# 2. Formata como swap
mkswap /dev/zram0

# 3. Ativa com alta prioridade
swapon /dev/zram0 -p 100

7. Solução definitiva: isolando aplicações com Cgroups v2#

Para evitar que navegadores ou clientes Electron sequestrem a RAM e arrastem o desktop inteiro junto em caso de vazamento de memória, implementei uma contenção via Systemd Slice.

1. Criando a fatia de recursos#

Crie o arquivo ~/.config/systemd/user/apps-limited.slice:

[Unit]
Description=Slice limitada para Apps pesados
Before=slices.target

[Slice]
MemoryMax=4G
MemoryHigh=3.5G
CPUWeight=50
IOWeight=50

2. Tratando o socket de d-bus ao rodar via automações#

Ao rodar systemctl --user daemon-reload a partir de uma sessão administrativa, o erro de D-Bus ocorre porque o shell não herda as variáveis da sessão gráfica. Aponte o socket explicitamente:

USER_UID=$(id -u $USER)
XDG_RUNTIME_DIR=/run/user/$USER_UID DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$USER_UID/bus systemctl --user daemon-reload

3. Wrapper de execução automática#

Para garantir que o navegador sempre inicialize dentro da fatia limitada, criei /usr/local/bin/brave:

#!/bin/bash
TARGET_USER="$USER"
USER_ID=$(id -u $TARGET_USER)
export XDG_RUNTIME_DIR="/run/user/$USER_ID"
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus"

systemd-run --user --slice=apps-limited.slice --scope /usr/bin/brave-browser "$@"
chmod +x /usr/local/bin/brave

Com isso, ao atingir 3.5 GB (MemoryHigh), o kernel aplica throttling suave; ao atingir 4.0 GB (MemoryMax), o kernel encerra apenas a aba agressora dentro do cgroup, preservando a interface gráfica intacta.


Considerações práticas de estabilidade#

Problemas de desempenho raramente surgem de forma isolada: a saturação de memória levou ao uso excessivo de swap, gerando contenção de travas (futex) no compositor do Cinnamon, que por sua vez colapsou e deixou renderers órfãos disputando CPU com atualizações em lote de disco.

Tratar travamentos recorrentes não é uma questão de reiniciar a máquina a cada congelamento, mas de compreender a cadeia de eventos no kernel, isolar aplicações pesadas via Cgroups v2 e gerenciar o agendador de memória de forma previsível.

Este artigo foi útil?

Deixe uma reação rápida para apoiar o conteúdo:

CC BY-NC

Este post está licenciado sob CC BY-NC.

Comentários

Participe da discussão abaixo.

0 comentários