Administração Linux

Processos no Linux e systemd: ps, sinais, units, journalctl e timers

Intermediário10 min de leituraAtualizado

Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês

Um sistema Linux em execução é uma árvore de processos supervisionada pelo systemd. Este guia começa pelos processos — estados, ps e top, sinais, jobs em segundo plano e prioridade de escalonamento — e depois passa ao systemd: o que é uma unit, como escrever e endurecer um serviço, como dependências e targets ordenam o boot e quando um timer é uma resposta melhor do que uma entrada no cron.

O que você vai aprender

  • Interpretar as colunas de estado, memória e CPU na saída do ps e do top
  • Enviar o sinal certo para parar, recarregar ou inspecionar um processo e entender o que não pode ser capturado
  • Controlar jobs em primeiro e segundo plano e desvincular do terminal tarefas de longa duração
  • Escrever, habilitar e endurecer uma unit de serviço do systemd e ler seus logs com journalctl
  • Expressar a ordem de boot com dependências e targets e agendar tarefas com timers do systemd

Tudo o que roda em uma máquina Linux é um processo, e todo processo, exceto o primeiro, foi criado por outro. Esse simples fato explica muita coisa: por que uma variável do shell não sobrevive a um script, por que fechar um terminal mata o que foi iniciado nele e por que o gerenciamento de serviços é, na verdade, uma questão de parentesco e ciclo de vida, e não de iniciar programas.

Nas primeiras décadas, o Linux gerenciava esse ciclo de vida com scripts de shell em /etc/init.d. Hoje toda distribuição inicializa com o systemd, que supervisiona os serviços diretamente, acompanha seus processos em control groups, coleta a saída deles em um journal estruturado e inicia tudo em paralelo de acordo com as dependências declaradas. Este guia cobre primeiro os conceitos de processos de que você precisa no shell e depois a camada do systemd construída sobre eles.

A vida de um processo

Um processo é criado quando outro já existente chama fork(), gerando uma cópia quase idêntica, e essa cópia normalmente chama execve() para substituir sua imagem de memória por outro programa. O filho herda do pai o ambiente, os descritores de arquivo abertos, o diretório de trabalho e a umask — é justamente essa herança que faz o redirecionamento do shell funcionar, como explicado em fundamentos do shell.

Quando um processo termina, ele vira um zumbi até que o pai chame wait() para coletar o status de saída. Se o pai morrer antes, o órfão é reatribuído ao PID 1, que faz a coleta. Em um host com systemd, o PID 1 é o próprio systemd; dentro de um contêiner, é o entrypoint da sua imagem, seja ele qual for. Por isso contêineres que rodam um script de shell como PID 1 acumulam zumbis e ignoram sinais, a menos que o script tenha sido escrito para esse papel.

A coluna de estado exibida pelas ferramentas mostra pelo que o kernel está esperando:

EstadoSignificado
Rem execução ou executável — em uma CPU ou na fila de execução
Sespera interrompível — aguardando entrada, um timer, um socket
Despera não interrompível — geralmente bloqueado em I/O de disco ou NFS; não pode ser morto
Tparado, normalmente por Ctrl-Z ou SIGSTOP
Zzumbi — encerrado, status de saída ainda não coletado

Um monte de processos em D é a assinatura clássica de um problema de armazenamento, não de CPU. Essa distinção importa na hora de ler o load average, e é explorada em mais detalhes em logs e troubleshooting.

Vendo o que está rodando

O ps imprime um instantâneo. Duas invocações cobrem a maioria das necessidades: ps aux, no estilo BSD, para uma visão geral ampla, e ps -ef para as mesmas informações no estilo UNIX. A visão em árvore (forest) é a que vale a pena memorizar, porque a pergunta costuma ser sobre parentesco.

