anomalía de printf después de "fork()"
SO: Linux, Idioma: C puro
Estoy avanzando en el aprendizaje de programación en C en general, y programación en C bajo UNIX en un caso especial.
Detecté un comportamiento extraño (para mí) de la printf()
función después de usar una fork()
llamada.
Código
#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;
}
Producción
Hello, my pid is 1111
I was forked! :DHello, my pid is 1111
2222 was forked!
¿Por qué apareció la segunda cadena "Hola" en la salida del niño?
Sí, es exactamente lo que el padre imprimió cuando comenzó, con el nombre del padre pid
.
¡Pero! Si colocamos un \n
carácter al final de cada cadena obtenemos el resultado esperado:
#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;
}
Producción :
Hello, my pid is 1111
I was forked! :D
2222 was forked!
¿Por que sucede? ¿Es un comportamiento correcto o es un error?
Observo que <system.h>
es un encabezado no estándar; Lo reemplacé <unistd.h>
y el código se compiló limpiamente.
Cuando la salida de su programa va a una terminal (pantalla), se almacena en línea. Cuando la salida de su programa va a una tubería, está completamente almacenada en el búfer. Puede controlar el modo de almacenamiento en búfer mediante la función C estándar setvbuf()
y los modos _IOFBF
(búfer completo), _IOLBF
(búfer de línea) y _IONBF
(sin almacenamiento en búfer).
Podría demostrar esto en su programa revisado canalizando la salida de su programa a, por ejemplo, cat
. Incluso con las nuevas líneas al final de las printf()
cadenas, verás la información doble. Si lo envía directamente al terminal, verá solo esa gran cantidad de información.
La moraleja de la historia es tener cuidado al llamar fflush(0);
para vaciar todos los buffers de E/S antes de realizar la bifurcación.
Análisis línea por línea, según lo solicitado (se eliminaron las llaves, etc., y se eliminaron los espacios iniciales mediante el editor de marcado):
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 );
El analisis:
- Copia "Hola, mi pid es 1234" en el búfer para salida estándar. Debido a que no hay una nueva línea al final y la salida se ejecuta en modo de búfer de línea (o modo de búfer completo), no aparece nada en la terminal.
- Nos proporciona dos procesos separados, con exactamente el mismo material en el buffer de salida estándar.
- El niño tiene
pid == 0
y ejecuta las líneas 4 y 5; el padre tiene un valor distinto de cero parapid
(una de las pocas diferencias entre los dos procesos: los valores de retorno degetpid()
ygetppid()
son dos más). - Agrega una nueva línea y "¡Me bifurcaron! :D" al búfer de salida del niño. La primera línea de salida aparece en la terminal; el resto se mantiene en el búfer ya que la salida está almacenada en línea.
- Todo se detiene durante 3 segundos. Después de esto, el niño sale normalmente por el retorno al final de la calle principal. En ese punto, los datos residuales en el búfer de salida estándar se vacían. Esto deja la posición de salida al final de una línea ya que no hay una nueva línea.
- El padre viene aquí.
- El padre espera que el niño termine de morir.
- El padre agrega una nueva línea y "¡1345 se bifurcó!" al buffer de salida. La nueva línea enví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 eliminan; Como todavía no hay una nueva línea al final, la posición del cursor está después del signo de exclamación y el símbolo 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 printf()
declaraciones (lo que se convierte en una práctica estándar muy rápidamente) altera mucho el resultado:
#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;
}
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 a la terminal, se almacena en un búfer de línea, por lo que la línea 'Hola' aparece antes y fork()
solo había una copia. Cuando la salida se canaliza a cat
, está completamente almacenada en el búfer, por lo que no aparece nada antes fork()
y ambos procesos tienen la línea 'Hola' en el búfer que se vaciará.
La razón es que sin el \n
al final de la cadena de formato, el valor no se imprime inmediatamente en la pantalla. En lugar de ello, se almacena dentro del proceso. Esto significa que en realidad no se imprime hasta después de la operación de bifurcación, por lo que se imprime dos veces.
Agregar el \n
aunque obliga a vaciar el búfer y mostrarlo en la pantalla. Esto sucede antes de la bifurcación y, por lo tanto, solo se imprime una vez.
Puede forzar que esto ocurra utilizando el fflush
método. Por ejemplo
printf( "Hello, my pid is %d", getpid() );
fflush(stdout);
fork()
crea efectivamente una copia del proceso. Si, antes de llamar fork()
, tenía datos almacenados en el 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 vaciar su búfer (como imprimir una nueva línea en el caso de la salida del terminal), verá esa salida almacenada en el búfer además de cualquier salida nueva producida por ese proceso. Entonces, si va a usar stdio tanto en el padre como en el hijo, debe hacerlo fflush
antes de bifurcar, para asegurarse de que no haya datos almacenados en el búfer.
A menudo, el niño se utiliza sólo para llamar a una exec*
función. Dado que eso reemplaza la imagen completa del proceso secundario (incluidos los búferes), técnicamente no hay necesidad de hacerlo 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 ejecutiva. En particular, evite imprimir el error en stdout o stderr usando cualquier función stdio ( write
está bien) y luego llame _exit
(o _Exit
) en lugar de llamar exit
o simplemente regresar (lo que eliminará cualquier salida almacenada en el búfer). O evite el problema por completo tirando de la cadena antes de bifurcar.