3º Projeto de MC104 - SISTEMAS OPERACIONAIS

Comunicação entre Processos: Filas de Mensagens

 

Objetivos do Projeto

Desenvolver um programa com base no Programa Exemplo de modo a explorar a Comunicação entre Processos utilizando mecanismo de troca de mensagens. Dentre os objetivos deste projeto destacamos:

 

Aspectos Teóricos

A seguir são discutidos conceitos diretamente relacionados com o projeto e, deste modo, sua leitura é importante e será cobrada quando da apresentação. Caso o entendimento desses conceitos não se concretize procure reler e, se necessário, realizar pequenas experiências de programação até que ocorra o entendimento.

Comandos IPC: "ipcs" e "ipcrm"

Como mencionado, este projeto enfoca um método primário para IPC, filas de mensagens. Entretanto, antes de apresentar como é possível criar e remover esses tipos de construções na linguagem C, dois novos comandos são introduzidos pois são muito úteis ao longo deste projeto. Primeiro, o comando ipcs pode ser usado para listar os recursos IPC alocados. Segundo, o comando ipcrm pode ser usado para remover os recursos IPC alocados. Procure mais informação sobre esses comandos usando o comando man, pois você deverá usá-los ao longo deste experimento.

IPC através de Fila de Mensagem

Uma fila é uma estrutura de dados que permite atendimento FIFO (First In, First Out - primeiro que chega, primeiro que sai). Em uma fila de mensagens, a primeira mensagem que é colocada na fila é a primeira mensagem a ser lida da fila, ocorrendo um sincronismo entre origem e destino, pois as mensagens são lidas na ordem que foram enviadas. O oposto a isto é o assincronismo, onde a ordem recebida pode ser diferente da ordem enviada. Há quatro chamadas de sistema associadas com filas de mensagem:

  • msgget(...) - é usada para criar uma fila de mensagens e/ou obter o identificador de uma fila de mensagens baseado em sua chave de sistema. A chave é um número único que identifica a fila de mensagens. Cada processo que deseja se comunicar com a fila de mensagens deve conhecer a sua chave. O identificador é um número designado pelo sistema que é obtido usando a chamada msgget(...) e a chave. O identificador é um parâmetro para os outros comandos de filas de mensagens.
  • msgctl(...) - é usado para realizar operações de controle na fila (inclusive removê-la).
  • msgsnd(...) - é usada para colocar uma mensagem na fila.
  • msgrcv(...) - é usada para ler uma mensagem da fila.

O seguinte esquema ilustra como usar uma fila de mensagens: a. use "msgget(...)" para obter o número identificador correspondente a uma chave única, criando a fila de mensagens se necessário; b. use "msgsnd(...)" e "msgrcv(...)", respectivamente, para enviar e retirar dados da fila de mensagens identificada pelo número identificador previamente obtido e c. use "msgctl(...)" para remover a fila de mensagens do sistema.

Filas de mensagens são relativamente simples de usar, posto que , o Sistema Operacional controla os detalhes internos de comunicação. Quando se envia uma mensagem através da fila, qualquer processo que espera por uma mensagem naquela fila é alertado. O sistema operacional verifica a integridade da fila e não permite que dois processos tenham acesso a uma fila de modo destrutivo, não sendo necessário, portanto, travar o acesso a ela. Adicionalmente, filas de mensagens constituem um excelente mecanismo para que processos troquem informações de "controle". Embora as filas de mensagens tenham estas vantagens, elas têm duas desvantagens distintas: a. filas de mensagens são lentas em transferir grandes quantias de dados; b. limitação no tamanho do pacote de dados que pode ser transferido; por conseguinte, filas de mensagens são melhores quando taxas lentas de transferência de dados podem são utilizadas (com "bandwidth" limitado).

 

Entendendo o Programa Exemplo

O programa exemplo para recursos compartilhados procura estabelecer o tempo que leva para transferir uma mensagem através de uma fila de mensagens. É um programa simples, mas apresenta algumas técnicas interessantes que podem ser usadas em uma variedade de aplicações diferentes. Aqui está o algoritmo básico do programa:

Observe como a fila de mensagens é criada:

  if( (queue_id = msgget(key, IPC_CREAT | 0666)) == -1 ) {
    fprintf(stderr, "Não foi possível criar fila de mensagem\n");
    exit(1);
  }

No código acima, a chamada "msgget(...)" cria uma fila de mensagens. Os argumentos são uma chave única "key" (que foi escolhida com valor "1234", como declarado acima; este valor é como um nome para a fila de mensagens, qualquer número que não está sendo atualmente usado pode ser usado) e um conjunto de flags que neste caso são "IPC_CREAT" e "0666" em conjunto. "IPC_CREAT" diz que se quer criar a fila se ela não existe e "0666" são as permissões de acesso do Unix (permissão de leitura e escrita para todos). O valor de retorno é armazenado em "queue_id" (o identificador da fila de mensagem obtida com a chave "key") que será usado para outras chamadas de funções que manipulam mensagens. Note que criando a fila antes de criar os filhos com fork, ambos herdam o "queue_id" e assim a chamada "msgget(...)" só precisa de ser feita uma vez.