$ ps -eo pid,ppid,user,stat,pcpu,pmem,rss,etime,cmd --sort=-pcpu | head -5
  PID  PPID USER     STAT %CPU %MEM   RSS     ELAPSED CMD
 1842     1 postgres Ss    7.4  9.1 748120  3-04:11:52 /usr/lib/postgresql/16/bin/postgres
 2310  1842 postgres Ss    2.1  1.4 118004  3-04:11:50 postgres: checkpointer
 9921     1 www-data S     1.8  0.6  52208    02:14:07 php-fpm: pool www

$ ps -ef --forest | grep -A3 sshd

RSS é o resident set size, a memória física realmente mapeada; VSZ (tamanho virtual) conta o espaço de endereçamento e costuma ser enorme e sem significado para uma JVM ou um banco de dados. Compare processos pela RSS, não pela VSZ.

Para uma visão ao vivo, o top se atualiza a cada poucos segundos: pressione 1 para separar o uso de CPU por núcleo, M para ordenar por memória, P por CPU, k para enviar um sinal a um processo e f para escolher as colunas. O htop, se estiver instalado, adiciona uma árvore rolável, suporte a mouse e envio de sinais a um processo por menu. O pidstat 2, do pacote sysstat, amostra CPU, memória e I/O por processo em intervalos fixos, o que é muito mais informativo do que um único instantâneo quando você está caçando um pico intermitente.

Para encontrar um processo pelo nome existe o pgrep, e pgrep -a mostra a linha de comando completa:

$ pgrep -a -u www-data php-fpm
9921 php-fpm: pool www
9922 php-fpm: pool www

Sinais

Um sinal é uma pequena notificação assíncrona entregue a um processo. A maioria pode ser capturada por um handler, ignorada ou deixada com a ação padrão.

SinalNúmeroAção padrãoUso típico
SIGHUP1encerrardesconexão do terminal; por convenção, "recarregar a configuração" em daemons
SIGINT2encerrarCtrl-C
SIGQUIT3encerrar + coreCtrl-\; uma JVM imprime um thread dump em vez disso
SIGKILL9encerrarnão pode ser capturado; último recurso
SIGTERM15encerraro padrão educado de kill e systemctl stop
SIGSTOP / SIGCONT19 / 18parar / continuarpausa e retomada que não podem ser capturadas
SIGUSR1 / SIGUSR210 / 12encerrardefinidos pela aplicação, muitas vezes para rotação de logs ou para ligar o debug
kill 9921                 # SIGTERM by default
kill -HUP $(pgrep -o nginx)
pkill -u bob -TERM        # every process belonging to bob
kill -QUIT <jvm-pid>      # thread dump into the JVM's stdout
kill -0 9921 && echo "still alive"   # signal 0 tests existence and permission only

Partir direto para o kill -9 é o mau hábito mais comum nessa área. Ele tira do processo qualquer chance de esvaziar um buffer de escrita, fazer commit de uma transação ou limpar um arquivo de lock, e deixa para trás sockets e arquivos PID obsoletos. Envie SIGTERM, dê alguns segundos e só então escale. Se um processo ignora SIGTERM repetidamente, isso é um bug que vale reportar, não um motivo para continuar usando -9.

O SIGKILL também não ajuda com um processo preso no estado D: ele está bloqueado dentro do kernel, e o sinal só é entregue quando ele volta ao espaço de usuário. Corrija o armazenamento ou o mount de rede pelo qual ele está esperando.

Controle de jobs no shell

Shells interativos agrupam os processos de uma linha de comando em um job para que você possa suspendê-los e retomá-los.

$ ./import-data.sh
^Z
[1]+  Stopped                 ./import-data.sh
$ bg                    # resume it in the background
$ jobs -l
[1]+ 21507 Running       ./import-data.sh &
$ fg %1                 # bring it back to the foreground

& inicia um comando diretamente em segundo plano; wait bloqueia até os jobs em segundo plano terminarem. Nada disso sobrevive à sessão: quando o terminal fecha, o SIGHUP vai para o job. nohup cmd & e disown %1 contornam isso, mas em um sistema moderno as respostas honestas são um multiplexador de terminal (tmux, screen) para trabalho interativo e uma unit transitória para qualquer coisa que rode sem supervisão:

