anomalía de printf después de "fork()"

Resuelto pechenie asked hace 14 años • 3 respuestas

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 \ncará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?

pechenie avatar Mar 28 '10 02:03 pechenie
Aceptado

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

  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 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.
  2. Nos proporciona dos procesos separados, con exactamente el mismo material en el buffer de salida estándar.
  3. El niño tiene pid == 0y 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 "¡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.
  5. 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.
  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 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á.

Jonathan Leffler avatar Mar 27 '2010 20:03 Jonathan Leffler

La razón es que sin el \nal 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 \naunque 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 fflushmétodo. Por ejemplo

printf( "Hello, my pid is %d", getpid() );
fflush(stdout);
JaredPar avatar Mar 27 '2010 19:03 JaredPar

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 fflushantes 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 fflushsi 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 ( writeestá bien) y luego llame _exit(o _Exit) en lugar de llamar exito 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.

mark4o avatar Mar 28 '2010 11:03 mark4o