2010-03-27 16 views
59

OS: Linux, Język: pure Cprintf anomalia po "fork()"

ruszam naprzód nauki programowania C w ogóle, a programowanie C pod UNIX w szczególnym przypadku.

Wykryłem dziwne (dla mnie) zachowanie funkcji printf() po użyciu połączenia fork().

Kod

#include <stdio.h> 
#include <system.h> 

int main() 
{ 
    int pid; 
    printf("Hello, my pid is %d", getpid()); 

    pid = fork(); 
    if(pid == 0) 
    { 
      printf("\nI was forked! :D"); 
      sleep(3); 
    } 
    else 
    { 
      waitpid(pid, NULL, 0); 
      printf("\n%d was forked!", pid); 
    } 
    return 0; 
} 

Wyjście

Hello, my pid is 1111 
I was forked! :DHello, my pid is 1111 
2222 was forked! 

Dlaczego drugi "Hello" string wystąpić na wyjściu dziecka?

Tak, to jest dokładnie to, co rodzic wydrukował, gdy zaczął, z rodzicem pid.

Ale! Jeśli kładziemy \n charakter na końcu każdego łańcucha możemy uzyskać oczekiwany wynik:

#include <stdio.h> 
#include <system.h> 

int main() 
{ 
    int pid; 
    printf("Hello, my pid is %d\n", getpid()); // SIC!! 

    pid = fork(); 
    if(pid == 0) 
    { 
      printf("I was forked! :D"); // removed the '\n', no matter 
      sleep(3); 
    } 
    else 
    { 
      waitpid(pid, NULL, 0); 
      printf("\n%d was forked!", pid); 
    } 
    return 0; 
} 

Output:

Hello, my pid is 1111 
I was forked! :D 
2222 was forked! 

Dlaczego tak się dzieje? Czy to jest prawidłowe zachowanie, czy jest to błąd?

Odpowiedz

75

Pragnę zauważyć, że <system.h> jest niestandardowy nagłówek; Wymieniłem go na <unistd.h>, a kod skompilowałem czysto.

Gdy dane wyjściowe twojego programu trafiają do terminala (ekranu), jest buforowany liniowo. Kiedy wyjście twojego programu przechodzi do potoku, jest ono w pełni zbuforowane. Możesz kontrolować tryb buforowania za pomocą funkcji standardowej C setvbuf() i _IOFBF (pełne buforowanie), _IOLBF (buforowanie linii) i _IONBF (bez buforowania).

Możesz to udowodnić w swoim zmienionym programie, wyprowadzając wynik twojego programu na, powiedzmy, cat. Nawet z nowymi liniami na końcu ciągów printf() zobaczysz podwójną informację. Jeśli wyślesz go bezpośrednio do terminalu, zobaczysz tylko jedną partię informacji.

Morał z tej historii polega na tym, aby przed rozwidleniem się opróżnić wszystkie bufory wejścia/wyjścia przed wywołaniem fflush(0);.


linia po linii analizy, na żądanie (szelki itp usunięty - i usunięto spacje redaktor znaczników):

  1. printf("Hello, my pid is %d", getpid());
  2. pid = fork();
  3. if(pid == 0)
  4. printf("\nI was forked! :D");
  5. sleep(3);
  6. else
  7. waitpid(pid, NULL, 0);
  8. printf("\n%d was forked!", pid);

Analiza:

  1. Kopie "Witam, mój PID 1234" do bufora na standardowe wyjście. Ponieważ na końcu nie ma nowej linii, a wyjście działa w trybie buforowania linii (lub w trybie pełnego buforowania), nic nie pojawia się na terminalu.
  2. Daje nam dwa oddzielne procesy, z dokładnie tym samym materiałem w buforze standardowym.
  3. Dziecko ma pid == 0 i wykonuje linie 4 i 5; rodzic ma niezerową wartość dla pid (jedna z niewielu różnic między tymi dwoma procesami - wartości zwracane z getpid() i getppid() są dwiema więcej).
  4. Dodaje znak nowej linii i "Byłem rozwidlony!: D" do bufora wyjściowego elementu potomnego. Pierwszy wiersz danych wyjściowych pojawia się na terminalu; reszta jest przechowywana w buforze, ponieważ wyjście jest buforowane liniowo.
  5. Wszystko zatrzymuje się na 3 sekundy. Następnie dziecko wychodzi normalnie przez powrót na końcu głównej. W tym momencie resztkowe dane w buforze standardowym zostają przepłukane. Pozostawia to pozycję wyjściową na końcu linii, ponieważ nie ma nowej linii.
  6. Nadchodzi rodzic.
  7. Rodzic czeka, aż dziecko skończy umierać.
  8. Element nadrzędny dodaje znak nowej linii i "1345 został rozwidlony!" do bufora wyjściowego. Nowa linia opróżnia komunikat "Cześć" na wyjściu po niepełnej linii wygenerowanej przez dziecko.