systemd-run --unit=import-once --collect /usr/local/bin/import-data.sh
journalctl -u import-once -f

Prioridade de escalonamento

O escalonador do kernel pondera os processos pelo valor nice, de -20 (o mais favorável) a 19 (o menos favorável). Só o root pode diminuir um valor nice.

nice -n 10 tar -czf /backup/site.tar.gz /var/www   # start a batch job politely
renice -n 5 -p 21507                               # adjust a running one
ionice -c 3 -p 21507                               # idle I/O class: only when nothing else wants the disk

Valores nice são uma dica, não uma cota. Quando você precisa de um teto de verdade, use os limites de control groups que o systemd expõe em qualquer unit — CPUQuota=, MemoryMax=, IOWeight= —, porque eles são impostos pelo kernel em vez de negociados pelo escalonador.

systemd: units e o arquivo de unit

O systemd gerencia units, cada uma descrita por um arquivo no estilo INI. O sufixo define o tipo: .service para um daemon ou uma tarefa pontual, .socket para ativação por socket, .timer para tarefas agendadas, .mount e .automount para sistemas de arquivos, .target para um ponto de agrupamento, .path para gatilhos no sistema de arquivos e .slice para particionamento de recursos.

Os arquivos de unit vêm de três lugares, em ordem crescente de prioridade: /usr/lib/systemd/system/ (instalados pelos pacotes; nunca edite), /etc/systemd/system/ (os seus) e diretórios drop-in como /etc/systemd/system/nginx.service.d/override.conf, para sobrescrever algumas configurações de uma unit empacotada. systemctl edit nginx cria o drop-in para você; systemctl cat nginx mostra a configuração efetiva, já mesclada.

Aqui está uma unit completa para uma aplicação Java, do tipo que você implantaria junto com o empacotamento descrito em empacotamento e deploy:

# /etc/systemd/system/orders-api.service
[Unit]
Description=Orders API
Documentation=https://example.com/runbooks/orders-api
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=exec
User=orders
Group=orders
WorkingDirectory=/opt/orders-api
EnvironmentFile=/etc/orders-api/env
ExecStart=/usr/lib/jvm/java-21-openjdk-amd64/bin/java -XX:MaxRAMPercentage=70 -jar app.jar
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s

# Hardening: each line removes something the service does not need
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/orders-api
MemoryMax=2G

[Install]
WantedBy=multi-user.target

Três campos merecem comentário. Type= diz ao systemd quando o serviço conta como iniciado: exec assim que o binário é executado, simple assim que o fork acontece, notify quando o programa sinaliza que está pronto via sd_notify, forking para daemons à moda antiga que vão sozinhos para segundo plano e oneshot para uma tarefa que roda e termina. Errar aqui é o motivo pelo qual uma unit dependente às vezes sobe antes de o serviço estar realmente escutando; notify é a opção precisa quando o programa a suporta.

Restart=on-failure reinicia depois de um código de saída diferente de zero ou de um sinal, mas não depois de uma parada limpa. Combine com StartLimitIntervalSec= e StartLimitBurst= em [Unit] para que um loop de crashes acabe desistindo em vez de martelar a máquina.

WantedBy=multi-user.target é aquilo sobre o que o systemctl enable age: habilitar cria um link simbólico em multi-user.target.wants/, e é por isso que habilitar e iniciar são operações separadas — uma afeta o próximo boot, a outra afeta o agora.

Operando o systemd

sudo systemctl daemon-reload            # after adding or editing any unit file
sudo systemctl enable --now orders-api  # start it and start it at boot
systemctl status orders-api             # state, PID, cgroup, last log lines
systemctl is-active orders-api          # scriptable: exit code plus one word
sudo systemctl restart orders-api
sudo systemctl reload orders-api        # runs ExecReload, no downtime if supported
systemctl list-units --type=service --state=running
systemctl list-unit-files --state=enabled
systemctl show orders-api -p MemoryMax -p Restart

