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
- Linux man page: pipe(2) — Documentação oficial da chamada de sistema pipe() em Linux.
- Linux man page: mkfifo(3) — Documentação oficial da função mkfifo() para criação de FIFOs.
- Linux man page: dup2(2) — Documentação oficial sobre duplicação de descritores de arquivo.
- Beej's Guide to Unix IPC: Pipes — Tutorial abrangente sobre pipes e FIFOs com exemplos práticos em C.
- GNU C Library: Pipes and FIFOs — Seção da documentação da GNU C Library sobre pipes e FIFOs.
- Advanced Programming in the UNIX Environment (Stevens) — Livro clássico com capítulos detalhados sobre IPC, incluindo pipes.
- Tutorialspoint: Pipe System Call in C — Tutorial prático com exemplos de pipe() e redirecionamento.