Sistemas Operacionais, 10 semestre/2004

Experimento #1

 

Código Fonte do Programa Exemplo

1. Introdução

    Este primeiro experimento permite o contato com dois assuntos importantes em Sistemas Operacionais. O primeiro é o da criação de processos. O segundo está relacionado ao conceito de tempo e engloba vários fatores importantes que precisam ser percebidos sobre a duração da execução de um programa em um ambiente multitarefa.

    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:

3. Teoria

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.

Duração de um trecho de programa

A duração da execução de um trecho de programa dentro do computador nunca pode ser medido com certeza. Primeiramente, um computador é uma máquina digital, então, sua realidade é a execução de uma série de passos simples e determinísticos (sempre que repetidos geram o mesmo resultado). Mesmo supondo que estes passos sejam realizados em um segundo ou um nano segundo, os mecanismos disponíveis para medição de tempo em um computador sempre vão apresentar alguma limitação. Além do mais, o próprio conjunto de passos necessários para medir o tempo causa um profundo efeito no tempo que está sendo medido. Os atos de "ler" o tempo do relógio do sistema e copiar em uma área de memória local, onde o processo pode ter acesso, requer uma quantidade de tempo. Os projetistas do SO podem implementar algum esquema para compensar esse tempo adicional, mas isso é improvável, pois, como é visto a seguir, a duração desse tempo é não-determinística. Assim, o tempo medido pode ser um pouco impreciso.

Multitarefa

O conceito de multitarefa não é novo. Olhando um sistema com um único processador (ou um multi-processador que executa processos múltiplos), no processamento multi-tarefa diferentes processos conseguem uma parte do tempo disponível do processador . Por exemplo, se três processos precisam executar, o sistema operacional deixará um processo rodar por uma pequena quantia de tempo e o próximo processo pegará o processador por uma pequena quantia de tempo, e, então, o terceiro processo pegará por mais uma quantia pequena de tempo (este período pequeno de tempo é chamado fatia de tempo de processamento). Depois que cada processo acabar de usar o processador durante sua fatia de tempo, o ciclo se repetirá. Isto dará a ilusão que todos os processos estão executando concorrentemente, devido à velocidade de execução da CPU.

Comandos comuns para Trabalhar com Processos: ps e kill

Um primeiro comando muito importante no Unix é o comando man (de manual), através do qual pode-se obter auxílio a respeito de outro comando. Por exemplo, tente chamar man gettimeofday (gettimeofday é uma função que é discutida adiante e serve para a leitura de um relógio).

Antes de examinar as chamadas de sistema usadas pelo UNIX para executar operações sobre processos, pode ser útil olhar vários comandos que serão úteis na execução deste e dos experimentos restantes.

Primeiro, o comando ps permite ver informações sobre os processos que estão em execução naquele instante na máquina. Digitando ps em uma janela, aparecerá uma lista de processos. Embora a maioria dos SOs Unix tenha este comando para relacionar os processos ativos, o mesmo pode ser diferente em cada SO. Execute o comando man ps para aprender mais sobre o comando que lista informações de processos.

