Processos

Estados, espaço de endereçamento, fork, exec, wait, descritores e saída.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Uma shell que lança três compilações cria três processos, execuções com identidade e estado próprios. Cada um pode correr o mesmo compilador, com argumentos e dados diferentes. O PID distingue as execuções; o espaço de endereçamento separa os dados privados. Dois processos podem apresentar o mesmo endereço virtual sem esse número designar a mesma variável física.

Estado do processo

O bloco de controlo do processo, ou PCB, representa o estado que o sistema precisa de gerir: identificador PID, relação com o pai, registos guardados, estado de execução, mapa de memória, descritores, sinais, limites e dados de escalonamento. Numa implementação real, esta informação pode estar distribuída por várias estruturas.

O espaço de endereçamento contém código e dados do programa. Não o confundas com o PCB, que é informação de gestão no núcleo. O limite teórico de um endereço com mm bits é 2m2^m posições, ou seja isso não significa que cada processo tenha essa quantidade de RAM atribuída.

Terminar liberta a memória de execução e outros recursos. Pode restar um registo com PID e estado de saída para o pai consultar. Esse filho é um zombie e já não executa instruções. Um filho vivo cujo pai terminou é órfão, outra situação; em Linux pode ser adotado por um subreaper ou pelo processo de inicialização.

fork devolve duas vezes

fork() cria um filho. Retorna -1 em caso de falha, 0 no filho e o PID do filho no pai. Ambos continuam depois da chamada. A variável que recebe o retorno tem valores diferentes nas duas memórias.

Os programas desta página têm uma única thread antes de fork. Num processo com várias threads, o filho só conserva a thread que chamou fork, mas pode herdar locks obtidos pelas outras. Até exec, só deve chamar funções seguras nesse contexto, as async-signal-safe; copiar o exemplo para uma aplicação multithread exige esse cuidado.

int x = 4;
pid_t pid = fork();
if (pid == 0) x = 9;

Este é um excerto, assumindo fork bem-sucedido. O filho tem x == 9; o pai conserva x == 4. A cópia costuma ser implementada com copy-on-write: as páginas físicas podem ser partilhadas inicialmente e copiadas quando alguém escreve. A semântica dos dados privados continua a ser a de memórias separadas.

Contar processos pela árvore

Desenha cada fork como uma bifurcação com os PIDs reais. Num exemplo com pai 100: o primeiro fork cria o filho 101; depois o pai cria 102 e o filho 101 cria 103. A árvore tem quatro processos e mostra que PID identifica o processo, não o programa que ele corre.

Se todos os processos executam três chamadas sucessivas de fork, temos 1→2→4→81 \to 2 \to 4 \to 8 processos, incluindo o inicial, assumindo sucesso. Foram criados sete filhos no total.

Agora considera um ciclo em que cada filho termina imediatamente:

for (int i = 0; i < 3; i++) {
    pid_t pid = fork();
    if (pid == 0) _exit(0);
}

Há um pai que faz as três iterações, por isso ficam quatro processos criados ao longo da execução, não oito. O excerto omite tratamento de falhas e recolha; serve só para desenhar a árvore. A fórmula 2n2^n depende de cada descendente chegar às chamadas seguintes.

exec substitui, não cria

A família exec carrega outro programa no processo atual. Mantém o PID, mas substitui a imagem de memória, incluindo código, heap e pilha. Em caso de sucesso não retorna ao código antigo.

execlp recebe argumentos individuais e procura o comando no PATH. execvp recebe um vetor terminado por NULL, também com pesquisa no PATH. O primeiro argumento do novo programa costuma ser o seu nome. Passar apenas "ls -l" como nome não faz parsing de shell: tentaria encontrar um executável com esse nome inteiro.

Os descritores mantêm-se abertos salvo os marcados para fechar em exec, por exemplo com FD_CLOEXEC ou O_CLOEXEC. É isso que permite preparar redirecionamentos antes da substituição do programa.

