Programa 1 Fabio Imada

retornar

Problema do Produtor-Consumidor

Introdução

Para o estudo de comunicação interprocessos através de um unnamed pipe, o problema do produtor-consumidor será implementado em linguagem C para ser rodado em ambiente Linux.
No caso, foi utilizado o Ubuntu 9.04 para o desenvolvimento do programa que irá implementar o problema.

Criação de processos: fork()

Para que o programa produza os diversos processos, foi utilizado a função fork().
A função fork() produz um processo a partir do processo pai.
Para diferenciar qual processo é qual, a função retorna:

  • 0 para o processo filho;
  • PID do processo filho para o processo pai;
  • -1 em caso de erro;

Para gerar diversos processos foram criados vários processos a partir de um processo pai. Para tanto, utilizou-se um loop onde a condição para continuar eram: (o processo ser o pai, isto é, fork()>=0) E (a quantidade de processos criados ser menor que a quantidade total de processos desejada).

Iteração (fork()>=0 && n<N_CONSUMIDOR+N_PRODUTOR)
   |
   |-> filho, n=0
   |-> filho, n=1
   |-> filho, n=2
   |-> ...
   |-> filho, n=N_CONSUMIDOR+N_PRODUTOR-1
   V
  pai, n=N_CONSUMIDOR+N_PRODUTOR

Dessa forma, obtém-se (número desejado de processos - 1) processos filhos e 1 processo pai.
Em seguida foram atribuídas as "funções" dos processos.

Comunicação interprocessos: pipe(int fd[2])

A comunicação entre os processos ocorre através do pipe.
Utilizando o pipe, define-se que os processos consumidores utilizarão o lado de leitura apenas e os produtores, o de escrita.

É interessante destacar que:

  • O pipe é protegido de condições de corrida;
  • Para o caso de o pipe estar vazio, ele coloca o processo que queria lê-lo para dormir até que algum dado seja inserido.

Desse modo, não foi necessário implementar nenhum tipo de mutex para proteger os dados compartilhados (pipe).

Processos zombies

Normalmente, os processos filhos notificam o processo pai ao terminarem sua execução para serem retirados da tabela de processos. No entanto, os processos filhos não conseguem fazer tal notificação quando o processo pai está bloqueado.
Então esses processos já foram completamente executados e ainda não foram retirados da tabela de processos, tornando-se processos zombies.
Portanto, para gerar processos zombies bastou colocar o processo pai em um loop infinito while(1). Assim os produtores, ao terminarem sua execução, tornavam-se zombies. Os consumidores estão em um loop infinito e, por essa razão, não tornaram-se zombies: estão dormindo, aguardando a entrada de dados no pipe.

Desenvolvimento

A utilização de pipes, traz o foco da resolução apenas ao processamento paralelo, já que o pipe possui proteção contra problemas de corrida.
Notou-se a necessidade de utilizar um intervalo entre a criaćão de itens, para que os produtores passassem a produzir intercaladamente, caso contrário parecia que cada produtor estava produzindo todo seu estoque por vez; e entre o consumo dos itens, para que todos os consumidores tivessem a oportunidade de consumir, caso contrário apenas o primeiro consumidor consumia tudo o que era produzido.

É interessante notar que optou-se por terminar o processo junto com os processos produtores, já que este limita o que é produzido e, portanto, o que pode ser consumido. O término do processo pai faz com que os filhos consumidores também terminam, evitando que processos sem função (consumidores) fiquem ocupando recursos e espaço na tabela de processos. Por outro lado, há o risco de algumas mensagens não serem processadas (no entanto, aumentou-se o tempo médio de consumo dos pacotes e o que ocorreu foi que os processos consumidores utilizaram todo o conteúdo do pipe antes de terminarem).

Também foi pedido que fossem criados processos zumbis, que são processos que terminaram, mas continuam ocupando recursos e espaço na tabela de processos. Nesse exemplo, isso só poderia ser conseguido com os processos produtores (os produtores estão num loop infinito). Modificou-se então o processo pai, que ao invés de aguardar o término dos filhos produtores passaria a ficar num loop while(TRUE). Assim, impossibilitado de receber o sinal de término dos filhos e retirá-los da tabela de processos.
Tentou-se matar o processo pai apenas, mas os filhos passaram a ser filhos do processo 1 (processo raiz do Linux) e os zumbis foram imediatamente terminados.

