Unconfigured Ad Widget

Collapse

Anúncio

Collapse
No announcement yet.

A duplicação do buffer de saída na chamada de sistema fork() do Linux

Collapse
X
 
  • Filter
  • Tempo
  • Show
Clear All
new posts

  • Font Size
    #1

    C / C++ A duplicação do buffer de saída na chamada de sistema fork() do Linux

    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.

    Código:
    p = fork();
    if (p == 0)
       printf("FILHO\n");
    else
       printf("PAI\n");
    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.

    Código:
    fork();
    fork();
    fork();
    printf("x");
    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.

    Código:
    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    {
      int i;
    
      for (i=0; i<2; ++i)  {
          fork();
          printf("x");
      }
      return(0);
    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.

    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);
    }
    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.

    Código:
    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    {
      int i;
    
      for (i=0; i<2; ++i)  {
          fork();
          printf("%d",i);
      }
      return(0);
    }
    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".

    Código:
    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    {
      printf("x");
      fork();
      printf("y");
      return(0);
    }
    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
X
Working...
X