Dlaczego program z fork () czasami drukuje dane wyjściowe wiele razy?

50

W Programie 1 Hello worldjest drukowany tylko raz, ale kiedy go wyjmuję \ni uruchamiam (Program 2), wydruk jest drukowany 8 razy. Czy ktoś może mi wyjaśnić znaczenie \ntutaj i jak to wpływa na fork()?

Program 1

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...\n");
    fork();
    fork();
    fork();
}

Wyjście 1:

hello world... 

Program 2

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...");
    fork();
    fork();
    fork();
}

Wyjście 2:

hello world... hello world...hello world...hello world...hello world...hello world...hello world...hello world...
lmaololrofl
źródło
10
Spróbuj uruchomić Program 1 z wyjściem do pliku ( ./prog1 > prog1.out) lub potoku ( ./prog1 | cat). Przygotuj się na zawrót głowy. :-) ⁠
G-Man mówi „Reinstate Monica”
Odpowiednie pytania i odpowiedzi dotyczące innego wariantu tego problemu: system C („bash”) ignoruje standardowe wejście
Michael Homer
13
Zebrało to kilka bliskich głosów, więc komentarz na ten temat: pytania dotyczące „API UNIX C API i interfejsów systemowych” są wyraźnie dozwolone . Problemy z buforowaniem są częstym zjawiskiem również w skryptach powłoki i fork()są również nieco specyficzne dla Uniksa, więc wydaje się, że jest to dość temat na unix.SE.
ilkkachu
@ilkkachu faktycznie, jeśli przeczytasz ten link i klikniesz meta pytanie, do którego się odnosi, to bardzo wyraźnie określa, że ​​to nie jest temat. Tylko dlatego, że coś jest C, a unix ma C, nie czyni tego na temat.
Patrick
@Patrick, właściwie to zrobiłem. I nadal uważam, że pasuje do klauzuli „w granicach rozsądku”, ale oczywiście to tylko ja.
ilkkachu

Odpowiedzi:

93

Podczas wyświetlania na standardowe wyjście przy użyciu funkcji biblioteki C printf(), dane wyjściowe są zwykle buforowane. Bufor nie jest opróżniany, dopóki nie zostanie wypisany znak nowej linii, wywołanie fflush(stdout)lub wyjście z programu ( _exit()choć nie przez wywołanie ). Standardowy strumień wyjściowy jest domyślnie buforowany w ten sposób, gdy jest podłączony do TTY.

Po rozwidleniu procesu w „Programie 2” procesy podrzędne dziedziczą każdą część procesu nadrzędnego, w tym również niepoprawiony bufor wyjściowy. To skutecznie kopiuje niewypełniony bufor do każdego procesu potomnego.

Po zakończeniu procesu bufory są opróżniane. Zaczynasz w sumie osiem procesów (w tym proces pierwotny), a niewypełniony bufor zostanie opróżniony po zakończeniu każdego procesu.

Jest ósma, ponieważ za każdym razem fork()dostajesz dwa razy więcej procesów, które miałeś przed fork()(ponieważ są one bezwarunkowe), a ty masz trzy z nich (2 3 = 8).

Kusalananda
źródło
14
Powiązane: może skończyć mainsię _exit(0)po prostu zrobić wywołanie systemowe wyjście bez spłukiwania buforów, a następnie zostanie ona wydrukowana zero razy bez nowej linii. ( Implementacja exit () w Syscall i dlaczego _exit (0) (wyjście przez syscall) uniemożliwia mi otrzymywanie jakiejkolwiek treści standardowej? ). Lub możesz potokować Program1 do catlub przekierować go do pliku i zobaczyć, jak drukuje się 8 razy. (standardowe wyjście jest buforowane domyślnie, gdy nie jest to TTY). Lub dodaj fflush(stdout)do przypadku no-newline przed drugim fork()...
Peter Cordes,
17

W żaden sposób nie wpływa na widelec.

W pierwszym przypadku powstaje 8 procesów, które nie mają nic do napisania, ponieważ bufor wyjściowy został już opróżniony (z powodu \n).

W drugim przypadku nadal masz 8 procesów, każdy z buforem zawierającym „Hello world ...”, a bufor jest zapisywany na końcu procesu.

edc65
źródło
12

@Kusalananda wyjaśnił, dlaczego dane wyjściowe są powtarzane . Jeśli jesteś ciekawy, dlaczego dane wyjściowe są powtarzane 8 razy, a nie tylko 4 razy (program podstawowy + 3 widelce):