Observe a seguir duas estruturas importantes que são usadas para transferir dados através das filas de mensagens. A primeira estrutura é a estrutura de dados usada para o envio de dados pela fila de mensagens. É uma estrutura padrão que permanece a mesma para qualquer aplicação de fila de mensagens, apenas com o tamanho de "mtext" mudando de acordo com o tamanho dos dados que precisam ser transferidos. Esta estrutura contém dois pedaços de informação. O primeiro é o tipo da mensagem (mtype) que é escolhido pelo usuário. Para este experimento, fica sempre o mesmo. O próximo é um array de caracteres de tamanho igual ao tamanho dos dados que precisam ser transferidos. A estrutura de dados é definida abaixo.

  struct typedef  {
    long mtype;
    char mtext[sizeof(data_t)];
  }  msgbuf_t;

A estrutura seguinte é a estrutura de dados usada da informação que precisa ser transferida pela fila. Contém o número da mensagem (msg_no ? dentro do loop) e o tempo obtido antes do envio (send_time). Esta estrutura é definida pelo usuário e muda de fila de mensagens para fila de mensagens, dependendo do que o usuário desejar enviar.

  struct typedef  {
    unsigned msg_no;
    struct timeval send_time;
  }  data_t;

O seguinte esquema é usado para enviar uma mensagem pela fila:

O seguinte esquema é usado para receber uma mensagem na fila:

Vejamos em detalhes a função "Sender(...)". A função "Receiver(...)" permanece como um exercício para o aluno. Note, que a função "Sender(...)" e a função "Receiver(...)" são chamadas cada uma delas por um dos filhos do pai. Primeiro, na função "Sender(...)" declaram-se as estruturas necessárias e associa-se o ponteiro como descrito acima.

  Msgbuf_t message_buffer;
  data_t *data_ptr = (data_t *) (message_buffer.mtext);

Depois, na função "Sender(...)" entra-se em um loop de forma que a mensagem seja enviada um certo número de vezes especificado. Para esta experiência, "Sender(...)" repete um loop NO_OF_ITERATIONS vezes. Dentro do loop, na função "Sender(...)" chama-se "gettimeofday(...)" para obter o tempo em que a mensagem foi colocada na fila. O dado é então copiado na estrutura.

  gettimeofday(&send_time,NULL);
  message_buffer.mtype = MESSAGE_MTYPE;
  data_ptr->msg_no = count;
  data_ptr->send_time = send_time;

Em seguida, na função "Sender(...)" colocam-se os dados na fila através da chamada "msgsnd(...)". Nesta chamada, queue_id corresponde ao valor de retorno da chamada "msgget(...)". O segundo argumento é um ponteiro à estrutura que será transferida, neste caso "message_buffer". O terceiro argumento é o tamanho dos dados contido dentro da estrutura. O argumento final é um flag que está fixado em zero para o nosso caso.

  if( msgsnd(queue_id,(strct msgbuf *)&message_buffer,sizeof(data_t),0) == -1) {
    fprintf(stderr, "Impossivel enviar mensagem!\n");
    exit(1) ;
  }

Finalmente, em "Sender(...)" espera-se por alguns microsegundos para dar a oportunidade a "Receiver(...)" para ser executada. Isto é feito chamando "usleep(...)".

 

Descrição do Projeto

Cada projeto constitui uma atividade que precisa ser completada através de duas tarefas básicas. A primeira se refere à compilação e entendimento do programa exemplo que trata de assuntos cobertos em sala de aula e na teoria. A segunda se refere à implementação de uma modificação sobre o exemplo e/ou a geração de um novo código fonte.

No tocante a primeira tarefa, executar o programa várias vezes. Procure rodar o programa várias vezes com a máquina sem carga e outras vezes com ela carregada de processos CPU-bound, ou seja, aumente a sua carga através da execução de um número grande de programas que utilizem muita CPU, e veja o que acontece com os resultados. Explique o que fez para aumentar a carga do computador e apresente os resultados obtidos. Analise os resultados de pior caso (máximo) e os do caso médio (média). Tente entender o que está acontecendo dentro da máquina!

Para a apresentação dos resultados do programa exemplo, crie um quadro semelhante ao apresentado em seguida. Analise os resultados e tente achar um padrão.

Execução

Médio (mseg)

Máximo (mseg)

1

0.003

0.278

2

0.003

0.519

3

0.003

0.332

4

0.004

42.705

0.004

40.068

10

0.003

22.953

Um dos fatores que determinam a duração para uma mensagem ser transferida entre dois processos é o tamanho da mensagem. Altere o Programa Exemplo de maneira que:

Execute o programa modificado várias vezes. De novo, faça variar a carga da máquina como feito com o programa exemplo. Crie um quadro adequado para apresentar os resultados. O que acontece com os resultados? O que se pode afirmar em relação aos resultados que se tinha com o programa original? Analise os resultados obtidos e explique as diferenças.

Por fim, para sedimentar os conhecimentos sobre o mecanismo de IPC estudado, gere 2 novos programas. Um programa servidor de ordenação de números inteiros e outro cliente. O algoritmo básico de cada um destes programas é em seguida apresentado.

Algoritmo do Programa Servidor:

Algoritmo do Programa Cliente:

Execute o programa várias vezes alterando entre elas o TIPO das mensagens que são trocadas entre o cliente e o servidor (experimente números positivos, negativos e zero). Experimente também rodar só o cliente em todos os casos. Veja o que acontece com os resultados. Apresente os resultados obtidos. Tente entender o que está acontecendo! Analise os resultados.

 


Luís Fernando Faina - faina@facom.ufu.br
Last modified: Mon Apr 4 08:38:25 2005