Este artigo relata uma situação inusitada que ocorreu durante a execução de um programa de teste da chamada de sistema fork() do Unix e do Linux, esclarecendo detalhes sobre o funcionamento desta chamada de sistema no que diz respeito a buffers.
Por: Roland Teodorowitsch
Introdução
Durante a aula de projeto de sistemas operacionais do curso de Ciência da Computação da Universidade Luterana do Brasil, campus Gravataí, em 12 de março de 2009, eu estava explicando aos alunos o funcionamento da chamada de sistema fork() do Unix, quando ocorreu um fato inusitado. Esta chamada cria uma cópia do processo atual, duplicando código e valores de variáveis. Trata-se da chamada básica do Unix para criar um novo processo. Como código e variáveis são duplicados, as diferenças entre o processo criador (também chamado de pai) e o processo criado (chamado de filho) são:
* o novo processo terá um PID (Process IDentification) diferente do processo criador;
* e a chamada fork() retornará o PID do processo-filho para o processo-pai e 0 para o processo-filho.
E, para ambos os processos, a execução continuará na instrução seguinte à chamada fork().
No restante deste artigo são apresentados alguns exemplos de uso da chamada fork() para testar o seu funcionamento, o "desafio" que gerou a "situação inusitada", a explicação para a origem da situação inusitada e a prova da explicação. Por fim, são apresentadas algumas conclusões.
Desafios iniciais
Depois de descrever o funcionamento da chamada fork(), sempre proponho alguns desafios aos alunos para ver se eles entenderam a explicação. O primeiro desafio gosto de apresentar lembrando aos alunos que nas disciplinas de linguagens de programação eles sempre aprendem que, no caso de comandos como if e else, se a condição do if for verdadeira, o comando ou bloco executado será o do if. Caso a condição seja falsa, o comando ou bloco executado será o do else. Pergunto-lhes então o que seria impresso depois do trecho de código-fonte mostrado na Figura 1.
Figura 1 - Exemplo de código-fonte com if
A resposta, por mais estranha que seja, não pode ser outra que não: as duas cadeias de caracteres são impressas. A execução do fork() duplica o processo, de forma que, apesar de termos um único código-fonte, teremos 2 processos executando o if. Para um dos processos a condição do if será falsa, para o outro, não.
Outro desafio interessante é questionar quanto ao número de caracteres 'x' impresso após a execução do trecho de código-fonte da Figura 2.
Figura 2 - Exemplo de código-fonte com vários fork() em sequência
Às vezes é possível ouvir alguns números improváveis antes da resposta correta: oito. Após o primeiro fork(), teremos 2 processos. Cada um deles executa um fork(), resultando em 4 processos. E, por fim, cada um destes 4 processos executa um último fork(), resultando em 8 processos. E cada um destes 8 processos imprime um caractere 'x'.
Um novo desafio
Talvez estimulado pelo fado da resposta correta ao desafio da Figura 2 não ter demorado a aparecer, pensei então em outro desafio. E se o fork() for colocado em um laço? Antes de lançar o desafio para a turma, ainda considerei se a explicação não poderia se tornar mais complicada do que eu gostaria. Mas quem trabalha com Sistemas Operacionais não pode ter medo de códigos ou explicações complicadas. E a situação poderia ilustrar perfeitamente as consequências de se colocar um fork() dentro de um laço sem maiores cuidados.
Lancei então o desafio para a turma e não demorou muito para que o fato inusitado, ao qual me referi no início do artigo, surgisse. O desafio consistia em determinar quantos caracteres 'x' apareceriam na tela depois da execução do código-fonte da Figura 3.
Figura 3 - Exemplo de código-fonte para o novo desafio
Resultados diferentes
Recomendei que a turma digitasse e testasse o código da Figura 3 e em seguida discutiríamos os resultados. Para minha surpresa a maioria da turma obteve 8 como resposta e dois alunos obtiveram 6...
Analisamos então o código-fonte e chegamos à seguinte conclusão: na primeira iteração do laço, um fork() seria executado e dois processos imprimiriam o caractere 'x', indo em seguida para a segunda iteração. Nesta segunda iteração (ainda bem que não usei 10 como limite para o laço...), os dois processos executariam uma chamada fork(), tendo-se 4 processos, cada um imprimindo mais um caractere 'x'. Total de caracteres 'x' impressos: 6 (seis).
Verificamos então as diferenças entre os códigos que resultaram no valor esperado (seis) e no valor que a maioria obteve (oito). A maioria digitou o código exatamente da forma apresentada na Figura 3. Os dois alunos que obtiveram o valor esperado, digitaram o código "à sua maneira", fazendo pequenas adaptações. Considerando as poucas variações que um código tão pequeno pode apresentar, chega a ser difícil compreender a origem da diferença de resultado.
Isolando e eliminando as diferenças insignificantes, ficou claro que o que estava ocasionando a diferença era um '\n' (caractere de nova linha) colocado na cadeia de caracteres da chamada printf(). Uma chamada fflush(stdout) colocada logo após o printf() tinha o mesmo efeito do uso de printf() com '\n'. A chamada fflush(stdout) esvazia o buffer de saída do processo (que por padrão é o terminal de vídeo), garantindo que tudo o que foi mandado imprimir seja visualizado imediatamente. O código com esta chamada, que gera o resultado esperado (seis), pode ser visto na Figura 4.
Figura 4 - Exemplo de código-fonte para o novo desafio com fflush(stdout)
A questão principal era então: por que o primeiro código apresentou dois caracteres "x" a mais além do esperado? De onde saíram estes dois caracteres "x"?
Como o horário da aula estava se encerrando, não foi possível encontrar uma resposta para a questão na hora.
A resposta
No caminho até a minha casa, pensando na influência da chamada fflush(stdout) e na definição da chamada de sistema fork() - cria uma cópia idêntica do processo... -, chequei a conclusão de que o que estava acontecendo era que o fork() duplicava também os buffers do processo, entre eles, é claro, o buffer de saída (stdout). A questão era que alguns processos-filho estavam recebendo caracteres "x" nos buffers que herdavam de seus pais.
Encontrada uma resposta lógica, o problema passou a ser: como provar esta teoria através de um exemplo prático e real.
A prova
Para facilitar o entendimento do que estava ocorrendo com o programa do laço de chamadas fork() (Figura 3), em vez de imprimir caracteres "x", fica mais interessante imprimir um número correspondente à iteração do laço. Esta modificação gera o programa da Figura 5.
Figura 5 - Exemplo de código-fonte do laço de chamadas fork() imprimindo o número da iteração
No caso da Figura 5, o resultado esperado deveria ser "010111", mas obtém-se "01010101" devido a duplicação dos buffers de saída. A Figura 6 apresenta um fluxograma do que ocorre ao longo da execução do código da Figura 5. Ao final da execução, ter-se-á 4 processos executando. Como mostra a figura, após cada fork(), o conteúdo do buffer de saída stdout é duplicado no processo filho, o que significa que o processo filho recebe neste buffer o que o seu processo-pai havia mandado imprimir via printf().
Há, no entanto, uma forma mais simples de comprovar o comportamento da chamada fork() em relação a situação descrita neste artigo. Este é o caso do programa da Figura 7. Neste código, depois de imprimir um caractere "x", executa-se uma chamada fork() e em seguida imprime-se um caractere "y". O resultado esperado deveria ser "xyy". No entanto, em função da duplicação do buffer de saída na chamada fork(), o resultado será "xyxy".
Figura 7 - Teste final
Conclusão
Este artigo apresentou uma situação de uso da chamada de sistema fork() do Unix que pode gerar resultados diferentes do esperado. Basicamente o que ocorre é que a chamada fork() duplica um processo, incluindo o buffer de saída, ou seja, incluindo tudo o que o processo-pai (criador) mandou imprimir com printf(), fprintf(stdout, ...) ou chamadas afins, e que ainda não foi descarregado do buffer de saída (stdout).
O comportamento esperado parte do princípio de o que é impresso aparece no vídeo imediatamente, o que nem sempre é verdade. Como a entrada e saída do Unix passa por um buffer, o conteúdo do buffer só é esvaziado no final de uma linha, quando o buffer está cheio ou quando força-se este esvaziamento (através da chamada fflush()). Desta forma, quando o buffer não é esvaziado, podem ocorrer comportamentos não esperados em chamadas como fork(), que duplicam o processo, incluindo o seus buffers.
Tal comportamento foi verificado no Linux, versões de núcleo 2.6. Acredita-se que as versões anteriores de Linux tenham o mesmo comportamento. É provável que outras versões de Unix também tenham o mesmo comportamento, mas isto não foi comprovado na prática.
A forma como o programa se manifestou nos fornece algumas interpretações interessantes para a história. A primeira delas é de que se não procurarmos experimentar situações novas, manteremos nosso conhecimento limitado à situação atual. É importante vivenciar situações novas.
Os alunos que escreveram o programa ao seu modo mostraram também que, às vezes, seguir um caminho diferente do que a maioria segue pode levar a caminhos diferentes e interessantes.
Ao mesmo tempo, à primeira vista, o problema mostrou-se sem explicação: pequenas diferenças no código-fonte gerando resultados diferentes. No entanto, uma análise mais detalhada, apresentou uma explicação plausível e comprovável para a situação.
Fonte: vivaoLinux
Postado Por: RedDeviL
Por: Roland Teodorowitsch
Introdução
Durante a aula de projeto de sistemas operacionais do curso de Ciência da Computação da Universidade Luterana do Brasil, campus Gravataí, em 12 de março de 2009, eu estava explicando aos alunos o funcionamento da chamada de sistema fork() do Unix, quando ocorreu um fato inusitado. Esta chamada cria uma cópia do processo atual, duplicando código e valores de variáveis. Trata-se da chamada básica do Unix para criar um novo processo. Como código e variáveis são duplicados, as diferenças entre o processo criador (também chamado de pai) e o processo criado (chamado de filho) são:
* o novo processo terá um PID (Process IDentification) diferente do processo criador;
* e a chamada fork() retornará o PID do processo-filho para o processo-pai e 0 para o processo-filho.
E, para ambos os processos, a execução continuará na instrução seguinte à chamada fork().
No restante deste artigo são apresentados alguns exemplos de uso da chamada fork() para testar o seu funcionamento, o "desafio" que gerou a "situação inusitada", a explicação para a origem da situação inusitada e a prova da explicação. Por fim, são apresentadas algumas conclusões.
Desafios iniciais
Depois de descrever o funcionamento da chamada fork(), sempre proponho alguns desafios aos alunos para ver se eles entenderam a explicação. O primeiro desafio gosto de apresentar lembrando aos alunos que nas disciplinas de linguagens de programação eles sempre aprendem que, no caso de comandos como if e else, se a condição do if for verdadeira, o comando ou bloco executado será o do if. Caso a condição seja falsa, o comando ou bloco executado será o do else. Pergunto-lhes então o que seria impresso depois do trecho de código-fonte mostrado na Figura 1.
Código:
p = fork(); if (p == 0) printf("FILHO\n"); else printf("PAI\n");
A resposta, por mais estranha que seja, não pode ser outra que não: as duas cadeias de caracteres são impressas. A execução do fork() duplica o processo, de forma que, apesar de termos um único código-fonte, teremos 2 processos executando o if. Para um dos processos a condição do if será falsa, para o outro, não.
Outro desafio interessante é questionar quanto ao número de caracteres 'x' impresso após a execução do trecho de código-fonte da Figura 2.
Código:
fork(); fork(); fork(); printf("x");
Às vezes é possível ouvir alguns números improváveis antes da resposta correta: oito. Após o primeiro fork(), teremos 2 processos. Cada um deles executa um fork(), resultando em 4 processos. E, por fim, cada um destes 4 processos executa um último fork(), resultando em 8 processos. E cada um destes 8 processos imprime um caractere 'x'.
Um novo desafio
Talvez estimulado pelo fado da resposta correta ao desafio da Figura 2 não ter demorado a aparecer, pensei então em outro desafio. E se o fork() for colocado em um laço? Antes de lançar o desafio para a turma, ainda considerei se a explicação não poderia se tornar mais complicada do que eu gostaria. Mas quem trabalha com Sistemas Operacionais não pode ter medo de códigos ou explicações complicadas. E a situação poderia ilustrar perfeitamente as consequências de se colocar um fork() dentro de um laço sem maiores cuidados.
Lancei então o desafio para a turma e não demorou muito para que o fato inusitado, ao qual me referi no início do artigo, surgisse. O desafio consistia em determinar quantos caracteres 'x' apareceriam na tela depois da execução do código-fonte da Figura 3.
Código:
#include <stdio.h> #include <unistd.h> int main() { int i; for (i=0; i<2; ++i) { fork(); printf("x"); } return(0);
Resultados diferentes
Recomendei que a turma digitasse e testasse o código da Figura 3 e em seguida discutiríamos os resultados. Para minha surpresa a maioria da turma obteve 8 como resposta e dois alunos obtiveram 6...
Analisamos então o código-fonte e chegamos à seguinte conclusão: na primeira iteração do laço, um fork() seria executado e dois processos imprimiriam o caractere 'x', indo em seguida para a segunda iteração. Nesta segunda iteração (ainda bem que não usei 10 como limite para o laço...), os dois processos executariam uma chamada fork(), tendo-se 4 processos, cada um imprimindo mais um caractere 'x'. Total de caracteres 'x' impressos: 6 (seis).
Verificamos então as diferenças entre os códigos que resultaram no valor esperado (seis) e no valor que a maioria obteve (oito). A maioria digitou o código exatamente da forma apresentada na Figura 3. Os dois alunos que obtiveram o valor esperado, digitaram o código "à sua maneira", fazendo pequenas adaptações. Considerando as poucas variações que um código tão pequeno pode apresentar, chega a ser difícil compreender a origem da diferença de resultado.
Isolando e eliminando as diferenças insignificantes, ficou claro que o que estava ocasionando a diferença era um '\n' (caractere de nova linha) colocado na cadeia de caracteres da chamada printf(). Uma chamada fflush(stdout) colocada logo após o printf() tinha o mesmo efeito do uso de printf() com '\n'. A chamada fflush(stdout) esvazia o buffer de saída do processo (que por padrão é o terminal de vídeo), garantindo que tudo o que foi mandado imprimir seja visualizado imediatamente. O código com esta chamada, que gera o resultado esperado (seis), pode ser visto na Figura 4.
Código:
#include <stdio.h> #include <unistd.h> int main() { int i; for (i=0; i<2; ++i) { fork(); printf("x"); fflush(stdout); } return(0); }
A questão principal era então: por que o primeiro código apresentou dois caracteres "x" a mais além do esperado? De onde saíram estes dois caracteres "x"?
Como o horário da aula estava se encerrando, não foi possível encontrar uma resposta para a questão na hora.
A resposta
No caminho até a minha casa, pensando na influência da chamada fflush(stdout) e na definição da chamada de sistema fork() - cria uma cópia idêntica do processo... -, chequei a conclusão de que o que estava acontecendo era que o fork() duplicava também os buffers do processo, entre eles, é claro, o buffer de saída (stdout). A questão era que alguns processos-filho estavam recebendo caracteres "x" nos buffers que herdavam de seus pais.
Encontrada uma resposta lógica, o problema passou a ser: como provar esta teoria através de um exemplo prático e real.
A prova
Para facilitar o entendimento do que estava ocorrendo com o programa do laço de chamadas fork() (Figura 3), em vez de imprimir caracteres "x", fica mais interessante imprimir um número correspondente à iteração do laço. Esta modificação gera o programa da Figura 5.
Código:
#include <stdio.h> #include <unistd.h> int main() { int i; for (i=0; i<2; ++i) { fork(); printf("%d",i); } return(0); }
No caso da Figura 5, o resultado esperado deveria ser "010111", mas obtém-se "01010101" devido a duplicação dos buffers de saída. A Figura 6 apresenta um fluxograma do que ocorre ao longo da execução do código da Figura 5. Ao final da execução, ter-se-á 4 processos executando. Como mostra a figura, após cada fork(), o conteúdo do buffer de saída stdout é duplicado no processo filho, o que significa que o processo filho recebe neste buffer o que o seu processo-pai havia mandado imprimir via printf().
Há, no entanto, uma forma mais simples de comprovar o comportamento da chamada fork() em relação a situação descrita neste artigo. Este é o caso do programa da Figura 7. Neste código, depois de imprimir um caractere "x", executa-se uma chamada fork() e em seguida imprime-se um caractere "y". O resultado esperado deveria ser "xyy". No entanto, em função da duplicação do buffer de saída na chamada fork(), o resultado será "xyxy".
Código:
#include <stdio.h> #include <unistd.h> int main() { printf("x"); fork(); printf("y"); return(0); }
Conclusão
Este artigo apresentou uma situação de uso da chamada de sistema fork() do Unix que pode gerar resultados diferentes do esperado. Basicamente o que ocorre é que a chamada fork() duplica um processo, incluindo o buffer de saída, ou seja, incluindo tudo o que o processo-pai (criador) mandou imprimir com printf(), fprintf(stdout, ...) ou chamadas afins, e que ainda não foi descarregado do buffer de saída (stdout).
O comportamento esperado parte do princípio de o que é impresso aparece no vídeo imediatamente, o que nem sempre é verdade. Como a entrada e saída do Unix passa por um buffer, o conteúdo do buffer só é esvaziado no final de uma linha, quando o buffer está cheio ou quando força-se este esvaziamento (através da chamada fflush()). Desta forma, quando o buffer não é esvaziado, podem ocorrer comportamentos não esperados em chamadas como fork(), que duplicam o processo, incluindo o seus buffers.
Tal comportamento foi verificado no Linux, versões de núcleo 2.6. Acredita-se que as versões anteriores de Linux tenham o mesmo comportamento. É provável que outras versões de Unix também tenham o mesmo comportamento, mas isto não foi comprovado na prática.
A forma como o programa se manifestou nos fornece algumas interpretações interessantes para a história. A primeira delas é de que se não procurarmos experimentar situações novas, manteremos nosso conhecimento limitado à situação atual. É importante vivenciar situações novas.
Os alunos que escreveram o programa ao seu modo mostraram também que, às vezes, seguir um caminho diferente do que a maioria segue pode levar a caminhos diferentes e interessantes.
Ao mesmo tempo, à primeira vista, o problema mostrou-se sem explicação: pequenas diferenças no código-fonte gerando resultados diferentes. No entanto, uma análise mais detalhada, apresentou uma explicação plausível e comprovável para a situação.
Fonte: vivaoLinux
Postado Por: RedDeviL