2010-03-27 16 views
59

SO: Linux, Idioma: C puroprintf anomalía después de "tenedor()"

estoy avanzando en el aprendizaje de programación C en general, y de programación C bajo UNIX en un caso especial.

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

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

salida

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

¿Por qué ocurrió la segunda cadena "Hola" en la salida del niño?

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

¡Pero! Si colocamos un carácter \n al final de cada cadena se obtiene 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; 
} 

salida:

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

¿Por qué se produce? ¿Es correcto el comportamiento o es un error?

Respuesta

75

Noté que <system.h> es un encabezado no estándar; Lo reemplacé con <unistd.h> y el código compilado limpiamente.

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

Puede demostrar esto en su programa revisado conectando la salida de su programa a, por ejemplo, cat. Incluso con las líneas nuevas 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 al fflush(0); para vaciar todos los búferes de E/S antes de bifurcar.


línea por línea de análisis, conforme a lo solicitado (frenos, etc eliminado - y los espacios iniciales eliminados por 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 análisis:

  1. copias "Hola, mi PID es 1234" en el búfer de 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 búfer 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 elemento secundario. La primera línea de salida aparece en la terminal; el resto se mantiene en el búfer ya que la salida se almacena en línea.
  5. Todo se detiene durante 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 a 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 vacía el mensaje 'Hola' a la salida, después de la línea incompleta generada por el niño.

El elemento primario ahora sale normalmente a través del retorno al final de la página principal, y los datos residuales se descargan; 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 de PID son diferentes - pero el aspecto general es clara.La adición de nuevas líneas a finales de los printf() declaraciones (que se convierte en una práctica estándar muy rápidamente) altera la salida de un montón:

#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 consigo:

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 , está en línea, por lo que la línea 'Hola' aparece antes del fork() y solo había una copia. Cuando la salida se canaliza a cat, está totalmente amortiguada, por lo que no aparece nada antes del fork() y ambos procesos tienen la línea 'Hola' en el búfer que se va a enjuagar.

+0

Ok, lo tengo. Pero todavía no puedo explicarme por qué aparece la "basura del tampón" al final de la línea recién impresa en la salida del niño. Pero espere, ahora dudo que sea realmente la producción de CHILD ... oh, podría explicar por qué la salida se ve EXACTAMENTE (cadena nueva ANTES de la anterior) así, paso a paso, así que estaría muy agradecido. ¡Gracias de todos modos! – pechenie

+0

¡Explicación MUY impresionante! Muchas gracias, ¡finalmente lo entendí claramente! P.S .: He dado un voto por usted anteriormente, y ahora hice clic estúpidamente en la "flecha hacia arriba" una vez más, por lo que el voto desapareció. Pero no puedo devolvérselo una vez más porque "la respuesta es demasiado antigua" :( P.P.S .: Le di un voto en otra pregunta. ¡Y gracias una vez más! – pechenie

22

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 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.

Al agregar \n, se 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.

Puede forzar que esto ocurra utilizando el método fflush. Por ejemplo

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

¡Gracias por tu respuesta! – pechenie

+0

'fflush (stdout);' Parece ser la respuesta más correcta aquí imo. –

5

fork() crea efectivamente una copia del proceso. Si, antes de llamar al 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. Entonces, si vas a usar stdio en el padre y el hijo, entonces debes fflush antes de bifurcar, para asegurarte de que no haya datos almacenados en el búfer.

A menudo, el elemento secundario 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 está bien), y luego llame al _exit (o _Exit) en lugar de llamar al exit o simplemente devolver (lo que eliminará cualquier salida almacenada en el búfer). O evite el problema por completo al enjuagar antes de bifurcar.