Perché un programma con fork () a volte stampa il suo output più volte?


50

Nel Programma 1 Hello worldviene stampato solo una volta, ma quando lo rimuovo \ned eseguo (Programma 2), l'output viene stampato 8 volte. Qualcuno può spiegarmi il significato di \nqui e come influisce fork()?

Programma 1

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

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

Uscita 1:

hello world... 

Programma 2

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

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

Uscita 2:

hello world... hello world...hello world...hello world...hello world...hello world...hello world...hello world...

10
Prova a eseguire il Programma 1 con l'output in un file ( ./prog1 > prog1.out) o in una pipe ( ./prog1 | cat). Preparati a farti saltare la testa. :-) ⁠
G-Man dice 'Ripristina Monica' il

Q + A rilevante che copre un'altra variante di questo problema: il sistema C ("bash") ignora stdin
Michael Homer,

13
Ciò ha raccolto alcuni voti ravvicinati, quindi un commento al riguardo: le domande su "API UNIX C e interfacce di sistema" sono esplicitamente consentite . I problemi di buffering sono un incontro comune anche negli script di shell ed fork()è anche un po 'specifico per unix, quindi sembrerebbe che questo sia abbastanza in argomento per unix.SE.
ilkkachu,

@ilkkachu in realtà, se leggi quel link e fai clic sulla meta domanda a cui fa riferimento, spiega chiaramente che questo è fuori tema. Solo perché qualcosa è C, e unix ha C, non lo rende in argomento.
Patrick,

@Patrick, in realtà, l'ho fatto. E penso ancora che si adatti alla clausola "entro limiti ragionevoli", ma ovviamente sono solo io.
ilkkachu,

Risposte:


93

Quando si invia all'output standard usando la printf()funzione della libreria C , l'output è di solito bufferato. Il buffer non viene scaricato fino a quando non viene emessa una nuova riga, non si chiama fflush(stdout)o si esce dal programma (non tramite la chiamata _exit()però). Per impostazione predefinita, il flusso di output standard è bufferizzato in linea in questo modo quando è collegato a un TTY.

Quando si esegue il fork del processo in "Programma 2", i processi figlio ereditano ogni parte del processo padre, incluso il buffer di output non scaricato. Ciò copia efficacemente il buffer non scaricato a ciascun processo figlio.

Al termine del processo, i buffer vengono scaricati. Si avvia un totale complessivo di otto processi (incluso il processo originale) e il buffer non scaricato verrà scaricato alla fine di ogni singolo processo.

Sono otto perché ad ognuno fork()ottieni il doppio del numero di processi che hai avuto prima del fork()(poiché sono incondizionati), e ne hai tre di questi (2 3 = 8).


14
Related: si può finire maincon _exit(0)a solo fare una chiamata di sistema di uscire senza buffer vampate di calore, e poi verrà stampato zero volte senza un ritorno a capo. ( L'implementazione di Syscall di exit () e How come _exit (0) (uscire da syscall) mi impedisce di ricevere contenuti stdout? ). Oppure puoi reindirizzare Program1 cato reindirizzarlo su un file e vederlo stampare 8 volte. (stdout è bufferizzato per impostazione predefinita quando non è un TTY). Oppure aggiungi un caso fflush(stdout)al no-newline prima del 2 ° fork()...
Peter Cordes,

17

Non influisce in alcun modo sulla forcella.

Nel primo caso, si ottengono 8 processi senza nulla da scrivere, poiché il buffer di output è già stato svuotato (a causa di \n).

Nel secondo caso hai ancora 8 processi, ognuno con un buffer contenente "Hello world ..." e il buffer è scritto alla fine del processo.


12

@Kusalananda ha spiegato perché l'output si ripete . Se sei curioso di sapere perché l'output viene ripetuto 8 volte e non solo 4 volte (il programma di base + 3 forchette):

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
}

2
questo è di base del fork
Prvt_Yadav il

3
@Debian_yadav probabilmente è ovvio solo se hai familiarità con le sue implicazioni. Come svuotare i buffer stdio , ad esempio.
roaima,

