¿Por qué un programa con fork () a veces imprime su salida varias veces?

50

En el Programa 1 Hello worldse imprime solo una vez, pero cuando lo elimino \ny lo ejecuto (Programa 2), la salida se imprime 8 veces. ¿Alguien puede explicarme el significado de \naquí y cómo afecta al fork()?

Programa 1

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...\n");
    fork();
    fork();
    fork();
}

Salida 1:

hello world... 

Programa 2

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...");
    fork();
    fork();
    fork();
}

Salida 2:

hello world... hello world...hello world...hello world...hello world...hello world...hello world...hello world...
lmaololrofl
fuente
10
Intente ejecutar el Programa 1 con salida a un archivo ( ./prog1 > prog1.out) o una tubería ( ./prog1 | cat). Prepárate para alucinar. :-) ⁠
G-Man dice 'Reincorporar a Monica' el
Preguntas y respuestas relevantes que cubren otra variante de este problema: el sistema C (“bash”) ignora el stdin
Michael Homer
13
Esto ha reunido algunos votos cercanos, por lo que un comentario al respecto: las preguntas sobre "API de UNIX C e interfaces del sistema" están explícitamente permitidas . Los problemas fork()de almacenamiento en búfer son un encuentro común también en los scripts de shell, y también son algo específicos de Unix, por lo que parece que esto es bastante sobre el tema para Unix.SE.
ilkkachu
@ilkkachu en realidad, si lees ese enlace y haces clic en la meta pregunta a la que se refiere, explica muy claramente que esto está fuera de tema. El hecho de que algo sea C, y unix tiene C, no lo hace sobre el tema.
Patrick
@Patrick, en realidad, lo hice. Y sigo pensando que se ajusta a la cláusula "dentro de lo razonable", pero por supuesto que soy solo yo.
ilkkachu

Respuestas:

93

Cuando se envía a la salida estándar utilizando la printf()función de la biblioteca C , la salida generalmente se almacena en búfer. El búfer no se vacía hasta que fflush(stdout)salga una nueva línea, llame o salga del programa ( _exit()aunque no mediante una llamada ). El flujo de salida estándar se almacena de manera predeterminada en línea de esta manera cuando está conectado a un TTY.

Cuando se bifurca el proceso en el "Programa 2", los procesos secundarios heredan cada parte del proceso principal, incluido el búfer de salida no vaciado. Esto copia efectivamente el búfer no vaciado a cada proceso secundario.

Cuando finaliza el proceso, las memorias intermedias se vacían. Comienza un gran total de ocho procesos (incluido el proceso original), y el búfer no vaciado se vaciará al finalizar cada proceso individual.

Es ocho porque en cada fork()uno obtienes el doble del número de procesos que tenías antes fork()(ya que son incondicionales), y tienes tres de estos (2 3 = 8).

Kusalananda
fuente
14
Relacionado: puede terminar maincon _exit(0)solo hacer una llamada al sistema de salida sin borrar los buffers, y luego se imprimirá cero veces sin una nueva línea. ( La implementación de syscall de exit () y ¿Cómo es que _exit (0) (salir por syscall) me impide recibir ningún contenido estándar? ). O puede canalizar Program1 cato redirigirlo a un archivo y ver que se imprima 8 veces. (stdout está completamente protegido por defecto cuando no es un TTY). O agregue un fflush(stdout)al caso de no nueva línea antes del 2do fork()...
Peter Cordes
17

No afecta el tenedor de ninguna manera.

En el primer caso, terminas con 8 procesos sin nada que escribir, porque el búfer de salida ya estaba vacío (debido a \n).

En el segundo caso, todavía tiene 8 procesos, cada uno con un búfer que contiene "Hola mundo ..." y el búfer se escribe al final del proceso.

edc65
fuente
12

@Kusalananda explicó por qué se repite la salida . Si tiene curiosidad por qué la salida se repite 8 veces y no solo 4 veces (el programa base + 3 bifurcaciones):