Esperar e interpretar o estado

waitpid(pid, &estado, 0) espera pelo filho indicado e recolhe o estado. wait escolhe um filho que termine. O estado bruto não é diretamente o código de saída: testa WIFEXITED antes de WEXITSTATUS, e WIFSIGNALED antes de WTERMSIG.

O programa seguinte executa um comando com os argumentos que receber. Guarda-o em executar.c e compila num terminal UNIX com cc -std=c17 -Wall -Wextra -Wpedantic executar.c -o executar.

#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "uso: %s comando [argumentos]\n", argv[0]);
        return 1;
    }
    pid_t filho = fork();
    if (filho < 0) {
        perror("fork");
        return 1;
    }
    if (filho == 0) {
        execvp(argv[1], argv + 1);
        perror("execvp");
        _exit(127);
    }
    int estado;
    pid_t r;
    do {
        r = waitpid(filho, &estado, 0);
    } while (r < 0 && errno == EINTR);
    if (r < 0) {
        perror("waitpid");
        return 1;
    }
    if (WIFEXITED(estado)) {
        printf("estado=%d\n", WEXITSTATUS(estado));
        return WEXITSTATUS(estado);
    }
    if (WIFSIGNALED(estado))
        printf("sinal=%d\n", WTERMSIG(estado));
    return 1;
}

./executar printf 'ola\n' escreve ola e depois estado=0. O waitpid impõe que o pai só imprime depois do fim do filho. Repara em execvp(argv[1], argv + 1): argv[1] é o nome do comando e argv + 1 é o vetor de argumentos do novo programa, a começar nesse nome e terminado por NULL. Para um nome inexistente, execvp falha, o filho termina com 127 e o pai mostra esse estado. O filho não deve continuar o código da shell depois da falha.

return de uma função auxiliar só retorna dessa função. exit termina o processo, executa funções registadas com atexit e trata buffers stdio. _exit termina sem essa limpeza da biblioteca, por isso no filho que falhou antes de exec usa _exit para evitar repetir limpezas e descarregar buffers herdados do pai.

Buffers e ordem observável

Um printf("antes") sem descarga antes de fork pode deixar bytes num buffer que será copiado; se pai e filho terminarem com exit, ambos podem descarregar a cópia, por isso usa fflush antes de criar o filho quando tens saída pendente. Uma mudança de linha não garante descarga em todas as situações: num terminal, stdout costuma ter buffer por linha, mas redirecionado para ficheiro costuma ter buffer por blocos.

Duas linhas impressas por processos concorrentes não têm ordem garantida só porque aparecem nessa ordem no código de ramos diferentes. Para impores ordem, precisas de uma relação como esperar pelo fim do filho ou receber uma mensagem dele. Um sleep só atrasa, ou seja não prova sincronização.

Criar vários trabalhadores

Para lançar várias tarefas em paralelo, o pai cria os filhos primeiro e espera depois. Guarda cada PID devolvido num vetor, com o nome da tarefa correspondente. Depois percorre o vetor com waitpid, um filho de cada vez, e regista o estado de cada um. Só avança para a etapa dependente, como ligar ficheiros objeto depois de compilá-los, quando todos terminaram com sucesso. Se esperar logo em cada iteração, serializa as tarefas. Se recolher sem associar PID a tarefa, não sabe qual falhou.

O programa seguinte lança três filhos que terminam com estados distintos e recolhe cada um pelo seu PID. Guarda-o em trabalhadores.c e compila num terminal UNIX com cc -std=c17 -Wall -Wextra -Wpedantic trabalhadores.c -o trabalhadores.

