Intermediário Fundamental Progresso 0%

Processos

De fork() a sinais e jobs, como o kernel cria, gerencia e encerra processos.

01. Conceito

O que é um processo

Explicação

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.

Analogia: Receita vs Cozinheiro

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.

Exemplo Concreto
$ 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)
           └─...
O que você precisa guardar
  • 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 aux lista todos os processos; pstree mostra a hierarquia
02. Identidade

PID, PPID e a árvore de processos

Explicação

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.

Exemplo Concreto
# 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
O que você precisa guardar
  • $$ = 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
03. Ciclo de Vida

fork() e exec(): como processos nascem

Explicação

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.

Analogia: Célula dividindo-se

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.

Exemplo Concreto
# 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
Erros comuns

"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.

O que você precisa guardar
  • 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
04. Estados

Estados do processo

Explicação

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.

Exemplo Concreto
# 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>
Erros comuns

"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()).

O que você precisa guardar
  • 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)
05. Controle

Sinais: SIGTERM, SIGKILL e outros

Explicação

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.

Exemplo Concreto
# 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)
Erros comuns

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 killallkill 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.

O que você precisa guardar
  • 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
06. Execução

Jobs: foreground, background e nohup

Explicação

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.

Exemplo Concreto
# 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
ℹ️ Saiba mais

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.

O que você precisa guardar
  • comando & — inicia em background
  • Ctrl+Z — pausa job atual; bg continua em background
  • jobs — lista jobs do shell; fg %N / bg %N para controlá-los
  • nohup — processo sobrevive ao fechar terminal
  • disown — desvincula job do shell (esqueceu o nohup)
07. Anatomia

O /proc/[PID]: anatomia de um processo vivo

Explicação

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
Exemplo Concreto
$ 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
ℹ️ Saiba mais

/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.

O que você precisa guardar
  • /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)
  • exe e cwd são symlinks para o executável e diretório atual
  • lsof -p PID é uma abstração amigável sobre /proc/[PID]/fd/

🃏 Flashcards

📝 Quiz: Módulo 04