Sistemas Operacionais, 10 semestre/2004
Experimento #2
Código Fonte do Programa Exemplo
1. Introdução
O experimento #1 permite o aprendizado sobre criação de processos concorrentes no Sistema Operacional Unix. Porém, a criação de processos não é de tudo útil, existem aplicações que requerem que haja algum método que permita o compartilhamento de dados (informação) entre eles. IPC (Inter-Process Communication), ou comunicação entre processos, consiste de um conjunto de métodos que permitem que processos no Sistema Operacional Unix possam se comunicar.
Como existem muitas versões de Unix, existem métodos diferentes para realização de IPC. A variante Berkley do Unix usa sockets. A variante da AT&T, o System V, usa filas de mensagens e compartilhamento de memória. Além disso, no POSIX.1b, filas de mensagens e memória compartilhada são definidas de modo diferente que no System V. Pipes constituem outro tipo de mecanismo para IPC. Até mesmo um arquivo pode ser usado para comunicação entre processos. Apesar da variedade de métodos diferentes para IPC, é importante entender e perceber como processos podem se comunicar.
Neste experimento são exploradas algumas das chamadas para mecanismos de IPC existentes no System V, ou seja, aquelas para manipulação de filas de mensagens.
Este exercício foi definido a partir dos experimentos existentes em
http://www.rt.db.erau.edu/experiments/unix-rt que pertencem ao Laboratório Embry-Riddle de tempo-real.
2. Objetivos
A seguir estão os objetivos deste experimento com relação ao aluno:
- Perceber o porquê dos processos precisarem se comunicar.
- Entender um método que permite que processos possam se comunicar.
A seguir são discutidos conceitos diretamente relacionados com o experimento, 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.
Este experimento enfoca um método primário para IPC, filas de mensagens. Antes de apresentar como é possível criar e remover esses tipos de construções na linguagem C, dois novos comandos são introduzidos, pois deverão ser muito úteis ao longo deste e dos futuros experimentos. 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.
Filas 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 assíncronismo, onde a ordem recebida pode ser diferente da ordem enviada. Há quatro chamadas de sistema associadas com filas de mensagem:
msgget () msgctl () msgsnd () msgrcv ()
O seguinte esquema ilustra como usar uma fila de mensagens:
- Use msgget () para obter o número ID correspondente a uma chave única, criando a fila de mensagens se necessário.
- Use msgsnd () e msgrcv (), respectivamente, para transferir dados para e retirar dados da fila de mensagens identificada pelo número ID previamente obtido.
- Use msgctl () para remover a fila de mensagens do sistema.
Filas de mensagens são relativamente simples de usar. 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 processo algum tenha acesso a uma fila de modo destrutivo. Embora as filas de mensagens tenham estas vantagens, elas têm duas desvantagens distintas. Filas de mensagens são lentas em transferir grandes quantias de dados e há um limite claro para o tamanho do pacote de dados que pode ser transferido. Então, filas de mensagens são melhores quando taxas lentas de transferência de dados podem ser usadas (com "bandwidth" limitado). Filas de mensagens são um excelente modo de IPC para processos passarem informação de "controle" para outro processo.
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 para o programa:
- O pai cria a fila de mensagens
- O pai cria dois filhos
- O primeiro filho vai:
- Receber uma mensagem da fila que contém um valor de tempo
- Chamar gettimeofday () para adquirir a hora atual
- Usando o tempo que se encontra na mensagem, calcular a diferença entre os mesmos
- Repetir os passos acima um número determinado de vezes
- Exibir os resultados
- O segundo filho vai:
- Chamar gettimeofday () para adquirir a hora atual
- Colocar o tempo obtido em uma mensagem
- Colocar a mensagem na fila .
- Dormir para permitir que o irmão possa ser executado.
- Repetir os passos acima um número determinado de vezes.
- O pai espera os filhos terminarem
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");
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 é queue_id 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 os filhos 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 é 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, com o tamanho de mtext que muda de acordo com o tamanho dos dados que precisam ser transferidos. Esta 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 é usada para os dados que precisam ser transferidos 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:
- Declare uma estrutura do tipo msgbuf_t e um ponteiro para a estrutura data_t.
- Associe o ponteiro data_t ao endereço de mtext na estrutura msgbuf_t.
- Usando o ponteiro de data_t, preencha os dados a serem enviados.
- Chame msgsnd () para enviar a mensagem na fila.
O seguinte esquema é usado para receber uma mensagem na fila:
- Declare uma estrutura do tipo msgbuf_t e um ponteiro para a estrutura data_t.
- Associe o ponteiro data_t ao endereço de mtext na estrutura msgbuf_t.
- Chame msgrcv () para receber a mensagem pela fila.
- Usando o ponteiro data_t, leia os dados que foram recebidos.
Detalhes do remetente
A função sender () é examinada agora em detalhes (a análise da função receiver () permanece como um exercício para o leitor). Note, a função sender() e a função receiver() são ambas chamadas por um dos filhos separadamente 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);
Logo, na função sender() entra-se em um "loop" de forma que a mensagem é enviada sem que a haja a especificação do tempo necessário para esse envio. 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.
If ( msgsnd(queue_id,(strct msgbuf *)&message_buffer,sizeof(data_t),0) == -1) {
fprint(stdree, "impossivel enviar mensagem!\n");
exit(1) ;
}
Finalmente, em sender() espera-se por alguns micro segundos para dar a oportunidade a receiver () para ser executada. Isto é feito chamando usleep ().
Cada experimento constitui uma atividade que precisa de ser completada através de duas tarefas básicas. A primeira se refere à compilação e entendimento de um 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.
Cada trabalho de programação deve ser acompanhado de um relatório com as seguintes partes obrigatórias:
- Introdução, indicando, em não mais do que 20 linhas, quais são os objetivos do experimento;
- Resultados da execução do programa exemplo;
- Resultados da execução do programa modificado;
- Análise dos resultados, onde deve-se explicar o motivo da desigualdade de resultados;
- Conclusão indicando o que foi aprendido com o experimento.
A entrega de cada trabalho deve ocorrer através do envio de um e-mail "Encaminhando programa 2", de acordo com o cronograma previamente estabelecido. A data e hora limites correspondem à segunda-feira às 24:00, da semana marcada para entrega e apresentação. Anexos a esse e-mail devem constar:
- os dois programas fonte,
- os dois executáveis,
- o relatório final do trabalho e
- uma imagem do comando ls -l sobre os arquivos usados no experimento ao final do mesmo.
Solicita-se que, se usado um compactador de arquivos, que este seja o zip.
A falta de qualquer elemento no e-mail ou a perda da data de entrega implica na perda da nota correspondente. Somente duas exceções serão consideradas, o fechamento do laboratório durante o período disponibilizado para a realização do experimento, ou problema de doença avisado com antecedência mínima de dois dias antes da data da entrega.
Laboratório cheio, quedas de máquinas, falta de linha telefônica, problemas pessoais ou "blackouts" não serão aceitos como desculpas por atrasos. Por isso, recomenda-se fortemente que o início do trabalho ocorra o mais rapidamente possível.
Tarefas
A primeira parte da tarefa é compilar e executar as instruções disponíveis no programa exemplo.
Executar o programa 10 (dez) vezes. Procure carregar o computador a cada execução, ou seja, aumentar a sua carga através da execução de um número maior de programas que realmente disputem a CPU, e veja se o desvio aumenta. Explique o que fez para aumentar a carga do computador e apresente os dez resultados obtidos. Analise os resultados que recebe destas execuções. 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 (seg) |
Máximo (seg) |
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. A outra é a carga da máquina. Altere o programa exemplo de maneira que:
- um valor numérico que deve ser acrescido ao tamanho original da mensagem a ser enviada seja lido a partir do teclado;
- declare uma matriz com o tamanho correspondente ao número de interações;
- faça com que o filho receptor não calcule mais os tempos médio e máximo, ele deve apenas colocar os tempos na matriz declarada;
- faça com que o pai calcule e exiba o tempo total, o tempo médio e o tempo máximo, após o término dos dois filhos. Cuide para que o valor dos tempos estejam em mili segundos.
Execute o programa modificado dez vezes com o mesmo número de interações. Para cada interação alterne o valor que deve ser acrescido ao tamanho da mensagem. Veja que o tamanho mude de 500 em 500 bytes. Crie um quadro adequado para apresentar os resultados. Analise os resultados obtidos e explique as diferenças.
O resultado do experimento será apresentado em sala na aula seguinte ao dia marcado para a entrega, com a presença obrigatória de todos os alunos, de acordo com o cronograma previamente estabelecido.
Serão escolhidos dois alunos para a apresentação e discussão dos resultados. A critério do professor pode, inclusive, ocorrer o convite a qualquer dos alunos não escolhidos para que façam essa apresentação.
Todos os alunos que completaram o experimento devem preparar para a apresentação:
- A introdução,
- Os resultados,
- A análise dos resultados, e
- A conclusão.
Durante a apresentação deverão ser respondidas perguntas do Professor e de colegas.