2010-03-27 7 views
59

SO: Linux, Lingua: puro Cprintf anomalia dopo "fork()"

Mi sto muovendo in avanti ad imparare la programmazione C in generale, e di programmazione C sotto UNIX in un caso speciale.

Ho rilevato uno strano (per me) comportamento della funzione printf() dopo l'utilizzo di una chiamata fork().

Codice

#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; 
} 

uscita

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

Perché il secondo "Ciao" stringa si verificano in uscita del bambino?

Sì, è esattamente ciò che il genitore ha stampato quando è stato avviato, con il genitore pid.

Ma! Se mettiamo un personaggio \n alla fine di ogni stringa otteniamo i risultati attesi:

#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; 
} 

uscita:

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

Perché succede? È un comportamento corretto, o è un bug?

risposta

75

Ho notato che <system.h> è un'intestazione non standard; L'ho sostituito con <unistd.h> e il codice è stato compilato in modo pulito.

Quando l'uscita del programma sta per un terminale (schermo), è in modalità linea bufferizzata. Quando l'output del tuo programma va in pipe, è completamente bufferizzato. È possibile controllare la modalità di buffering tramite la funzione C standard setvbuf() e _IOFBF (buffering completo), _IOLBF (buffer di linea) e _IONBF (senza buffer).

È possibile dimostrarlo nel programma modificato piping l'output del programma per, ad esempio, cat. Anche con le nuove righe alla fine delle stringhe printf(), vedresti le doppie informazioni. Se lo invii direttamente al terminale, vedrai solo un sacco di informazioni.

La morale della storia è fare attenzione a chiamare fflush(0); per svuotare tutti i buffer I/O prima della foratura.


linea per linea di analisi, come richiesto (tra parentesi graffe, ecc rimossi - e gli spazi iniziali Rimosso dall'editor markup):

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

L'analisi:

  1. Copie "Ciao, il mio PID è 1234" nel buffer per l'output standard. Poiché alla fine non è presente una nuova riga e l'output è in esecuzione in modalità buffer di riga (o in modalità buffer completo), sul terminale non viene visualizzato nulla.
  2. Fornisce due processi separati, con esattamente lo stesso materiale nel buffer stdout.
  3. Il bambino ha pid == 0 ed esegue le righe 4 e 5; il genitore ha un valore diverso da zero per pid (una delle poche differenze tra i due processi - i valori di ritorno da getpid() e getppid() sono altri due).
  4. Aggiunge una nuova riga e "Sono stato biforcuto!: D" al buffer di output del bambino. La prima riga di output appare sul terminale; il resto viene tenuto nel buffer poiché l'output è bufferizzato dalla linea.
  5. Tutto si ferma per 3 secondi. Dopo questo, il bambino esce normalmente attraverso il ritorno alla fine del main. A quel punto, i dati residui nel buffer stdout vengono svuotati. Questo lascia la posizione di uscita alla fine di una riga poiché non esiste una nuova riga.
  6. Il genitore viene qui.
  7. Il genitore aspetta che il bambino finisca di morire.
  8. Il genitore aggiunge una nuova riga e "1345 è stato biforcuto!" al buffer di output. La nuova riga svuota il messaggio "Ciao" sull'output, dopo la riga incompleta generata dal figlio.

Il genitore ora esce normalmente dal ritorno alla fine del main e i dati residui vengono svuotati; poiché non c'è ancora una fine riga alla fine, la posizione del cursore si trova dopo il punto esclamativo e il prompt della shell appare sulla stessa riga.

Quello che vedo è:

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: 

I numeri PID sono diversi - ma l'aspetto generale è chiaro.L'aggiunta di nuove righe alla fine degli printf() dichiarazioni (che diventa una pratica standard molto rapidamente) altera l'uscita molto:

#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; 
} 

io ora ottenere:

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: 

Si noti che quando l'uscita va al terminale , è in modalità line buffer, quindi la riga 'Hello' viene visualizzata prima dello fork() e c'era solo una copia. Quando l'output viene convogliato su cat, è completamente bufferizzato, quindi non viene visualizzato nulla prima che lo fork() e entrambi i processi abbiano la riga "Ciao" nel buffer da svuotare.

+0

Ok, ho capito. Ma non riesco ancora a spiegarmi perché il "buffer garbage" appare alla fine della riga appena stampata nell'output del bambino? Ma aspetta, ora dubito che sia davvero l'uscita di CHILD .. oh, potresti spiegare perché l'output ha un aspetto ESATTAMENTE (nuova stringa PRIMA del vecchio) così, passo dopo passo, quindi sarei molto grato. Grazie comunque! – pechenie

+0

spiegazione MOLTO impressionante! Grazie mille, finalmente l'ho capito chiaramente! P.S .: Ho dato un voto per te in precedenza, e ora ho stupidamente cliccato sulla "freccia su" ancora una volta, quindi voto scomparso. Ma non posso dartelo ancora una volta a causa di "la risposta è troppo vecchia" :( P.P.S .: ti ho dato un voto in un'altra domanda e grazie ancora! – pechenie

22

Il motivo è che senza lo \n alla fine della stringa di formato il valore non viene immediatamente stampato sullo schermo. Invece è bufferizzato all'interno del processo. Ciò significa che non viene stampato fino a dopo l'operazione della forcella, quindi viene stampato due volte.

Aggiungendo il \n, tuttavia, si forza il buffer da svuotare ed emettere sullo schermo. Questo accade prima della forcella e quindi viene stampato una sola volta.

È possibile forzare il verificarsi di questo problema utilizzando il metodo fflush. Ad esempio

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

Grazie per la risposta! – pechenie

+0

'fflush (stdout);' Sembra essere la risposta più corretta qui imo. –

5

fork() crea effettivamente una copia del processo. Se, prima di chiamare fork(), disponeva di dati archiviati nel buffer, sia il genitore che il figlio avranno gli stessi dati memorizzati nel buffer. La prossima volta che ognuno di essi fa qualcosa per svuotare il suo buffer (come la stampa di una nuova riga nel caso dell'output del terminale), vedrete quell'output bufferizzato oltre a qualsiasi nuovo output prodotto da quel processo. Pertanto, se si intende utilizzare stdio sia in genitore che in figlio, è necessario eseguire il fflush prima di eseguire la biforcazione per assicurarsi che non vi siano dati memorizzati nel buffer.

Spesso, il bambino viene utilizzato solo per chiamare una funzione exec*. Dal momento che sostituisce l'immagine completa del processo figlio (compresi eventuali buffer) non è tecnicamente necessario fflush se questo è davvero tutto ciò che si farà nel bambino. Tuttavia, se possono esserci dati bufferizzati, è necessario prestare attenzione a come viene gestito un errore di exec. In particolare, evitare di stampare l'errore su stdout o stderr utilizzando qualsiasi funzione stdio (write è ok), quindi chiamare _exit (o _Exit) anziché chiamare exit o semplicemente tornare (che svuoterà qualsiasi output bufferizzato). O evitare del tutto il problema sciacquando prima di biforcarsi.