Pipes: comunicação entre processos

1. Introdução aos Pipes

Pipes são um dos mecanismos mais antigos e fundamentais de comunicação entre processos (IPC) no Unix/Linux. Um pipe funciona como um canal unidirecional de dados: um processo escreve em uma extremidade e outro processo lê da outra extremidade. Os dados trafegam em ordem FIFO (first-in, first-out).

Existem dois tipos principais de pipes:

  • Pipes anônimos: criados dinamicamente por processos relacionados (tipicamente pai e filho) e desaparecem quando o último processo os fecha.
  • Pipes nomeados (FIFOs): possuem um nome no sistema de arquivos e permitem comunicação entre processos não relacionados.

O caso de uso mais típico é a comunicação entre um processo pai e seu filho, criado via fork(). O pipe estabelece um canal onde o pai escreve e o filho lê, ou vice-versa.

2. Criando e Usando Pipes Anônimos com pipe()

A chamada de sistema pipe() cria um pipe anônimo:

int pipe(int fd[2]);

Ela preenche o array fd com dois descritores:
- fd[0]: extremidade de leitura
- fd[1]: extremidade de escrita

Exemplo básico: processo pai escreve uma mensagem e o filho a lê.

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];
    pid_t pid;
    char mensagem[] = "Olá do pai!";
    char buffer[100];

    if (pipe(fd) == -1) {
        perror("pipe");
        return 1;
    }

    pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {  // Processo filho
        close(fd[1]);  // Fecha extremidade de escrita
        read(fd[0], buffer, sizeof(buffer));
        printf("Filho recebeu: %s\n", buffer);
        close(fd[0]);
    } else {  // Processo pai
        close(fd[0]);  // Fecha extremidade de leitura
        write(fd[1], mensagem, strlen(mensagem) + 1);
        close(fd[1]);
        wait(NULL);  // Aguarda o filho terminar
    }

    return 0;
}

Observação importante: cada processo deve fechar a extremidade que não utilizará. Isso evita deadlocks e libera recursos.

3. Redirecionamento de Entrada/Saída com Pipes

Combinando pipe() e dup2(), podemos redirecionar a entrada/saída padrão de um processo. dup2(fd_origem, fd_destino) duplica o descritor fd_origem para o número fd_destino.

Exemplo: implementar o pipeline ls | wc -l em C.

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    int fd[2];
    pid_t pid1, pid2;

    if (pipe(fd) == -1) {
        perror("pipe");
        return 1;
    }

    pid1 = fork();
    if (pid1 == -1) {
        perror("fork");
        return 1;
    }

    if (pid1 == 0) {  // Primeiro filho: executa ls
        close(fd[0]);               // Fecha leitura
        dup2(fd[1], STDOUT_FILENO); // Redireciona stdout para o pipe
        close(fd[1]);               // Fecha cópia original
        execlp("ls", "ls", NULL);
        perror("execlp ls");
        return 1;
    }

    pid2 = fork();
    if (pid2 == -1) {
        perror("fork");
        return 1;
    }

    if (pid2 == 0) {  // Segundo filho: executa wc -l
        close(fd[1]);               // Fecha escrita
        dup2(fd[0], STDIN_FILENO);  // Redireciona stdin do pipe
        close(fd[0]);               // Fecha cópia original
        execlp("wc", "wc", "-l", NULL);
        perror("execlp wc");
        return 1;
    }

    // Processo pai: fecha ambos os lados e aguarda os filhos
    close(fd[0]);
    close(fd[1]);
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);

    return 0;
}

Cuidados essenciais:
- Feche sempre as extremidades do pipe no pai após criar os filhos.
- Use dup2() antes de fechar o descritor original.
- Verifique o retorno de todas as chamadas de sistema.

4. Pipes Bidirecionais com Dois Pipes

Pipes são unidirecionais. Para comunicação bidirecional entre pai e filho, precisamos criar dois pipes.