int main()
{
    printf("hello world...");
    fork(); // here it creates a copy of itself --> 2 instances
    fork(); // each of the 2 instances creates another copy of itself --> 4 instances
    fork(); // each of the 4 instances creates another copy of itself --> 8 instances
}
Honza Zidek
fuente
2
esto es básico de fork
Prvt_Yadav
3
@Debian_yadav probablemente sea obvio solo si está familiarizado con sus implicaciones. Al igual que el lavado de buffers estándar , por ejemplo.
roaima
2
@Debian_yadav: en.wikipedia.org/wiki/False_consensus_effect : ¿por qué deberíamos hacer preguntas si todos lo saben todo?
Honza Zidek
8
@Debian_yadav No puedo leer la mente del OP, así que no lo sé. De todos modos, stackexchange es un lugar donde también otros buscan conocimiento y creo que mi respuesta puede ser una adición útil a la buena respuesta de Kulasandra. Mi respuesta agrega algo (básico pero útil), comparado con el de edc65 que simplemente repite lo que Kulasandra dijo 2 horas antes que él.
Honza Zidek
2
Esto es solo un breve comentario a una respuesta, no una respuesta real. La pregunta se refiere a "varias veces" no por eso es exactamente 8.
tubo de
3

El trasfondo importante aquí es que stdoutse requiere que el estándar proteja la línea como configuración predeterminada.

Esto hace que \na vacíe la salida.

Como el segundo ejemplo no contiene la nueva línea, la salida no se vacía y, como fork()copia todo el proceso, también copia el estado del stdoutbúfer.

Ahora, estas fork()llamadas en su ejemplo crean 8 procesos en total, todos ellos con una copia del estado del stdoutbúfer.

Por definición, todos estos procesos llaman exit()cuando regresan main()y exit()llamadas fflush()seguidas de fclose()todas las secuencias stdio activas . Esto incluye stdouty, como resultado, ve el mismo contenido ocho veces.

Es una buena práctica llamar fflush()a todas las secuencias con salida pendiente antes de llamar fork()o dejar que la llamada secundaria bifurcada explícitamente _exit()solo salga del proceso sin vaciar las secuencias stdio.

Tenga en cuenta que las llamadas exec()no eliminan las memorias intermedias estándar, por lo que está bien no preocuparse por las memorias intermedias si usted (después de llamar fork()) llama exec()y (si eso falla) _exit().

Por cierto: para comprender que el almacenamiento en búfer incorrecto puede causar, aquí hay un error anterior en Linux que se ha solucionado recientemente:

El estándar requiere stderrque no stderresté almacenado en búfer de manera predeterminada, pero Linux lo ignoró e hizo que la línea esté en búfer y (aún peor) esté completamente en búfer en caso de que stderr fuera redirigido a través de una tubería. Entonces, los programas escritos para UNIX produjeron cosas sin nueva línea demasiado tarde en Linux.

Vea el comentario a continuación, parece estar solucionado ahora.

Esto es lo que hago para solucionar este problema de Linux:

    /* 
     * Linux comes with a broken libc that makes "stderr" buffered even 
     * though POSIX requires "stderr" to be never "fully buffered". 
     * As a result, we would get garbled output once our fork()d child 
     * calls exit(). We work around the Linux bug by calling fflush() 
     * before fork()ing. 
     */ 
    fflush(stderr); 

Este código no daña en otras plataformas, ya que llamar fflush()a una transmisión que se acaba de vaciar es un noop.

astuto
fuente
2
No, se requiere que stdout esté completamente protegido, a menos que se trate de un dispositivo interactivo, en cuyo caso no está especificado, pero en la práctica está protegido de línea. Se requiere que stderr no esté completamente protegido. Ver pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/…
Stéphane Chazelas
Mi página de manual para setbuf()Debian ( la de man7.org se parece ) dice que "el flujo de error estándar stderr siempre está sin búfer por defecto". y una prueba simple parece actuar de esa manera, independientemente de si la salida va a un archivo, una tubería o un terminal. ¿Tiene alguna referencia de qué versión de la biblioteca C haría de otra manera?
ilkkachu
44
Linux es un kernel, el almacenamiento en búfer stdio es una característica del usuario, el kernel no está involucrado allí. Hay varias implementaciones de libc disponibles para los núcleos de Linux, la más común en los sistemas de tipo servidor / estación de trabajo es la implementación de GNU, con la cual stdout está completamente protegido (línea almacenada si tty), y stderr no está almacenado.
Stéphane Chazelas
1
@schily, solo la prueba que ejecuté: paste.dy.fi/xk4 . También obtuve el mismo resultado con un sistema horriblemente desactualizado.
ilkkachu
1
@schily Eso no es cierto. Por ejemplo, estoy escribiendo este comentario usando Alpine Linux, que usa musl en su lugar.
NieDzejkob