Comunicação entre processos

Pipes, FIFOs, dup2, sockets, sinais e memória partilhada, com ordem e fim de dados.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

O pai produz bytes e o filho consome-os. Um pipe permite transferi-los entre memórias privadas, mas não basta escrever: o pai também tem de fechar a escrita para o filho reconhecer o fim. O canal define como os dados passam; o protocolo define quando cada processo lê, escreve e termina.

Escolher um mecanismo

Começa pelo caso mais simples: um pipe de pai para filho, com uma só direção e fim dos dados marcado por EOF. Os outros mecanismos resolvem variações deste problema: nome persistente com FIFO, dois sentidos ou rede com sockets, notificação sem dados com sinais e acesso direto com memória partilhada.

MecanismoO que transportaIdentificaçãoCondição a tratar
PipeFluxo de bytes, um sentidoDescritoresFechar todas as escritas para obter EOF
FIFOFluxo de bytes, um sentidoNome no sistema de ficheirosAbertura pode esperar pela outra ponta
SocketFluxo ou datagramasDescritor e endereçoDefinir fronteiras das mensagens
SinalNotificação de eventoPID e número simbólicoTratador restrito e sinais pendentes
Memória partilhadaBytes acessíveis pelos processosMapeamentoSincronizar acessos e tempo de vida

Partilhar memória não torna uma operação composta atómica, ou seja continua a precisar de sincronização. Receber um sinal também não entrega automaticamente uma estrutura C com todos os dados do remetente.

Pipe: leitura e escrita

pipe(fd) cria fd[0] para ler e fd[1] para escrever. Depois de fork, ambos os processos herdam ambas as pontas. Quem só lê deve fechar a escrita; quem só escreve deve fechar a leitura.

Se o buffer estiver vazio e existir uma escrita aberta, read bloqueia. Se estiver vazio e todas as pontas de escrita tiverem sido fechadas, read devolve 0, ou seja EOF. Se não houver leitores, uma escrita pode gerar SIGPIPE e falhar com EPIPE se o sinal não terminar o processo.

O seguinte programa envia uma mensagem do pai ao filho. Guarda-o em pipe.c, compila num terminal UNIX com cc -std=c17 -Wall -Wextra -Wpedantic pipe.c -o pipe e corre ./pipe.

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

static int escrever_tudo(int fd, const char *buf, size_t n) {
    size_t feito = 0;
    while (feito < n) {
        ssize_t k = write(fd, buf + feito, n - feito);
        if (k < 0 && errno == EINTR) continue;
        if (k <= 0) return -1;
        feito += (size_t)k;
    }
    return 0;
}

int main(void) {
    int fd[2];
    if (pipe(fd) < 0) { perror("pipe"); return 1; }
    pid_t filho = fork();
    if (filho < 0) {
        perror("fork");
        close(fd[0]); close(fd[1]);
        return 1;
    }
    if (filho == 0) {
        close(fd[1]);
        char buf[32];
        for (;;) {
            ssize_t n = read(fd[0], buf, sizeof buf);
            if (n < 0 && errno == EINTR) continue;
            if (n < 0) _exit(1);
            if (n == 0) break;
            if (escrever_tudo(STDOUT_FILENO, buf, (size_t)n) < 0)
                _exit(1);
        }
        close(fd[0]);
        _exit(0);
    }
    close(fd[0]);
    const char mensagem[] = "dados do pai\n";
    int erro = escrever_tudo(fd[1], mensagem, sizeof mensagem - 1) < 0;
    close(fd[1]);
    int estado;
    pid_t r;
    do { r = waitpid(filho, &estado, 0); } while (r < 0 && errno == EINTR);
    if (r < 0) { perror("waitpid"); return 1; }
    return erro || !WIFEXITED(estado) || WEXITSTATUS(estado) != 0;
}

A saída é dados do pai. Não enviamos o terminador zero, porque o filho escreve a quantidade realmente lida.

Conta os descritores depois de fork: o pai e o filho herdam as duas pontas, por isso há quatro referências abertas. O pai fecha a leitura e conserva a escrita; o filho fecha a escrita e conserva a leitura. O pai fecha a escrita antes de esperar, porque o filho precisa desse fecho para sair do ciclo. Qualquer cópia esquecida da ponta de escrita adia o EOF.

Pipes são fluxos, não caixas de mensagens. Uma escrita pode ser entregue em várias leituras. Escritas até PIPE_BUF têm uma garantia de não entrelaçamento com outras escritas no pipe, mas isso não obriga read a devolver uma mensagem inteira. Dois sentidos independentes precisam de dois pipes ou de um socket bidirecional.

