anomalía printf después de “fork ()”

Sistema operativo: Linux, Idioma: C puro

Avanzo en el aprendizaje de la progtwigción C en general, y la progtwigción en C bajo UNIX en un caso especial.

Detecté un comportamiento extraño (para mí) de la printf() después de usar una llamada fork() .

Código

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

Salida

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

¿Por qué se produjo la segunda cadena “Hola” en la salida del hijo?

Sí, es exactamente lo que el padre imprimió cuando comenzó, con el pid del padre.

¡Pero! Si colocamos un caracter \n al final de cada cadena obtenemos el resultado esperado:

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

Salida :

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

¿Por que sucede? ¿Es correcto el comportamiento o es un error?

que es un encabezado no estándar; Lo reemplacé con y el código comstackdo limpiamente.

Cuando la salida de su progtwig va a un terminal (pantalla), se almacena en línea. Cuando la salida de su progtwig va a una tubería, está totalmente amortiguada. Puede controlar el modo de almacenamiento en búfer con los modos de función estándar setvbuf() y _IOFBF (búfer completo), _IOLBF (búfer de línea) y _IONBF (sin búfer).

Puede demostrar esto en su progtwig revisado conectando la salida de su progtwig a, por ejemplo, cat . Incluso con las nuevas líneas al final de las cadenas printf() , verá la información doble. Si lo envía directamente a la terminal, verá solo una gran cantidad de información.

La moraleja de la historia es tener cuidado de llamar a fflush(0); vaciar todos los búferes de E / S antes de bifurcar.


Análisis línea por línea, según se solicitó (se eliminaron llaves, etc. y se eliminaron los espacios iniciales mediante el editor de marcado):

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

El analisis:

  1. Copia “Hola, mi pid es 1234” en el búfer para salida estándar. Debido a que no hay una línea nueva al final y la salida se está ejecutando en modo de línea-buffer (o modo de buffer completo), no aparece nada en el terminal.
  2. Nos da dos procesos separados, con exactamente el mismo material en el buffer stdout.
  3. El niño tiene pid == 0 y ejecuta las líneas 4 y 5; el padre tiene un valor distinto de cero para pid (una de las pocas diferencias entre los dos procesos – los valores de retorno de getpid() y getppid() son dos más).
  4. Agrega una nueva línea y “¡Fui bifurcada!: D” al búfer de salida del niño. La primera línea de salida aparece en la terminal; el rest se mantiene en el búfer ya que la salida se almacena en línea.
  5. Todo se detiene por 3 segundos. Después de esto, el niño sale normalmente por la vuelta al final de la pista principal. En ese punto, los datos residuales en el búfer stdout se vacían. Esto deja la posición de salida al final de una línea ya que no hay línea nueva.
  6. El padre viene aquí.
  7. El padre espera que el niño termine de morir.
  8. El padre agrega una nueva línea y “¡1345 fue bifurcado!” al buffer de salida. La nueva línea vacía el mensaje ‘Hola’ a la salida, después de la línea incompleta generada por el niño.

El padre ahora sale normalmente a través del retorno al final de main, y los datos residuales se vacían; dado que todavía no hay una línea nueva al final, la posición del cursor está después del signo de exclamación, y el indicador del shell aparece en la misma línea.

Lo que veo es:

 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: 

Los números PID son diferentes, pero la apariencia general es clara. Agregar nuevas líneas al final de las instrucciones printf() (que se convierte en práctica estándar muy rápidamente) altera mucho la salida:

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

Ahora obtengo:

 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: 

Tenga en cuenta que cuando la salida va al terminal, se almacena en la línea, por lo que la línea ‘Hola’ aparece antes de la fork() y solo había una copia. Cuando la salida se canaliza a cat , está totalmente amortiguada, por lo que no aparece nada antes de la fork() y ambos procesos tienen la línea ‘Hello’ en el búfer que se va a enjuagar.

El motivo es que sin \n al final de la cadena de formato, el valor no se imprime inmediatamente en la pantalla. En cambio, está almacenado en el proceso. Esto significa que no se imprime hasta después de la operación de la horquilla, por lo tanto, se imprime dos veces.

Agregar \n sin embargo, obliga a que el búfer se vacíe y se envíe a la pantalla. Esto sucede antes de la horquilla y, por lo tanto, solo se imprime una vez.

Puedes forzar esto para que ocurra usando el método fflush . Por ejemplo

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

fork() crea efectivamente una copia del proceso. Si, antes de llamar a fork() , tenía datos que estaban almacenados en búfer, tanto el padre como el hijo tendrán los mismos datos almacenados en el búfer. La próxima vez que cada uno de ellos haga algo para descargar su búfer (como la impresión de una nueva línea en el caso de la salida del terminal), verá esa salida almacenada en el búfer además de cualquier nueva salida producida por ese proceso. Por lo tanto, si va a utilizar stdio en el padre y el hijo, debe fflush antes de bifurcar, para asegurarse de que no haya datos almacenados en el búfer.

A menudo, el niño se usa solo para llamar a una función exec* . Como eso reemplaza la imagen completa del proceso secundario (incluidos los almacenamientos intermedios) técnicamente no hay necesidad de fflush si eso es realmente todo lo que va a hacer en el niño. Sin embargo, si puede haber datos almacenados en el búfer, debe tener cuidado con la forma en que se maneja una falla del ejecutivo. En particular, evite imprimir el error a stdout o stderr usando cualquier función stdio ( write is ok), y luego llame a _exit (o _Exit ) en lugar de llamar a exit o simplemente regresar (lo que eliminará cualquier salida almacenada). O evite el problema por completo al enjuagar antes de bifurcar.