Exemplo: pai envia um comando, filho processa e responde.

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd_pai_para_filho[2];
    int fd_filho_para_pai[2];
    pid_t pid;
    char comando[] = "CALCULAR";
    char resposta[100];
    int numero = 42;

    if (pipe(fd_pai_para_filho) == -1 || pipe(fd_filho_para_pai) == -1) {
        perror("pipe");
        return 1;
    }

    pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {  // Filho
        close(fd_pai_para_filho[1]); // Fecha escrita do pipe pai->filho
        close(fd_filho_para_pai[0]); // Fecha leitura do pipe filho->pai

        char buffer[100];
        read(fd_pai_para_filho[0], buffer, sizeof(buffer));
        if (strcmp(buffer, "CALCULAR") == 0) {
            int valor;
            read(fd_pai_para_filho[0], &valor, sizeof(valor));
            int resultado = valor * 2;
            write(fd_filho_para_pai[1], &resultado, sizeof(resultado));
        }

        close(fd_pai_para_filho[0]);
        close(fd_filho_para_pai[1]);
    } else {  // Pai
        close(fd_pai_para_filho[0]); // Fecha leitura do pipe pai->filho
        close(fd_filho_para_pai[1]); // Fecha escrita do pipe filho->pai

        write(fd_pai_para_filho[1], comando, strlen(comando) + 1);
        write(fd_pai_para_filho[1], &numero, sizeof(numero));

        int resultado;
        read(fd_filho_para_pai[0], &resultado, sizeof(resultado));
        printf("Pai recebeu resultado: %d\n", resultado);

        close(fd_pai_para_filho[1]);
        close(fd_filho_para_pai[0]);
        wait(NULL);
    }

    return 0;
}

5. Pipes Nomeados (FIFOs) com mkfifo()

FIFOs são pipes que existem como arquivos especiais no sistema de arquivos. Podem ser criados com a função mkfifo() ou o comando mkfifo. Permitem comunicação entre processos não relacionados.

Exemplo: um processo escritor e outro leitor.

Escritor (writer.c):

#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *fifo_path = "/tmp/meu_fifo";
    mkfifo(fifo_path, 0666);  // Cria o FIFO com permissões

    int fd = open(fifo_path, O_WRONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    char mensagem[] = "Dados do escritor!";
    write(fd, mensagem, strlen(mensagem) + 1);
    close(fd);

    return 0;
}

Leitor (reader.c):

#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    const char *fifo_path = "/tmp/meu_fifo";
    char buffer[100];

    int fd = open(fifo_path, O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    read(fd, buffer, sizeof(buffer));
    printf("Leitor recebeu: %s\n", buffer);
    close(fd);

    return 0;
}

Comportamento de bloqueio:
- open() para leitura bloqueia até que um escritor abra o FIFO.
- open() para escrita bloqueia até que um leitor abra o FIFO.
- read() bloqueia se não houver dados (a menos que todas as extremidades de escrita estejam fechadas).

6. Tratamento de Erros e Boas Práticas

Verificação de retorno: toda chamada de sistema (pipe(), fork(), dup2(), close(), read(), write()) deve ter seu retorno verificado.

if (pipe(fd) == -1) {
    perror("pipe");
    exit(EXIT_FAILURE);
}

Prevenção de deadlocks:
- Em pipes bidirecionais, nunca deixe ambos os processos esperando leitura no mesmo pipe.
- Feche extremidades não utilizadas imediatamente.
- Considere usar select() ou poll() para operações não bloqueantes.

Limpeza de FIFOs: remova o arquivo FIFO com unlink() após o uso.

unlink("/tmp/meu_fifo");

Boas práticas adicionais:
- Sempre feche descritores que não serão mais usados.
- Use wait() ou waitpid() para evitar processos zumbis.
- Em pipelines complexos, feche todos os descritores no pai antes de aguardar os filhos.

7. Comparação com Outros Mecanismos de IPC

Mecanismo Direção Complexidade Desempenho Uso típico
Pipes Unidirecional Baixa Médio Processos relacionados, pipelines shell
Signals Unidirecional (notificação) Baixa Alto Eventos assíncronos, interrupções
Memória Compartilhada Bidirecional Alta Muito alto Grandes volumes de dados, baixa latência
Sockets Bidirecional Média Médio Comunicação em rede, processos não relacionados

Pipes vs. Signals: signals são assíncronos e carregam apenas um número inteiro (o sinal). Pipes permitem transferir dados arbitrários de forma síncrona e ordenada.

Pipes vs. Memória Compartilhada: pipes são mais simples e seguros (sem necessidade de semáforos), mas têm menor desempenho por exigir cópias de dados entre kernel e espaço do usuário.

Pipes vs. Sockets: sockets são mais flexíveis (funcionam em rede) e permitem comunicação bidirecional nativa, mas têm maior overhead. Pipes locais são mais eficientes para comunicação entre processos no mesmo sistema.

Referências