Criar o canal antes de fork

O canal tem de existir antes de ser herdado. Se primeiro fizeres fork e depois cada processo chamar pipe(fd), obténs dois pipes distintos. Até podem receber os mesmos números de descritor, como 3 e 4, porque esses números só identificam posições nas tabelas locais de cada processo.

No filho que fecha a escrita do seu próprio pipe, a leitura encontra o canal vazio e sem escritores, por isso devolve EOF. No pai que fecha a leitura do outro pipe, uma tentativa de escrita não tem leitor: pode terminar com SIGPIPE, ou falhar com EPIPE se o sinal for ignorado. O pai não está a escrever no canal do filho.

A ordem correta é criar um pipe, verificar sucesso, criar o filho e fechar as pontas desnecessárias em cada processo. A mesma distinção entre número local e objeto do núcleo explica por que dup e duas chamadas independentes a open têm efeitos diferentes.

Redirecionar com dup2

dup2(origem, destino) faz destino referir a descrição aberta de origem, fechando o destino anterior quando necessário. A shell usa esta ideia para preparar stdin e stdout antes de exec.

Numa pipeline produtor | consumidor, um processo supervisor pode criar o pipe e dois filhos. No produtor:

close(fd[0]);
if (dup2(fd[1], STDOUT_FILENO) < 0) _exit(1);
close(fd[1]);
execvp(comando1[0], comando1);
_exit(127);

No consumidor, troca leitura e escrita e duplica fd[0] para STDIN_FILENO. Estes são excertos com descritores distintos dos canais padrão e vetores de argumentos terminados por NULL. O supervisor fecha ambas as pontas e recolhe os dois filhos. Se conservar a escrita, o consumidor pode nunca receber EOF. Se esperar por um produtor de dados grandes antes de lançar o consumidor, o pipe pode encher e bloquear o produtor.

FIFOs e sockets

mkfifo cria uma entrada com nome. Os dados ficam no canal do núcleo, não são armazenados como conteúdo persistente de um ficheiro regular. O nome continua a existir até ser removido, mesmo depois de todos fecharem o canal.

mkfifo canal
cat canal

No segundo terminal, printf 'ola\n' > canal permite que a leitura avance. Quando a escrita fecha, cat recebe EOF e termina. Remove canal quando acabares. Com abertura bloqueante, abrir só para leitura costuma esperar por um escritor e abrir só para escrita espera por um leitor.

socketpair(AF_UNIX, SOCK_STREAM, 0, fd) cria dois descritores bidirecionais locais. Cada processo pode ler e escrever na ponta que conserva. SOCK_STREAM continua a exigir um protocolo de mensagens: por exemplo linhas, um comprimento prefixado ou uma quantidade fixa de bytes. Não imprimas um buffer com %s sem garantir \0.

Num protocolo pedido/resposta sobre fluxo, define primeiro as fronteiras: linhas terminadas por newline, comprimento prefixado ou tamanho fixo. Depois define o fim do pedido; uma combinação comum é o servidor ler até EOF antes de responder.

O cliente que conserva a ponta aberta para receber a resposta precisa de terminar apenas a escrita com shutdown(fd, SHUT_WR). O servidor esvazia os dados pendentes e depois vê EOF, mas ainda pode responder no sentido inverso; close fecharia também a leitura do cliente. Se ambos enviarem mensagens grandes sem ler, os buffers podem encher e ambos bloquear, por isso define a ordem de troca ou trata os dois sentidos em simultâneo.

Sockets AF_UNIX também podem ser identificados por um endereço local; AF_INET permite comunicação por rede. Pai e filho são uma forma simples de distribuir descritores, não uma restrição geral a todos os sockets ou a toda a memória partilhada.

Sinais sem trabalho inseguro

Um sinal notifica um evento, como pedido de terminação ou alteração de configuração. kill envia o sinal, sujeito a permissões. Usa nomes como SIGINT, SIGTERM, SIGHUP e SIGUSR1, não números dependentes do sistema. SIGKILL e SIGSTOP não podem ser capturados nem ignorados.

Um tratador pode interromper código que já está dentro de printf ou malloc, por isso voltar a chamar essas funções no tratador não é seguro; guarda uma indicação do tipo volatile sig_atomic_t e faz o trabalho no fluxo normal. Este tipo permite a indicação simples, mas não torna qualquer estrutura partilhada segura. Os exemplos das aulas usam signal com printf no tratador por brevidade didática; em código de produção segue o padrão desta secção, com sigaction, máscara e apenas funções async-signal-safe no tratador.