int main()
{
    printf("hello world...");
    fork(); // here it creates a copy of itself --> 2 instances
    fork(); // each of the 2 instances creates another copy of itself --> 4 instances
    fork(); // each of the 4 instances creates another copy of itself --> 8 instances
}
Honza Zidek
źródło
2
jest to podstawa rozwidlenia
Prvt_Yadav
3
@Debian_yadav prawdopodobnie oczywiste tylko wtedy, gdy znasz jego implikacje. Jak na przykład opróżnianie buforów stdio .
roaima
2
@Debian_yadav: en.wikipedia.org/wiki/False_consensus_effect - dlaczego mielibyśmy zadawać pytania, jeśli wszyscy wiedzą wszystko?
Honza Zidek
8
@Debian_yadav Nie mogę czytać w myślach OP, więc nie wiem. W każdym razie stackexchange to miejsce, w którym również inni szukają wiedzy i myślę, że moja odpowiedź może być przydatnym dodatkiem do dobrej odpowiedzi Kulasandry. Moja odpowiedź dodaje coś (podstawowego, ale przydatnego) w porównaniu z edc65, który po prostu powtarza to, co Kulasandra powiedział 2 godziny przed nim.
Honza Zidek
2
To tylko krótki komentarz do odpowiedzi, a nie faktyczna odpowiedź. Pytanie brzmi: „wiele razy”, a nie dlaczego dokładnie 8.
fajka
3

Ważnym tłem jest to, że stdoutzgodnie z ustawieniami domyślnymi wymagane jest buforowanie linii przez standard.

Powoduje to \nopróżnienie wyjścia.

Ponieważ drugi przykład nie zawiera nowej linii, dane wyjściowe nie są opróżniane, a ponieważ fork()kopiuje cały proces, kopiuje również stan stdoutbufora.

Teraz te fork()wywołania w twoim przykładzie tworzą łącznie 8 procesów - wszystkie z kopią stanu stdoutbufora.

Z definicji wszystkie te procesy wywołują exit()po powrocie, main()a następnie exit()wywołania na wszystkich aktywnych strumieniach stdio . Obejmuje to, w wyniku czego widzisz tę samą treść osiem razy.fflush()fclose()stdout

Dobrą praktyką jest wywoływanie fflush()wszystkich strumieni z oczekującymi fork()danymi wyjściowymi przed wywołaniem lub zezwolenie na rozwidlone wywołanie potomne, _exit()które wychodzi z procesu bez opróżniania strumieni stdio.

Zauważ, że dzwonienie exec()nie opróżnia buforów stdio, więc nie martw się o bufory stdio, jeśli (po wywołaniu fork()) zadzwonisz exec()i (jeśli to się nie powiedzie) _exit().

BTW: Aby zrozumieć, że może to spowodować nieprawidłowe buforowanie, oto poprzedni błąd w systemie Linux, który został niedawno naprawiony:

Standard wymaga stderrdomyślnie stderrbuforowania bufora, ale Linux zignorował to i sprawił, że linia została buforowana i (jeszcze gorzej) całkowicie buforowana na wypadek, gdyby stderr został przekierowany przez potok. Więc programy napisane dla UNIXa wypisały rzeczy bez Linii zbyt późno w Linuksie.

Patrz komentarz poniżej, wydaje się, że jest to teraz naprawione.

Oto co robię, aby obejść ten problem z Linuksem:

    /* 
     * Linux comes with a broken libc that makes "stderr" buffered even 
     * though POSIX requires "stderr" to be never "fully buffered". 
     * As a result, we would get garbled output once our fork()d child 
     * calls exit(). We work around the Linux bug by calling fflush() 
     * before fork()ing. 
     */ 
    fflush(stderr); 

Ten kod nie wyrządza szkody na innych platformach, ponieważ wywołanie fflush()strumienia, który został właśnie opróżniony, jest noop.

schily
źródło
2
Nie, wymagane jest pełne buforowanie standardowego wyjścia, chyba że jest to urządzenie interaktywne, w którym to przypadku nie jest określone, ale w praktyce jest buforowane liniowo. Stderr nie musi być w pełni buforowany. Zobacz pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/…
Stéphane Chazelas,
Moja strona podręcznika man setbuf()na Debianie ( ta na man7.org wygląda podobnie ) stwierdza, że ​​„Standardowy strumień błędów stderr jest zawsze domyślnie niebuforowany”. prosty test wydaje się działać w ten sposób, niezależnie od tego, czy dane wyjściowe trafią do pliku, potoku czy terminala. Czy masz jakieś odniesienia do tego, która wersja biblioteki C zrobiłaby inaczej?
ilkkachu
4
Linux jest jądrem, buforowanie stdio jest funkcją użytkownika, jądro nie jest w to zaangażowane. Istnieje wiele implementacji libc dla jąder Linuksa, najczęściej w systemach typu serwer / stacja robocza to implementacja GNU, z którą stdout jest w pełni buforowany (buforowany w linii jeśli tty), a stderr nie jest buforowany.
Stéphane Chazelas
1
@schily, tylko test, który przeprowadziłem: paste.dy.fi/xk4 . Ten sam wynik uzyskałem również z okropnie przestarzałym systemem.
ilkkachu
1
@schily To nieprawda. Na przykład piszę ten komentarz za pomocą Alpine Linux, który zamiast tego używa musla.
NieDzejkob,