Dois comandos se pagam quando algo está lento ou quebrado no boot: systemd-analyze blame ordena as units pelo tempo de inicialização, e systemd-analyze critical-chain mostra o caminho de dependências que de fato determinou a duração do boot. systemctl list-dependencies orders-api imprime a árvore na direção oposta.

systemctl mask é mais forte que disable: ele aponta a unit para /dev/null, de modo que nada, nem mesmo a dependência de outra unit, consegue iniciá-la. Lembre-se de fazer unmask antes de se perguntar por que o serviço de um pacote não sobe.

Lendo os logs de uma unit

O systemd captura a saída padrão e a saída de erro de cada unit no journal, marcadas com o nome da unit, o PID, o boot id e a prioridade. O journalctl faz as consultas.

journalctl -u orders-api -f                # follow, like tail -f
journalctl -u orders-api --since "1 hour ago" -p err
journalctl -b -1 -u orders-api             # previous boot
journalctl -u orders-api -o json-pretty | head -40
journalctl --disk-usage

A retenção do journal e se ele sobrevive a um reboot são configuradas em /etc/systemd/journald.conf; a questão do armazenamento e a relação com /var/log são tratadas no guia de troubleshooting.

Targets, dependências e ordenação

Um target é uma unit que agrupa outras, substituindo os antigos runlevels. multi-user.target é um sistema totalmente inicializado sem sessão gráfica; graphical.target o inclui e adiciona um display manager; rescue.target é o modo monousuário. systemctl get-default e set-default controlam qual deles é usado no boot.

Dependências e ordenação são conceitos deliberadamente separados:

  • Requires= e Wants= puxam outra unit — com Requires, sua unit falha se a outra falhar; com Wants, não.
  • After= e Before= apenas ordenam units que já estão sendo iniciadas; não puxam nada.

Uma unit com After=postgresql.service mas sem Wants= vai subir depois do banco de dados se o banco já estiver sendo iniciado de qualquer forma, e imediatamente se não estiver. Quase todo relato de "meu serviço sobe cedo demais" é um Wants= faltando ou o uso de network.target (interfaces configuradas) quando o que se queria era network-online.target (um endereço realmente utilizável).

Timers em vez de cron

Uma unit de timer ativa outra unit de acordo com um agendamento. São necessários dois arquivos: o trabalho e o gatilho.

# /etc/systemd/system/db-backup.service
[Unit]
Description=Nightly database dump