#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    const char *nomes[] = {"a", "b", "c"};
    pid_t pids[3];
    for (int i = 0; i < 3; i++) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            return 1;
        }
        if (pid == 0)
            _exit(10 + i);
        pids[i] = pid;
        printf("lancei %s com PID %ld\n", nomes[i], (long)pid);
    }
    fflush(stdout);
    int falhou = 0;
    for (int i = 0; i < 3; i++) {
        int estado;
        pid_t r;
        do {
            r = waitpid(pids[i], &estado, 0);
        } while (r < 0 && errno == EINTR);
        if (r < 0) {
            perror("waitpid");
            return 1;
        }
        if (WIFEXITED(estado)) {
            printf("%s (PID %ld): estado=%d\n", nomes[i], (long)pids[i],
                   WEXITSTATUS(estado));
            if (WEXITSTATUS(estado) != 10 + i)
                falhou = 1;
        } else {
            printf("%s: fim anormal\n", nomes[i]);
            falhou = 1;
        }
    }
    return falhou;
}

Saída observada (os PIDs variam em cada execução):

lancei a com PID 31832
lancei b com PID 31833
lancei c com PID 31834
a (PID 31832): estado=10
b (PID 31833): estado=11
c (PID 31834): estado=12

Os três filhos podem executar concorrentemente porque a criação antecede a recolha. Execução simultânea depende de haver vários núcleos disponíveis. Cada estado é lido com macros depois de confirmar o tipo de fim, e o vetor associa cada PID à sua tarefa.

Uma shell repete ler, analisar, criar, executar e recolher. Comandos internos que alteram o seu estado, como cd ou exit, ficam na própria shell. Um parser que divide apenas por espaços não suporta automaticamente aspas, expansões ou pipelines.

Exercícios

Contar descendentes

Assume sucesso. Um processo faz um fork; apenas o filho faz depois outro fork. Todos terminam. Quantos processos foram criados ao todo, incluindo o inicial?

Pista
Desenha um ramo para cada retorno zero.
Solução

São 3, porque o primeiro cria um filho e o segundo, executado só nesse filho, cria mais um. A regra 2 elevado a n exigiria que todos executassem as duas chamadas.

Erros frequentes

Contar chamadas escritas em vez de processos que chegam a cada chamada.

Voltar à explicação
Mesmo endereço, valores diferentes

Antes de fork, x=4. O filho escreve x=9 e termina. O pai faz waitpid e imprime x. Os endereços virtuais apresentados são iguais. Quais são os valores e o que prova essa igualdade de endereços?

Pista
O espaço privado é copiado logicamente.
Mais uma pista

wait impõe ordem de fim, não partilha de dados privados.

Solução

O filho tem 9 e o pai imprime 4. O mesmo endereço virtual pode traduzir para dados privados diferentes, especialmente após copy-on-write. waitpid não copia os valores alterados pelo filho.

Confere a tua resposta:

  • Indiquei pai=4 e filho=9.
  • Distingui endereço virtual de partilha de variável.
  • Expliquei que wait não copia a alteração.

Erros frequentes

Usar a igualdade numérica do apontador para concluir que x está partilhado.

Voltar à explicação
Recolher vários filhos

O pai lança três compilações em paralelo com fork e exec e só depois recolhe os resultados. Descreve onde guarda os PIDs, como espera por cada filho e o que verifica antes de passar à ligação dos objetos.

Pista

Esperar dentro do ciclo de criação serializa as tarefas.

Mais uma pista
O estado bruto exige macros de interpretação.
Solução

Guarda cada PID com o nome da tarefa num vetor. Depois percorre o vetor com waitpid, um filho de cada vez, repetindo em EINTR. Confirma WIFEXITED antes de WEXITSTATUS e WIFSIGNALED antes de WTERMSIG. Só avança para a ligação quando os três estados indicam sucesso; caso contrário, identifica qual a tarefa falhada pelo PID. O programa trabalhadores.c da lição executa este desenho e mostra cada PID com o seu estado.

Confere a tua resposta:

  • Criei todos os filhos antes de esperar.
  • Associei cada PID à sua tarefa.
  • Verifiquei cada estado com as macros antes de avançar.

Erros frequentes

Esperar logo em cada iteração, ou recolher sem associar cada estado à sua tarefa.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.