Usando Pipes Em C No Linux

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

Deixe um comentário