[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/db-backup.sh
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Run the nightly database dump

[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable --now db-backup.timer
systemctl list-timers --all
systemd-analyze calendar "Mon..Fri 09:00"    # verify an expression before trusting it
sudo systemctl start db-backup.service       # run it once, now, to test

Persistent=true executa um job perdido quando a máquina volta — a propriedade que falta ao cron e o motivo pelo qual backups pulam um dia silenciosamente em notebooks e instâncias spot. RandomizedDelaySec evita que cem máquinas batam no mesmo endpoint às 02:30:00 em ponto. OnUnitActiveSec= expressa "a cada 15 minutos desde a última execução" para jobs cuja duração varia. E como o trabalho é uma unit comum, a saída vai para o journal em vez de ser enviada por e-mail para uma caixa que ninguém lê.

O cron continua existindo e serve bem para um punhado de jobs pessoais; crontab -e edita sua própria tabela e /etc/cron.d/ guarda entradas do sistema com uma coluna de usuário. Só lembre que o cron roda com um ambiente mínimo e sem arquivos de login, então os scripts precisam de caminhos absolutos e de um PATH explícito.

Erros comuns

  • Editar uma unit empacotada em /usr/lib/systemd/system. A próxima atualização sobrescreve o arquivo. Use systemctl edit ou um arquivo em /etc/systemd/system.
  • Esquecer o daemon-reload. O systemd mantém os arquivos de unit em cache; uma alteração fica invisível até você recarregar, e o status não vai dizer por quê.
  • Confundir enable com start. Habilitar só afeta o boot. Use enable --now quando quiser as duas coisas.
  • Usar Type=simple para um programa que faz fork. O systemd acompanha o PID errado, o MAINPID fica incorreto, e reload e stop se comportam mal.
  • After=network.target para qualquer coisa que faça bind em um endereço público. Use network-online.target com o Wants= correspondente.
  • Matar com -9 por reflexo. Você perde o caminho de desligamento limpo que o serviço já implementa.
  • Confiar no nice para proteger uma máquina sobrecarregada. Em vez disso, defina CPUQuota= e MemoryMax= na unit.

Principais lições

  • Processos formam uma árvore; os filhos herdam ambiente e descritores, órfãos são adotados pelo PID 1, e um zumbi é um status de saída não coletado, não um programa em execução.
  • ps -eo com colunas explícitas, top/htop e pgrep respondem à maioria das perguntas do tipo "o que está rodando e por quê"; compare RSS, não VSZ.
  • Envie SIGTERM e espere antes do SIGKILL; por convenção, SIGHUP recarrega a configuração, e nada alcança um processo preso em I/O não interrompível.
  • Um arquivo de unit do systemd declara como iniciar um programa, quando ele conta como iniciado, como reiniciá-lo e no que ele pode mexer; drop-ins sobrescrevem units empacotadas com segurança.
  • Wants=/Requires= puxam units, After=/Before= apenas as ordenam, e network-online.target é do que os serviços de rede realmente precisam.
  • O journal guarda a saída de todas as units; journalctl -u … -p err --since é o caminho mais rápido para descobrir o que deu errado.
  • Timers dão às tarefas agendadas as mesmas units, o mesmo logging, as mesmas dependências e a mesma semântica de recuperação de tudo o mais que o systemd executa.

Fontes e leituras complementares

Perguntas frequentes

Por que meu serviço morre quando fecho a sessão SSH?
Um programa iniciado a partir de um shell de login pertence àquela sessão. Quando o terminal some, o kernel envia SIGHUP ao grupo em primeiro plano, e a maioria dos programas termina. Prefixar com nohup ou jogar para segundo plano com um e comercial só disfarça o problema. Tudo o que precisa sobreviver deve ficar em uma unit do systemd ou, no mínimo, em systemd-run --user --scope, que o coloca fora do cgroup da sessão.
Qual é a diferença prática entre SIGTERM e SIGKILL?
SIGTERM é um pedido educado: o processo pode capturá-lo, esvaziar buffers, fechar sockets, remover seu arquivo PID e sair de forma limpa. SIGKILL não pode ser capturado, bloqueado nem ignorado — o kernel destrói o processo na hora, então transações abertas se perdem e arquivos temporários ficam para trás. Envie sempre SIGTERM primeiro, espere e reserve o SIGKILL para um processo que realmente parou de responder.
Por que um processo aparece como defunct ou zumbi, e como removê-lo?
Um zumbi é um processo encerrado cujo status de saída ainda não foi coletado pelo pai com wait(). Ele não ocupa memória nem CPU, apenas uma entrada na tabela de processos. Não dá para matá-lo, porque ele já está morto. Corrija o pai — reinicie-o ou envie SIGCHLD a ele — e as entradas somem. Se o pai terminar, o init adota os filhos e os coleta.
Devo usar um timer do systemd ou um job do cron?
Timers se integram ao resto do systemd: a tarefa roda como uma unit, a saída vai para o journal, dependências e limites de recursos se aplicam, Persistent=true recupera execuções perdidas depois de uma parada e RandomizedDelaySec distribui a carga por uma frota inteira. O cron exige menos arquivos e serve bem para um script periódico simples em uma máquina gerenciada à mão. Em um servidor que você já administra com units, prefira timers por consistência.