Rodzic teraz wychodzi normalnie przez powrót na końcu głównej, a resztkowe dane są wypłukiwane; ponieważ na końcu wciąż nie ma znaku nowej linii, pozycja kursora znajduje się za wykrzyknikiem, a znak zachęty powłoki pojawia się w tym samym wierszu.

co widzę to:

Osiris-2 JL: ./xx 
Hello, my pid is 37290 
I was forked! :DHello, my pid is 37290 
37291 was forked!Osiris-2 JL: 
Osiris-2 JL: 

Numery PID są różne - ale ogólny wygląd jest jasne.Dodanie nowego wiersza do końca printf() oświadczenia (które bardzo szybko staje się standardową praktyką) zmienia wyjście A Lot:

#include <stdio.h> 
#include <unistd.h> 

int main() 
{ 
    int pid; 
    printf("Hello, my pid is %d\n", getpid()); 

    pid = fork(); 
    if(pid == 0) 
     printf("I was forked! :D %d\n", getpid()); 
    else 
    { 
     waitpid(pid, NULL, 0); 
     printf("%d was forked!\n", pid); 
    } 
    return 0; 
} 

teraz uzyskać:

Osiris-2 JL: ./xx 
Hello, my pid is 37589 
I was forked! :D 37590 
37590 was forked! 
Osiris-2 JL: ./xx | cat 
Hello, my pid is 37594 
I was forked! :D 37596 
Hello, my pid is 37594 
37596 was forked! 
Osiris-2 JL: 

Zauważ, że gdy wyjście idzie do terminala , jest buforowany liniowo, więc linia "Hello" pojawia się przed fork() i była tylko jedna kopia. Kiedy wyjście jest podłączone do cat, jest w pełni buforowane, więc nic nie pojawia się przed fork() i oba procesy mają linię "Hello" w buforze, który ma zostać przepłukany.

+0

Ok, rozumiem. Ale wciąż nie potrafię wyjaśnić, dlaczego "śmieci buforowe" pojawiają się na końcu nowo wydrukowanej linii na wyjściu dziecka? Ale czekaj, teraz wątpię, że to naprawdę wynik DZIECKA .. oh, czy mógłbyś wyjaśnić, dlaczego wyjście wygląda DOKŁADNIE (nowy ciąg PRZED starym) tak, krok po kroku, więc byłbym bardzo wdzięczny. Dziękuję i tak! – pechenie

+0

Bardzo imponujące wyjaśnienie! Dziękuję bardzo, wreszcie zrozumiałem to wyraźnie! P.S .: Już wcześniej głosowałem za, a teraz głupio kliknąłem "strzałkę w górę" jeszcze raz, więc głos zniknął. Ale nie mogę ci go jeszcze raz dać, ponieważ "odpowiedź jest zbyt stara" :( P.P.S .: Dałem Ci głos w innym pytaniu i jeszcze raz dziękuję! – pechenie

22

Powód jest taki, że bez \n na końcu ciągu formatu wartość nie jest od razu drukowana na ekranie. Zamiast tego jest buforowany w ramach procesu. Oznacza to, że nie jest on drukowany aż do momentu użycia wideł, dlatego wydrukuje się go dwukrotnie.

Dodanie \n wymusza jednak przepłukanie bufora i wyprowadzenie go na ekran. Dzieje się to przed widelcem i dlatego jest drukowane tylko raz.

Można wymusić to za pomocą metody fflush. Na przykład

printf("Hello, my pid is %d", getpid()); 
fflush(stdout); 
+0

Dziękuję za odpowiedź! – pechenie

+0

'fflush (stdout);' Wydaje się być poprawniejszą odpowiedzią tutaj imo. –

5

fork() skutecznie tworzy kopię procesu. Jeśli przed wywołaniem fork() dane były buforowane, zarówno rodzic, jak i dziecko będą mieć te same buforowane dane. Następnym razem, gdy każdy z nich zrobi coś, by opróżnić swój bufor (np. Drukowanie linii nowej w przypadku wyjścia terminala), zobaczysz to buforowane wyjście oprócz każdego nowego wyjścia wygenerowanego przez ten proces. Jeśli więc zamierzasz używać stdio zarówno w rodzicach, jak i dzieciach, powinieneś przed rozwidleniem uzyskać fflush, aby upewnić się, że nie ma zbuforowanych danych.

Często dziecko jest używane tylko do wywoływania funkcji exec*. Ponieważ zastępuje on pełny obraz procesu potomnego (w tym wszelkie bufory), nie ma potrzeby, aby to było fflush, jeśli tak naprawdę wszystko, co robisz w dziecku. Jednakże, jeśli mogą być buforowane dane, powinieneś być ostrożny w kwestii obsługi niepowodzenia exec. W szczególności unikaj drukowania błędu na standardowe wyjście lub stderr za pomocą dowolnej funkcji stdio (write jest w porządku), a następnie wywołaj _exit (lub _Exit) zamiast wywoływać exit lub po prostu powracając (co spowoduje opróżnienie dowolnego buforowanego wyjścia). Ewentualnie omiń problem przez spłukanie przed rozwidleniem.