O programa seguinte espera por SIGUSR1 sem a janela entre testar a condição e adormecer. Guarda-o como sinal.c e compila no terminal UNIX com cc -std=c17 -Wall -Wextra -Wpedantic sinal.c -o sinal.

#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

static volatile sig_atomic_t recebido = 0;
static void tratar(int sinal) {
    (void)sinal;
    recebido = 1;
}

int main(void) {
    sigset_t bloqueados, anterior;
    sigemptyset(&bloqueados);
    sigaddset(&bloqueados, SIGUSR1);
    if (sigprocmask(SIG_BLOCK, &bloqueados, &anterior) < 0) return 1;
    struct sigaction acao = {0};
    acao.sa_handler = tratar;
    sigemptyset(&acao.sa_mask);
    if (sigaction(SIGUSR1, &acao, NULL) < 0) return 1;
    printf("PID=%ld\n", (long)getpid());
    fflush(stdout);
    sigset_t espera = anterior;
    sigdelset(&espera, SIGUSR1);
    while (!recebido) sigsuspend(&espera);
    if (sigprocmask(SIG_SETMASK, &anterior, NULL) < 0) return 1;
    puts("SIGUSR1 recebido");
    return 0;
}

Segue a ordem segura: bloqueia SIGUSR1 com sigprocmask, instala o tratador com sigaction, mostra o PID e chama sigsuspend com uma máscara que desbloqueia o sinal. Corre ./sinal, copia o PID apresentado e envia kill -USR1 PID noutro terminal, substituindo PID. O programa mostra a mensagem e termina. sigsuspend muda temporariamente a máscara e espera de forma atómica. No padrão ingénuo while (!recebido) pause(), o sinal poderia chegar depois do teste e antes de pause, deixando o processo à espera de um segundo sinal.

Sinais tradicionais repetidos podem fundir-se enquanto estão pendentes. Não os uses como um contador fiável de mensagens. Para atualizar configuração, o tratador assinala o pedido e o ciclo principal lê o ficheiro.

Memória partilhada

mmap devolve um mapeamento ou MAP_FAILED, que não é necessariamente NULL. MAP_SHARED permite partilhar alterações; MAP_PRIVATE dá alterações privadas com copy-on-write. Um mapeamento anónimo partilhado criado antes de fork pode ser usado por pai e filho. Processos sem parentesco podem mapear o mesmo objeto partilhado.

Se o filho escreve uma mensagem no mapeamento e termina, e o pai só a lê depois de waitpid, a ordem de produção e leitura está imposta. Se ambos atualizam continuamente a mesma fila, precisam de sincronização apropriada para processos, não apenas de MAP_SHARED nem de volatile. munmap remove o mapeamento; fechar o descritor usado para o criar não o remove automaticamente.

Este exemplo completo usa um mapeamento anónimo de Linux. Guarda-o em partilha.c e compila com cc -std=c17 -Wall -Wextra -Wpedantic partilha.c -o partilha. Não usa o executor C do navegador.

#define _DEFAULT_SOURCE
#include <errno.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    size_t tamanho = sizeof(int);
    int *valor = mmap(NULL, tamanho, PROT_READ | PROT_WRITE,
                     MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (valor == MAP_FAILED) { perror("mmap"); return 1; }
    *valor = 4;
    pid_t filho = fork();
    if (filho < 0) {
        perror("fork");
        munmap(valor, tamanho);
        return 1;
    }
    if (filho == 0) {
        *valor = 9;
        _exit(0);
    }
    int estado;
    pid_t r;
    do { r = waitpid(filho, &estado, 0); } while (r < 0 && errno == EINTR);
    if (r < 0 || !WIFEXITED(estado) || WEXITSTATUS(estado) != 0) {
        munmap(valor, tamanho);
        return 1;
    }
    printf("partilhado=%d\n", *valor);
    if (munmap(valor, tamanho) < 0) { perror("munmap"); return 1; }
    return 0;
}

A saída é partilhado=9. Ao contrário da variável privada do exemplo de fork, este objeto pertence ao mapeamento partilhado. O pai espera pela terminação antes de ler; não existem duas atualizações concorrentes a coordenar neste exemplo.

Exercícios

A ponta que impede o fim

O filho fecha a sua escrita do pipe e lê em ciclo. O pai envia dados e faz waitpid sem fechar a escrita. Porque podem ficar ambos à espera? Explica também o que muda numa pipeline com dois filhos se o supervisor conservar uma cópia da escrita.

Pista

EOF depende de referências abertas, não da intenção de voltar a escrever.

Mais uma pista

O filho precisa de sair antes de waitpid poder terminar.

Solução

