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:
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).
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(...)".
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.