O segundo comando importante é o comando kill que é usado para enviar sinais a outros processos (sinais serão cobertos experimento #3). Para usar este comando digite kill pid, onde pid é a identificação numérica associada a um processo que se quer sinalizar. O pid de um processo pode ser obtido através de vários métodos, mas o mais comum é usar o comando ps. Tenha cuidado com os pids submetidos ao comando kill, pode-se terminar a execução, por exemplo, do shell, que é o programa que processa os comando Unix submetidos após o logon.

Criando e Administrando Processos em C: fork, exec, e wait

Existem três chamadas de sistema comuns, fornecidas pelo Unix, para criar e administrar processos: fork, exec e wait. A primeira chamada é usada para criar processos, a segunda para carregar uma imagem de processo (programa) diferente e a última para esperar pelo término de um processo. Caso não entenda alguma desta terminologia, não se preocupe, ela deverá ficar clara ao final dos experimentos. Procure utilizar o comando man sobre estes comandos para informação mais específica. Como exemplo de execução de um processo observe o que acontece quando se executa um ls em um shell:

A chamada fork cria um processo novo. O novo processo é idêntico ao original com exceção de uma diferença secundária, o valor de retorno da chamada fork () (há outras diferenças, mas neste momento não têm importância). Observe o seguinte trecho de código:

int rtn;

... algum codigo ...

rtn = fork();

if( rtn == 0 ) {

... codigo para o processo filho ...

}

else {

... codigo para o processo pai ...

}

Perceba que a chamada fork() retorna um valor que é armazenado na variável rtn. Se o valor de rtn é nulo, o processo é o processo novo e é chamado de filho. Se o valor de rtn é não-nulo, o processo é o processo original ou pai (no caso de ser o pai, o valor de rtn corresponde ao pid do filho). Imagine que a chamada fork () realiza uma clonagem. Antes de chamar fork () havia um processo, depois são dois! Criação de processos no Unix é um conceito importante que necessita ser entendido. Se não entendeu, procure escrever alguns programas semelhantes que exibam informações do que está acontecendo. Esta técnica pode ser usada para esclarecer o que está ocorrendo.

Uma rotina do SO escolhe qual processo vai ser processado em determinado momento. Um algoritmo de escalonamento e prioridades determina qual processo será executado. Considere que todos os processos têm uma prioridade igual, cada processo, então, terá uma fatia de tempo dada pelo sistema operacional para ser executado no processador. Ao término da fatia de tempo, o sistema operacional interrompe o processo (retira-o do processador) e aloca uma fatia de tempo para outro processo. Deste modo, cada processo que precisa ser processado vai conseguir. Às vezes, processos terminam e o processador é associado a um outro processo. O ato de retirar um processo correntemente em execução e substituí-lo por outro é denominado troca de contexto.

Uma troca de contexto demora uma quantia de tempo. Assim, se um processo está em execução e um segundo processo necessita ser processado, o primeiro processo tem que liberar o processador e uma troca de contexto tem que acontecer.

Considere o seguinte trecho:

while(1) {

    read_sensor ();

    usleep(1000);

}

Em princípio pode parecer que o trecho está logicamente correto, porém, um exame mais detalhado revela vários problemas: o "loop" requer algum tempo para executar, assim, o trecho ao invés de ocasionar a leitura do sensor a cada 1 mili segundo, demorará 1 mili segundo mais o tempo que leva para executar o "loop". Porém, não há nenhuma garantia que a chamada usleep () retorne exatamente a cada um mili segundo. Outros processos também podem estar sendo executados. Então, o processo em questão tem que esperar um período a mais de tempo pelo processador. Este período de tempo fará o "loop" demorar mais tempo que o idealmente necessário e, possivelmente, fará a leitura do sensor ficar tardia. Isto é chamado desvio ou "drift" e deve ser observado na execução do programa exemplo. Um desvio acontece quando um evento periódico se afasta levemente do tempo em que é suposto para ocorrer, causando pequenos erros na taxa periódica.

Programa Exemplo

Considere o Código Fonte do Programa Exemplo. Observe o trecho de código onde a chamada fork () acontece:

rtn = 1;

for( count = 0; count < NO_OF_CHILDREN; count++ ) {

    if( rtn != 0 ) {

    rtn = fork();

    }

else {

    break;

    }

}

Primeiro, rtn é fixado em um. Posteriormente, o "loop" é executado três vezes, criando os filhos, tantos quanto NO_OF_CHILDREN. Lembre que o pai recebe um número não-nulo na chamada da função fork () e o filho recebe um zero. Assim, o processo pai começa fora do "loop" com rtn 1 e depois terá rtn com um valor diferente de zero enquanto os filhos sempre têm rtn igual a zero.  Ambos, pai e filho, após a execução do fork (), estarão executando o comando for.

A cada execução do "loop", o pai tem rtn != 0 avaliado como verdadeiro, a chamada fork () é executada, enquanto cada um dos filhos, após sua criação, têm rtn != 0 avaliado como falso e saem fora do "loop". Lembre-se que o filho é uma duplicata exata do pai na hora do fork () com exceção do valor de retorno. NO_OF_CHILDREN define o número de filhos.

Veja no programa exemplo que, em seguida à chamada fork () e à execução do for, o comando if, fora do "loop",  verifica o valor de rtn e decide se o processo é um filho ou pai. Se é o pai, a chamada wait () é realizada para esperar pelo término dos filhos. Os filhos medirão o desvio. Cada filho executa, então, o trecho de código seguinte:

[...]

gettimeofday( &start_time, NULL );

for( count = 0; count < NO_OF_ITERATIONS; count++ ) {

        usleep(SLEEP_TIME);

}

gettimeofday( &stop_time, NULL );

[...]

gettimeofday () é uma chamada de sistema original da versão BSD4.3 do Unix (o comando man pode ser usado para obter mais informação sobre essa chamada de sistema). Essa chamada devolve uma estrutura timeval (veja a estrutura abaixo) que contém o número de segundos e micro segundos que decorreram desde 1 de janeiro de 1970 às 00:00 (o momento que é considerado o nascimento do Unix). Pode-se perceber, a partir do formato da estrutura timeval, que esta chamada de sistema não apresentará precisão maior que um micro segundo. Porém, é útil saber o quão precisa esta chamada é.

Struct timeval

{

Int tv_sec;

Int tv_usec;

};

O tempo inicial é retornado usando a chamada gettimeofday (). Depois, um for é executado NO_OF_ITERATIONS vezes. A cada vez, o processo pára de ser executado por um período equivalente a SLEEP_TIME (1 milis egundo). Ao término do for, o tempo final é medido. Teoricamente o tempo final menos o tempo inicial deve ser equivalente a NO_OF_ITERATIONS vezes SLEEP_TIME. Porém, este não é o caso, devido ao comando for demorar um pouco para ser executado porque as instruções que o compõem requerem algum tempo do processador, além do tempo necessário para chamar a rotina usleep (). Além disso, podem haver outros processos disputando o processador, ocasionando um atraso maior para que o processo volte a conseguir a CPU para continuar sua execução.

4. Resultado

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,

A entrega de cada trabalho deve ocorrer através do envio de um e-mail "Encaminhando programa 1", 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:

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; e 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 ou problemas pessoais 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 o programa exemplo. Se você não sabe como fazer isso, procure orientação com algum colega ou acesse alguma página na internet que trate do assunto. O compilador a ser usado deve ser o gcc em ambiente Linux.

Após compilado, 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, e veja se o desvio aumenta. Para isso, coloque para executar outros programas ou um mesmo várias vezes.

Não se esqueça de explicar 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. Uma lição importante deste experimento é aprender que, quando há dormência de processos, o tempo de resposta dos processos não é previsível.

Filho

Total (mseg)

Média (mseg)

1

0.278

0.003

2

0.519

0.003

3

0.332

0.003

Se estiver interessado, tente mudar algumas das constantes e veja como elas afetam os resultados.

Com base no exemplo, altere o programa de maneira que o número de processos filhos seja igual a 3. Ao invés de ter o código do filho no mesmo programa do código do pai, como acontece fora do "loop" onde está o fork(), crie outro programa para o filho e chame este programa usando a chamada exec (). O número de microsegundos para a chamada usleep () deve ser lido pelo processo a partir do teclado e deve ser igual a múltiplos de 50. Use kill para terminar os processos filhos e acerte onde vai ser feita a chamada para o primeiro gettimeofday ().

Execute o programa modificado dez (10) vezes e crie um quadro adequado para apresentar os resultados. Analise os resultados obtidos e explique as diferenças entre as 10 execuções e também com relação à execução do programa exemplo.

5. Apresentação

O resultado do experimento será apresentado em sala no dia de aula prática da semana marcada 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, em formato digital:

Durante a apresentação deverão ser respondidas perguntas do Professor e de colegas.