Pipes em C - Sincronismo entre processos com mais de um pipe

Em nosso tutorial passado, sobre Pipes em C, falamos dessa importante ferramenta para a troca de informações e dados entre processos em um Sistema Operacional.
Agora, iremos entrar em mais detalhes sobre o uso de pipes em C.

Pipes e Sincronismo entre Processos

No exemplo passado, fizemos o uso bem básico e simples do pipe, que foi de enviar uma informação de um processo para outro, através das chamadas de sistema write e read.

Agora, vamos fazer algo um pouco mais complexo e interessante: não vamos somente enviar a informação em um sentido (escrevendo de um lado, e lendo de outro), vamos fazer com que um processos escreva e leia, e o outro também.

Vamos resolver a seguinte questão:
Crie um programa em C onde o processo pai cria um processo filho.
No processo pai, será pedido dois números inteiros, que deverão ser enviados ao filho via pipe, e este vai fazer a soma e devolver o resultado desta operação para o pai, e tudo se repete.
Isso deve ocorrer indefinidamente.

A grande dificuldade desse tipo de evento, é o sincronismo.
Ou seja, existem estágios. Em alguns momentos um processo vai escrever algum dado, e o outro não deve fazer nada, além de esperar.

Depois ocorre o contrário, o processo que esperava é que vai escrever, e o outro é que tem que esperar. Isso é o sincronismo entre pipes, saber quando cada processo deve agir.


Usando dois Pipes para trocar informações

No exemplo do tutorial passado sobre o uso de pipes em C, as informações fluíam apenas em um sentido do pipe. Ou seja, um processo escrevia e o outro lia.

Na questão proposta, isso também ocorre.
Bem como o contrário: o pai escreve e o filho lê, depois é o filho que escreve e o pai que lê.

Uma solução para este tipo de situação, bem corriqueira em sistemas operacionais, é através do uso de dois pipes. Veja a imagem que ilustra essa solução:

Como usar Pipes em C - Como Programar - Tutorial de Sistemas Operacionais
Uso de dois pipes para troca de informações entre processos
Temos dois pipes, que são representados pelos files descriptors fd1 e fd2.
O pai vai escrever no Pipe 1, o fd1 e vai ler pelo Pipe 2, o fd2.
Já o filho vai ler do Pipe 1 e escrever no Pipe 2.

Ou seja, agora ambos processos podem enviar e receber informações do outro.
Vamos ver como passar essa ideia para código (em C, claro ;)








Como usar dois Pipes em C

O grande cuidado que devemos ter quando estamos trocando informações entre processos usando mais de um pipe é saber o momento em que cada coisa vai fazer algo, ou seja, a sincronia.
No próximo item deste tutorial, temos o código em C pronto, comentando e bem organizado.
Agora vamos explicá-lo!

O grande 'segredo' da coisa é a variável 'turn', existente em cada processo, que começa com valor 0.
Turn em inglês quer dizer turno, vez, rodada...ou seja, essa variável vai ditar as regras sobre o que cada processo deve fazer em cada momento.

Depois da criação dos pipes e do uso da chamada de sistema fork(), entramos no processo pai.
Aqui, declaramos o vetor de inteiros num que vai armazenar os dois inteiros do usuário, e o inteiro soma que vai receber o valor da soma dos filhos.

Em seguida devemos fechar os 'lados' do pipe que não vamos usar no processo pai, pela figura acima vemos que é o fd1[0] e o fd2[1], através da função close.

Agora o processo pai e o filho entram em looping infinito, definido por while(1).
Vamos definir dois estados, que ditarão as regras do sincronismo.

O primeiro estado é quando turn=0, aqui vai acontecer duas coisas: o pai vai pedir os números ao usuário e o filho vai esperar por esses dados.

Os números enviados pelo pai estão em um vetor, então basta passarmos o nome desse vetor e o número de bytes que queremos passar pela função write(), este número é o tamanho da variável num (que é calculado por sizeof(num) ).
Quando o pai termina essa tarefa, ele passa o estado para turn=1