Conteúdo do arquivo consumidor.c

#include <stdio.h>
#include <stdlib.h>
 
#define TRUE 1
#define FALSE 0
#define READ 0
#define WRITE 1
#define N_CONSUMIDOR 3
#define N_PRODUTOR 5    
#define N 100
 
int fd[2];
 
/* Para gerar o id do produto, foi considerado um máximo de 99 itens
 * por produtor! */
int produz_item(int producer, int n) {
    return producer * N + n;
}
 
/* Função executada no processo produtor */
/* São produzidas uma determinada quantidade de "itens" que são
 * posteriormente enviados. Foi colocado um intervalo randomico
 * entre cada criação para que os processos ocorram ao mesmo tempo */
 
void produtor(int prod_id) {
    int item, i, n = 0;
    close(fd[READ]);
    for (i = 0; i < N / N_PRODUTOR; i++) {
        usleep(rand() % 500000);
        item = produz_item(prod_id, n++);
        write(fd[WRITE], &item, sizeof(item));
    }
    close(fd[READ]);
}
 
/* Função executada no processo consumidor */
/* O consumidor fica esperando a chegada de "itens" indefinidamente. Após
 * o recebimento do item é impressa uma mensagem na tela para notificar o
 * usuário do recebimento e o processo aguarda um tempo aleatório pelo
 * mesmo motivo que no processo produtor. Isso impede que o programa acabe
 * uma vez que a condicão de término do processo pai é a espera pelo final 
 * de todos os  processos filhos.*/
 
void consumidor(int cons_id) {
    int item, cont_fim = 1;
    close(fd[WRITE]);
    while (read(fd[READ], &item, sizeof(int)) != 0) {
        int consumidor_id = cons_id;
        int produtor_id = item / N;
        printf("Consumidor: %d, Produtor: %d, Item: %d\n", consumidor_id,
                produtor_id, item);
        usleep(rand() % 300000);
    }
    close(fd[READ]);
}
 
int main() {
    int pid, pid1, cont;
    pipe(fd);
 
    /* Inicializando os processos */
    pid = 1;
    cont = 0;
    while ( pid != 0 && cont < N_CONSUMIDOR + N_PRODUTOR){
        pid = fork();
    if(cont == 1) pid1 = pid;
        if(pid != 0) cont += 1;
    }
 
    if(cont != N_CONSUMIDOR+N_PRODUTOR){
        cont += 1;
        if (cont <= N_CONSUMIDOR) {
            printf("Gerando consumidor %d\n", cont);
            consumidor(cont);
        } else {
            printf("Gerando produtor %d\n", cont - N_CONSUMIDOR);
            produtor(cont - N_CONSUMIDOR);
        }
        exit(EXIT_SUCCESS);
    }
    else{
        /* Para criar processos zombies, ao invés de aguardar o término dos
         * processos, devemos fazer com que o processo pai fique ocupado
         * para não conseguir receber os sinal de término do processo filho e,
         * desse modo, não retirá-lo da tabela de processos. Isto pode ser feito
         * colocando-se um while(TRUE) no lugar da iteração. */
 
        /* O uso do wait(), nesse caso, é inócuo pois é sabido que os processos
         * consumidores irão continuar travados indefinidamente enquanto
         * aguardam que algo seja adicionado ao pipe. Portanto, não é uma
         * condição de parada ideal para o processo pai (que nesse caso continua
         * executando indefinidamente) */
    for(cont = 0 ; cont < N_PRODUTOR ; cont++ )
            wait();
    }
}

Screenshots

A tela apresenta o comando para compilar o programa e sua execução:

consumidor.png

Para gerar os processos zombies, como foi descrito no desenvolvimento, substituiu-se o for(…){…} final por um while(TRUE).
Os processos zombies equivalem aos 5 consumidores, enquanto os processos em execução, aos 3 produtores e ao loop principal:

zombies.png

retornar

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License