Processos
De fork() a sinais e jobs, como o kernel cria, gerencia e encerra processos.
O que é um processo
Um processo é um programa em execução. Não é o arquivo do programa em disco — é a instância rodando na memória, com sua área de memória alocada, descritores de arquivos abertos, estado atual e contexto de execução.
Pense assim: o arquivo /usr/bin/python3 é o programa.
Quando você roda python3 app.py, o kernel cria um
processo — uma entidade viva com recursos próprios.
Cada processo tem um PID (Process ID) — número
único atribuído pelo kernel. O PID 1 é sempre o
processo init (ou systemd em distros
modernas) — o pai de todos os processos do sistema.
PPID é o PID do processo pai. Todo processo (exceto o PID 1) tem um pai. Se o pai morrer antes do filho, o filho é "adotado" pelo PID 1 — torna-se um processo órfão.
O programa (arquivo em disco) é como uma receita de culinária. O processo é o cozinheiro executando a receita — com ingredientes na bancada (memória alocada), utensílios em uso (descritores de arquivo) e em um passo específico da receita (estado de execução). Você pode ter vários cozinheiros executando a mesma receita ao mesmo tempo — são processos diferentes, com PIDs diferentes, mas baseados no mesmo programa.
$ ps aux | head -5
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 168548 11208 ? Ss Mai19 0:03 /sbin/init
root 2 0.0 0.0 0 0 ? S Mai19 0:00 [kthreadd]
usuario 1234 0.3 1.2 987654 51200 pts/0 Sl 10:30 0:05 python3 app.py
usuario 1235 0.0 0.0 21320 4096 pts/0 R+ 10:45 0:00 ps aux
$ pstree -p | head
systemd(1)─┬─agetty(850)
├─sshd(1100)─┬─sshd(1234)───bash(1235)───pstree(1240)
└─...
- Processo = programa em execução na memória, com PID único
- PID 1 = systemd (ou init) — o pai de todos os processos
- PPID = PID do processo pai — todo processo tem um
- Processo órfão: pai morreu antes do filho — adotado pelo PID 1
-
ps auxlista todos os processos;pstreemostra a hierarquia
PID, PPID e a árvore de processos
O kernel mantém uma tabela de processos — uma estrutura interna que registra todos os processos ativos. Cada entrada na tabela contém: PID, PPID, estado, uso de CPU/memória, arquivos abertos, e muito mais.
O PID é gerado sequencialmente pelo kernel.
Quando atinge o máximo (geralmente 32768 em sistemas de 32-bit, 4
milhões em 64-bit), o kernel reutiliza os menores disponíveis.
Você pode ver e alterar o limite em
/proc/sys/kernel/pid_max.
A árvore de processos reflete quem criou quem. No topo, o PID 1. Embaixo, todos os serviços e processos de usuário. Fechar um terminal mata todos os processos filhos daquele terminal.
# Ver PID e PPID de processos específicos
$ ps -o pid,ppid,comm -p $$
PID PPID COMMAND
1235 1100 bash
# $$ é uma variável especial: PID do shell atual
# Mostrar árvore com PIDs
$ pstree -p | grep bash
└─sshd(1234)───bash(1235)───vim(1290)
# Ver limite de PIDs
$ cat /proc/sys/kernel/pid_max
4194304
# Quantos processos existem agora?
$ ps aux | wc -l
187
-
$$= PID do shell atual;$PPID= PID do pai do shell -
ps aux— lista completa;ps -o pid,ppid,comm— colunas específicas pstree -p— visualiza a hierarquia com PIDs-
Limite de PIDs fica em
/proc/sys/kernel/pid_max
fork() e exec(): como processos nascem
fork() é a chamada de sistema que cria um novo processo. Ela faz uma cópia quase exata do processo atual — memória, descritores de arquivo, variáveis. O processo original é o pai; a cópia é o filho. Ambos continuam executando a partir do mesmo ponto.
Como saber quem é pai e quem é filho? fork() retorna
valores diferentes: no pai, retorna o
PID do filho (número > 0). No filho, retorna
0. Em caso de erro, retorna -1.
exec() substitui o processo atual por um novo programa. Não cria um novo processo — transforma o processo em execução em outro programa. O PID permanece o mesmo.
A combinação fork() + exec() é como a maioria dos
novos processos nasce no Unix: o shell faz fork() de
si mesmo, e o filho imediatamente faz exec() do
programa que você quer rodar.
fork() é como uma célula se dividindo: a célula original (pai) e a nova célula (filho) são cópias idênticas. exec() é como a nova célula se transformar em um tipo diferente — um neurônio em vez de outra célula genérica. No Unix, todo processo nasce por divisão (fork) e depois opcionalmente se transforma (exec) no programa que deve executar.
# O que acontece quando você digita: ls -la
# no bash:
# 1. bash chama fork()
# - Processo pai (bash): fork() retorna PID_filho (ex: 1291)
# - Processo filho (cópia do bash): fork() retorna 0
# 2. No filho, o código é aproximadamente:
# if (fork() == 0) {
# execve("/usr/bin/ls", ["ls", "-la"], env);
# }
# 3. exec() substitui o filho (bash) por /usr/bin/ls
# O PID continua o mesmo — mas o programa mudou
# 4. ls executa, imprime, termina
# 5. bash (pai) recebe o exit code via wait()
# strace mostra as syscalls reais:
$ strace -e trace=fork,exec bash -c "ls" 2>&1 | head
execve("/usr/bin/bash", ["bash", "-c", "ls"], ...) = 0
...
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|...) = 1291
...
execve("/usr/bin/ls", ["ls"], ...) = 0
"fork() cria um thread" — não. fork() cria um processo completo, com espaço de memória independente. Threads compartilham memória; processos não (por default). Para threads, use pthread_create() ou as abstrações da sua linguagem.
"exec() cria um processo novo" — não. exec() substitui o processo atual. O PID é o mesmo antes e depois do exec(). O que muda é o código e a memória em execução.
- fork(): cria cópia do processo. Retorna PID filho no pai, 0 no filho
- exec(): substitui o processo atual por outro programa (mesmo PID)
- fork() + exec() = como todo programa é iniciado a partir do shell
- Processo filho herda descritores de arquivo, variáveis e sinais do pai
Estados do processo
Todo processo está sempre em um estado. O estado aparece na coluna
STAT do ps aux. Saber ler esses
estados é fundamental para diagnosticar problemas de performance e
sistemas travados.
R (Running) — está usando a CPU ou está pronto para usar (na fila do scheduler). Processos em R realmente consomem CPU.
S (Sleeping — Interruptible) — aguardando um evento (I/O de rede, sinal, temporizador). Pode ser acordado a qualquer momento por um sinal. É o estado mais comum de processos ociosos.
D (Disk sleep — Uninterruptible) — aguardando I/O de dispositivo (disco, NFS). NÃO pode ser morto com SIGKILL. O kernel não entrega sinais para processos em estado D. Um processo travado aqui geralmente indica problema de hardware ou NFS pendurado.
Z (Zombie) — processo terminou, mas o pai não
coletou o exit code via wait(). Não consome CPU nem
memória real, mas ocupa uma entrada na tabela de processos. Muitos
zombies podem esgotar os PIDs disponíveis.
T (Stopped) — parado por sinal SIGSTOP ou SIGTSTP (Ctrl+Z), ou por um debugger. Não consome CPU. Pode ser retomado com SIGCONT.
# Ver estados de todos os processos
$ ps aux | awk '{print $8}' | sort | uniq -c | sort -rn
142 S # maioria dormindo (esperando eventos)
18 Ss # daemon sleeping (líder de sessão)
6 Sl # sleeping, multi-thread
3 R+ # rodando no foreground
1 Zs # ZOMBIE — alguém não chamou wait()
# Ver detalhes de um processo específico
$ ps -o pid,stat,comm -p 1234
PID STAT COMMAND
1234 Sl python3
# Letras adicionais do STAT:
# s = líder de sessão
# l = multi-thread
# + = no foreground (grupo de processo em foreground)
# < = prioridade alta (nice negativo)
# N = prioridade baixa (nice positivo)
# Identificar zombies
$ ps aux | grep Z
usuario 5678 0.0 0.0 0 0 pts/0 Z+ 11:20 0:00 [script] <defunct>
"Posso matar um processo D com kill -9" — não. Estado D (uninterruptible sleep) é imune a sinais, incluindo SIGKILL. O kernel retém os sinais até o processo sair do estado D. Se ele nunca sair, a única solução é reiniciar o sistema.
"Zombie consome muita memória" — falso. Zombie não tem memória alocada (já foi liberada ao terminar). O problema é que ele ocupa uma entrada na tabela de processos e um PID. Muitos zombies indicam bug no processo pai (que não chama wait()).
- R = rodando ou pronto para rodar (consome CPU)
- S = dormindo, pode ser acordado por sinal
- D = aguardando I/O — imune a sinais, incluindo SIGKILL
- Z = zombie — terminou, pai não fez wait()
- T = parado (Ctrl+Z ou debugger)
Sinais: SIGTERM, SIGKILL e outros
Sinais são notificações assíncronas enviadas a processos pelo kernel ou por outros processos. Cada sinal tem um número e um nome. O processo pode tratar (executar código customizado), ignorar ou deixar o comportamento padrão acontecer — exceto SIGKILL e SIGSTOP, que não podem ser capturados.
SIGTERM (15) — pede que o processo termine de forma limpa. O processo recebe o sinal, pode salvar dados, fechar conexões e então sair. Sempre tente SIGTERM primeiro.
SIGKILL (9) — mata imediatamente, sem aviso. O kernel encerra o processo diretamente — sem chance de limpar recursos, fechar arquivos ou salvar estado. Não pode ser ignorado ou bloqueado. Use como último recurso.
SIGHUP (1) — originalmente "hang up" (modem
desconectou). Hoje muitos daemons o usam para
recarregar configuração sem reiniciar:
kill -HUP $(pidof nginx).
SIGINT (2) — gerado pelo Ctrl+C no terminal. Interrompe o processo.
SIGSTOP (19) — pausa o processo completamente. Não pode ser ignorado. Ctrl+Z manda SIGTSTP (parecido, mas pode ser ignorado pelo processo).
SIGCONT (18) — continua um processo parado por SIGSTOP ou SIGTSTP.
# Listar todos os sinais disponíveis
$ kill -l | head -4
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
# Enviar SIGTERM (forma cortês — padrão do kill)
$ kill 1234
$ kill -15 1234 # equivalente
$ kill -TERM 1234 # equivalente
# SIGKILL — apenas se SIGTERM não funcionou após espera
$ kill -9 1234
$ kill -KILL 1234 # equivalente
# Recarregar configuração do nginx sem downtime
$ sudo kill -HUP $(pidof nginx)
# Enviar sinal para todos os processos de um nome
$ killall -TERM python3
$ pkill -TERM python3 # alternativa, aceita regex
# Ver qual sinal matou o último processo
$ echo $?
# 130 = terminado por SIGINT (128 + 2)
# 137 = terminado por SIGKILL (128 + 9)
# 143 = terminado por SIGTERM (128 + 15)
Usar kill -9 como primeira tentativa — é um sinal
de iniciante. SIGKILL não dá chance ao processo de limpar
recursos, fechar conexões de banco de dados, ou salvar estado.
Sempre use kill PID (SIGTERM) e aguarde alguns
segundos antes de escalar para -9.
Confundir kill e killall —
kill envia sinal para um PID específico.
killall nome mata por nome de processo.
pkill padrão aceita expressões regulares. Usar
killall -9 em um nome genérico pode matar mais processos do que o
esperado.
- SIGTERM (15): pede saída limpa — pode ser capturado. Use primeiro.
- SIGKILL (9): mata imediatamente — não pode ser ignorado. Use como último recurso.
- SIGHUP (1): recarregar configuração de daemons
- SIGINT (2): Ctrl+C — interrompe processo
- SIGSTOP/SIGCONT: pausa e retoma processo
- Exit code 128+N indica término por sinal N
Jobs: foreground, background e nohup
Um job é um processo (ou grupo de processos) gerenciado pelo shell. O shell controla se o job está em foreground (primeiro plano) ou background (segundo plano).
Foreground — processo vinculado ao terminal. O shell fica travado esperando o processo terminar — você não pode digitar outros comandos. O terminal inteiro está "ocupado".
Background — processo rodando sem bloquear o
terminal. Use & no final do comando para iniciar
em background. Use jobs para ver a lista,
fg %1 para trazer o job 1 ao foreground,
bg %1 para mandar um job parado ao background.
nohup ("no hang up") — mantém o processo rodando
mesmo após fechar o terminal. Sem nohup, fechar o terminal envia
SIGHUP para todos os processos filhos, matando-os.
nohup ./script.sh > output.log 2>&1 &
disown — remove o job da lista do shell. Fechar o terminal não enviará SIGHUP para ele. Útil quando você esqueceu de usar nohup e já iniciou o processo.
# Iniciar em background
$ ./backup.sh &
[1] 2345 # [número_job] PID
# Ver jobs do shell atual
$ jobs
[1]+ Running ./backup.sh &
[2]- Stopped vim arquivo.txt
# Trazer job 1 ao foreground
$ fg %1
# Mandar para background (se estava parado com Ctrl+Z)
$ bg %1
# Sequência completa: pausar e mandar para background
$ python3 servidor.py
^Z # Ctrl+Z — pausa, envia SIGTSTP
[1]+ Stopped python3 servidor.py
$ bg %1 # continua em background
[1]+ python3 servidor.py &
# nohup — processo sobrevive ao fechar terminal
$ nohup ./processo_longo.sh > output.log 2>&1 &
[1] 3456
nohup: ignorando entrada e redirecionando stderr para stdout
# disown — remover da lista do shell sem matar
$ ./script.sh &
[1] 4567
$ disown %1 # agora fechar terminal não o mata
# Ver processo rodando "solto" (sem terminal)
$ ps aux | grep script.sh
usuario 4567 0.1 0.0 ... ? S 11:30 script.sh
Para processos de longa duração em servidores, prefira ferramentas
dedicadas a nohup: screen ou
tmux (multiplexadores de terminal que persistem
sessões completas), ou ainda services do systemd (systemctl start meu-servico) que gerenciam reinicialização automática, logs e dependências.
comando &— inicia em background-
Ctrl+Z — pausa job atual;
bgcontinua em background -
jobs— lista jobs do shell;fg %N/bg %Npara controlá-los -
nohup— processo sobrevive ao fechar terminal -
disown— desvincula job do shell (esqueceu o nohup)
O /proc/[PID]: anatomia de um processo vivo
O diretório /proc é um
pseudo-sistema de arquivos — não existe no disco.
O kernel expõe informações internas sobre cada processo como se
fossem arquivos. Para cada processo com PID N, existe
/proc/N/ com dezenas de arquivos.
Arquivos importantes em /proc/[PID]/:
-
cmdline— o comando completo que iniciou o processo (argv) -
cwd— symlink para o diretório de trabalho atual exe— symlink para o executável do processo-
environ— variáveis de ambiente (separadas por null byte) -
fd/— diretório com symlinks para cada file descriptor aberto -
maps— regiões de memória mapeadas (código, heap, stack, libs) -
status— estado, PID, PPID, uso de memória em formato legível net/— informações de rede do processo
$ ls /proc/1234/
cmdline cwd environ exe fd fdinfo maps mem net ns root stat status
$ cat /proc/1234/status | head -8
Name: python3
State: S (sleeping)
Tgid: 1234
Pid: 1234
PPid: 1235
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
VmRSS: 51200 kB
# Ver qual executável é o processo
$ ls -la /proc/1234/exe
lrwxrwxrwx ... /proc/1234/exe -> /usr/bin/python3
# Ver arquivos abertos pelo processo (file descriptors)
$ ls -la /proc/1234/fd/
lrwxrwxrwx ... 0 -> /dev/pts/0 # stdin
lrwxrwxrwx ... 1 -> /dev/pts/0 # stdout
lrwxrwxrwx ... 2 -> /dev/pts/0 # stderr
lrwxrwxrwx ... 3 -> /var/log/app.log # arquivo aberto
# Ver argumentos do processo (separados por null)
$ cat /proc/1234/cmdline | tr '\0' ' '
python3 app.py --port 8080
# lsof é uma abstração sobre /proc/[PID]/fd
$ lsof -p 1234 | head -5
/proc também expõe informações do sistema inteiro:
/proc/cpuinfo (detalhes da CPU),
/proc/meminfo (uso de memória),
/proc/loadavg (carga do sistema),
/proc/uptime (tempo ligado). Ferramentas como
top, htop e free leem
exatamente esses arquivos por baixo dos panos.
-
/proc/[PID]/expõe estado do processo como arquivos virtuais -
status= estado, memória, UID/GID;cmdline= comando completo -
fd/= todos os file descriptors abertos (arquivos, sockets, pipes) -
exeecwdsão symlinks para o executável e diretório atual -
lsof -p PIDé uma abstração amigável sobre/proc/[PID]/fd/