Ainda no filho, em turn=0, ele vai receber um vetor e vai fazer a sua variável numeros, que é um ponteiro, apontar para esse vetor que recebeu pela função read, e vai ler sizeof(numeros) bytes.
Em suma: ele vai receber o vetor de inteiros com duas posições, pois foi isso que o pai passou.
Agora que recebeu, também muda seu estado para turn=1

Ok, o pai enviou e o filho recebeu, essa primeira parte já foi. Agora é turn=1
Neste passo, o filho calcula a soma dos dois números (na qual o vetor numeros aponta) e manda esses eles via função write para o pipe. No fim do processo filho, ele volta para turn=0, para que tudo posso ocorrer de novo.

Do outro lado do pipe, ainda no estágio turn=1, o processo pai fica esperando um inteiro através do pipe, usando a read.
Assim que recebe, armazena esse inteiro em sua variável soma, que é um inteiro.
Por fim, o pai exibe a soma e retorna para o valor turn=0, para que o processo volte do começo.









Código comentado em C do uso de Pipes Sincronizados


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void) {
    int fd1[2], /* Pai vai escrever e Filho ler por esse file descriptor */
         fd2[2], /* Pai vai ler e o Filho escrever por esse file descriptor */
         turn=0; /* Vai definir o que cada um vai fazer (ler, escrever, aguardar...) */
    pid_t pid;   /* Armazena o pid, para o tratamento de pai e filho */

    /* Cria o pipe 1 */
    if(pipe(fd1)<0) {
        perror("pipe") ;
        return -1 ;
    }
    /* Cria o pipe 2 */
    if(pipe(fd2)<0) {
        perror("pipe") ;
        return -1 ;
    }

    /* Cria processo filho. */
    pid = fork();

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

    if(pid > 0) {    /* Processo pai*/
        int num[2],  /* Números que o processo pai lê*/
            soma;    /* Resultado da soma, recebido pelo filho*/

        /* Fechando o descritor LEITURA no primeiro pipe. */
        close(fd1[0]);
        /* Fechando o descritor ESCRITA no segundo pipe. */
        close(fd2[1]);

        while(1)
            if(turn==0){ /* Pai vai escreever */
                printf("Insira o numero 1: "); scanf("%d", &num[0]);
                printf("Insira o numero 2: "); scanf("%d", &num[1]);

                write(fd1[1], num, sizeof(num)); /* Enviando o vetor de números pro filho */
                turn=1; /* Passa para o próximo passo, que é o pai ler a soma do filho */
            }else

            if(turn==1){ /* Pai vai ler a soma */
                read(fd2[0], &soma, sizeof(soma)); /* Pai leu o resultado da soma, e armazenou no inteiro 'soma' */
                printf("Soma: %d\n\n", soma);
                turn=0;  /* Retorna pro passo anterior, pra começar tudo de novo */
            }


        close(fd2[0]);
        close(fd1[1]);

    } else {
        int numeros[2],
             soma;

        /* Fechando o descritor ESCRITA no primeiro pipe. */
        close(fd1[1]);
        /* Fechando o descritor LEITURA no segundo pipe. */
        close(fd2[0]);

        while(1){
            if(turn==0){ /* Filho vai ler o vetor de numeros do pai */
                read(fd1[0], numeros, sizeof(numeros) ); /* Recebeu o vetor de inteiros do pai e colocou no vetor 'numeros' */
                turn=1;  /* Passa para o próximo passo, que é o filho somar e escrever o resultado da soma */
            }else

            if(turn==1){ /* Filho calcula a soma e retorna pro pai */
                soma = numeros[0] + numeros[1];

                write(fd2[1], &soma, sizeof(soma)); /* Envia a soma, qúe está na variável 'soma', para o pai */
                turn=0; /* Volta para o passo anterior, que é esperar vetor de inteiros do pai */
            }
        }

        close(fd2[1]);
        close(fd1[0]);
    }

    return 0 ;
}

Veja o programa funcionando:
Como programar pipes em C - Sistemas Operacionais
Programa que usa dois pipes de maneira sincronizada

Um comentário:

Anônimo disse...

Parabéns pela explicação!! Estava com dúvidas e entendi! Reproduzi seu algoritmo, fui compilar mas não rodou, isso seria por eu usar o Windows??