2
@Debian_yadav: en.wikipedia.org/wiki/False_consensus_effect - perché dovremmo porre domande se tutti sanno tutto?
Honza Zidek,

8
@Debian_yadav Non riesco a leggere la mente del PO, quindi non lo so. Comunque, stackexchange è un luogo dove anche altri cercano la conoscenza e penso che la mia risposta possa essere un'utile aggiunta alla buona risposta di Kulasandra. La mia risposta aggiunge qualcosa (di base ma utile), rispetto a quello di edc65 che ripete semplicemente ciò che Kulasandra ha detto 2 ore prima di lui.
Honza Zidek,

2
Questo è solo un breve commento a una risposta, non una risposta effettiva. La domanda riguarda "più volte" e non il motivo per cui è esattamente 8.
pipe

3

Lo sfondo importante qui è che stdoutè richiesto il buffer di linea dallo standard come impostazione predefinita.

Ciò provoca \na svuotare l'output.

Poiché il secondo esempio non contiene la nuova riga, l'output non viene svuotato e, poiché fork()copia l'intero processo, copia anche lo stato del stdoutbuffer.

Ora, queste fork()chiamate nel tuo esempio creano 8 processi in totale, tutti con una copia dello stato del stdoutbuffer.

Per definizione, tutti questi processi chiamano exit()al ritorno da main()e exit()chiamate fflush()seguite da fclose()tutti i flussi stdio attivi . Ciò include stdoute, di conseguenza, viene visualizzato lo stesso contenuto otto volte.

È buona norma chiamare fflush()tutti i flussi con output in sospeso prima di chiamare fork()o lasciare che il bambino biforcato chiami esplicitamente _exit()che esce dal processo solo senza svuotare i flussi stdio.

Nota che la chiamata exec()non scarica i buffer dello stdio, quindi è OK non preoccuparsi dei buffer dello stdio se tu (dopo aver chiamato fork()) chiami exec()e (se ciò fallisce) chiama _exit().

A proposito: per capire che può causare un buffering errato, ecco un ex bug in Linux che è stato corretto di recente:

Lo standard richiede stderrdi non essere bufferizzato per impostazione predefinita, ma Linux lo ha ignorato e ha reso il stderrbuffer di linea e (anche peggio) completamente bufferizzato nel caso in cui stderr fosse reindirizzato attraverso una pipe. Quindi i programmi scritti per UNIX hanno prodotto cose senza newline troppo tardi su Linux.

Vedi il commento qui sotto, sembra essere stato risolto ora.

Questo è quello che faccio per aggirare questo problema con Linux:

    /* 
     * 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); 

Questo codice non danneggia le altre piattaforme poiché la chiamata fflush()su uno stream appena scaricato è un noop.


2
No, lo stdout deve essere completamente bufferizzato a meno che non sia un dispositivo interattivo nel qual caso non è specificato, ma in pratica viene quindi bufferizzato in linea. stderr non deve essere completamente bufferizzato. Vedi pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/…
Stéphane Chazelas

La mia pagina man per setbuf(), su Debian ( quella su man7.org sembra simile ), afferma che "il flusso di errori standard stderr è sempre senza buffer per impostazione predefinita". e un semplice test sembra agire in quel modo, indipendentemente dal fatto che l'output vada a un file, una pipe o un terminale. Hai qualche riferimento per quale versione della libreria C farebbe altrimenti?
ilkkachu,

4
Linux è un kernel, il buffering di stdio è una funzionalità utente, il kernel non è coinvolto lì. Esistono diverse implementazioni libc disponibili per i kernel Linux, la più comune nei sistemi di tipo server / workstation è l'implementazione GNU, con la quale stdout è completamente buffer (line buffered se tty) e stderr non ha buffer.
Stéphane Chazelas,

1
@schily, solo il test che ho eseguito: paste.dy.fi/xk4 . Ho ottenuto lo stesso risultato anche con un sistema orribilmente obsoleto.
ilkkachu,

1
@schily Non è vero. Ad esempio, sto scrivendo questo commento usando Alpine Linux, che invece utilizza musl.
NieDzejkob,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.