anomalia printf dopo “fork ()”

Sistema operativo: Linux, linguaggio: puro C

Sto andando avanti nell’apprendimento della programmazione C in generale e nella programmazione C sotto UNIX in un caso speciale.

Ho rilevato uno strano (per me) comportamento della printf() dopo aver usato una chiamata a fork() .

Codice

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

Produzione

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

Perché la seconda stringa “Hello” si è verificata nell’output del figlio?

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

Ma! Se mettiamo un carattere \n alla fine di ogni stringa, otteniamo l’output atteso:

 #include  #include  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?

Prendo atto che è un’intestazione non standard; L’ho sostituito con e il codice è stato compilato in modo pulito.

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

Potresti dimostrarlo nel tuo programma rivisto collegando l’output del tuo programma a, per 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.


Analisi riga per riga, come richiesto (parentesi, ecc. Rimosse e gli spazi iniziali rimossi 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 );

Le analisi:

  1. Copie “Hello, my pid is 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. Ci 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 il pid (una delle poche differenze tra i due processi – i valori restituiti 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 attraverso il 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 newline alla fine delle istruzioni printf() (che diventa molto presto una pratica standard) altera molto l’output:

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

Ora ottengo:

 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’output arriva al terminale, è in modalità line buffer, quindi la riga ‘Hello’ appare prima del fork() e c’era solo una copia. Quando l’output viene convogliato su cat , è completamente bufferizzato, quindi non appare nulla prima di fork() ed entrambi i processi hanno la riga ‘Hello’ nel buffer da svuotare.

Il motivo è che senza il \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 fork, quindi viene stampato due volte.

Aggiungendo \n tuttavia, si forza il buffer a essere scaricato ed emesso sullo schermo. Questo accade prima della fork e quindi viene stampato una sola volta.

È ansible forzare questo accada usando il metodo fflush . Per esempio

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

fork() crea effettivamente una copia del processo. Se, prima di chiamare fork() , disponeva di dati archiviati nel buffer, sia il genitore che il figlio avrebbero 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. Quindi, se stai usando lo stdio sia in genitore che in figlio, allora devi fflush prima di forare, per assicurarti che non ci siano dati bufferizzati.

Spesso, il bambino viene utilizzato solo per chiamare una funzione exec* . Poiché questo 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 stderr o stderr usando qualsiasi funzione stdio ( write è ok), quindi chiamare _exit (o _Exit ) piuttosto che chiamare exit o semplicemente _Exit (che _Exit qualsiasi output bufferizzato). O evitare del tutto il problema sciacquando prima di biforcarsi.