Depois de consumir os dados, o filho vê o buffer vazio mas há uma escrita aberta no pai, por isso bloqueia. O pai espera pelo fim do filho. Fecha a escrita antes de esperar, e fecha outras cópias que possam existir. Numa pipeline, o supervisor fecha ambas as pontas após criar os filhos; conservar uma escrita tem o mesmo efeito, mesmo que o produtor já terminou.

Confere a tua resposta:

  • Identifiquei a escrita ainda aberta no pai.
  • Fechei todas as escritas quando acabam os dados.
  • Expliquei a espera com buffer vazio.

Erros frequentes

Achar que uma read curta prova EOF ou que só a ponta do filho importa.

Voltar à explicação
Um sinal perdido antes de pause

Num programa while (!recebido) pause(), o tratador muda recebido para 1. Mostra uma ordem que deixa o programa bloqueado depois de receber o sinal.

Pista
O teste e a entrada em espera são operações separadas.
Mais uma pista

O sinal pode chegar depois de confirmar recebido=0 e antes da chamada pause.

Solução

O ciclo testa recebido=0; o sinal chega e o tratador põe 1; o tratador retorna; o programa chama pause e espera outro sinal. Bloquear o sinal durante o teste e usar sigsuspend para desbloquear e dormir atomicamente fecha essa janela.

Confere a tua resposta:

  • Coloquei a chegada entre o teste e pause.
  • Expliquei que o tratador já terminou antes de dormir.
  • Propus máscara e sigsuspend ou mecanismo equivalente.

Erros frequentes

Pensar que volatile liga atomicamente o teste à espera ou chamar printf num tratador sem verificar segurança.

Voltar à explicação
Acabar o pedido e receber a resposta

Num socketpair de fluxo, cada processo fechou a ponta que não usa. O cliente envia um pedido e começa a ler a resposta sem fechar a escrita. O servidor só responde depois de ler EOF. Porque param os dois? Como terminar o pedido sem perder a possibilidade de receber a resposta?

Pista
A ausência temporária de bytes não é EOF.
Mais uma pista
O socket permite terminar apenas um dos sentidos.
Solução

O servidor esvazia o pedido e espera mais bytes porque a escrita do cliente continua aberta. O cliente espera uma resposta que o servidor ainda não produz. Depois de enviar todo o pedido, o cliente faz shutdown(fd, SHUT_WR) e verifica o retorno. O servidor recebe os bytes restantes e depois EOF, envia a resposta e fecha. O cliente conserva a leitura até receber a resposta e só depois fecha. As transferências continuam a exigir ciclos para resultados parciais.

Confere a tua resposta:

  • Identifiquei que o servidor espera EOF e o cliente espera a resposta.
  • Usei shutdown com SHUT_WR depois de enviar todo o pedido.
  • Conservei a leitura e só fechei o descritor depois de receber a resposta.

Erros frequentes

Usar close antes de receber a resposta, ou interpretar uma leitura curta como fim do pedido.
Voltar à explicação
Descritores iguais, canais distintos

Um programa faz primeiro fork. Depois, pai e filho chamam separadamente pipe(fd) e ambos recebem os números 3 e 4. O filho fecha fd[1] e lê de fd[0]; o pai fecha fd[0] e tenta escrever em fd[1]. Os canais estão inicialmente vazios e não há outras referências. Assume sucesso na criação.

Estão a comunicar? Que acontece à leitura do filho e à escrita do pai? Corrige a ordem de criação. Na versão corrigida, o pai envia 18 bytes e o filho faz apenas uma leitura com capacidade 7: pode concluir que recebeu todo o pedido?

Pista
Identifica o objeto do núcleo referido por cada número, não apenas o número.
Solução

Não comunicam: cada chamada cria outro canal. O filho fechou o único escritor do seu canal vazio, portanto recebe 0, EOF. O pai fechou o único leitor do seu canal; a escrita gera SIGPIPE e, se o sinal não o terminar, falha com EPIPE. Os mesmos números nas duas tabelas locais não representam necessariamente o mesmo objeto.

Cria um pipe antes de fork, verifica sucesso e só depois separa as pontas entre pai e filho. Na versão corrigida, uma leitura de capacidade 7 recebe no máximo 7 dos 18 bytes e pode receber menos. O filho precisa de ler em ciclo até EOF; o pai completa as escritas e fecha a escrita antes de esperar pela terminação. Uma leitura curta não marca o fim de uma mensagem.

Confere a tua resposta:

  • Identifiquei dois pipes distintos criados depois do fork.
  • Previ EOF no filho e ausência de leitor no canal do pai.
  • Criei o canal antes do fork e conservei ciclos de transferência.
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.