Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

506
Vistas
¿Por qué printf() en el padre casi siempre gana la condición de carrera después de fork()?

Hay un acertijo un tanto famoso de Unix: escriba una expresión if para hacer que el siguiente programa imprima Hello, world! en la pantalla. La expr en if debe ser una expresión C legal y no debe contener otras estructuras de programa.

 if (expr) printf("Hello, "); else printf("world!\n");

La respuesta es fork() .

Cuando era más joven, solo me reía y lo olvidaba. Pero al repensarlo, descubrí que no podía entender por qué este programa es sorprendentemente confiable de lo que debería ser. El orden de ejecución después de fork() no está garantizado y existe una condición de carrera, pero en la práctica, casi siempre verás Hello, world!\n , nunca world!\nHello, .

Para demostrarlo, ejecuté el programa durante 100.000 rondas.

 for i in {0..100000}; do ./fork >> log done

En Linux 5.9 (Fedora 32, gcc 10.2.1, -O2 ), después de 100001 ejecuciones, el niño solo ganó 146 veces, el padre tiene una probabilidad de ganar del 99,9985 %.

 $ uname -a Linux openwork 5.9.14-1.qubes.x86_64 #1 SMP Tue Dec 15 17:29:47 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ wc -l log 100001 log $ grep ^world log | wc -l 146

El resultado es similar en FreeBSD 12.2 (clang 10.0.1, -O2 ). El niño solo ganó 68 veces, o el 0,00067 % de las veces, mientras que el padre ganó el 99,993 % de todas las ejecuciones.

Una nota al margen interesante es que ktrace ./fork cambia instantáneamente el resultado dominante a world\nHello, (porque solo se rastrea el padre), lo que demuestra la naturaleza Heisenbug del problema. Sin embargo, rastrear ambos procesos a través ktrace -i ./fork revierte el comportamiento, porque ambos procesos son rastreados e igualmente lentos.

 $ uname -a FreeBSD freebsd 12.2-RELEASE-p1 FreeBSD 12.2-RELEASE-p1 GENERIC amd64 $ wc -l log 100001 log $ grep ^world log | wc -l 68

¿Independencia del almacenamiento en búfer?

Una respuesta sugiere que el almacenamiento en búfer puede influir en el comportamiento de esta condición de carrera. Pero el comportamiento aún se presenta después de eliminar \n de printf().

 if (expr) printf("Hello"); else printf("World");

Y desactivar el almacenamiento en búfer de stdout a través de stdbuf en FreeBSD.

 for i in {0..10000}; do stdbuf -i0 -o0 -e0 ./fork >> log echo > log done $ wc -l log 10001 log $ grep -v "^HelloWorld" log | wc -l 30

¿Por qué printf() en el padre casi siempre gana la condición de carrera después de fork() en la práctica? ¿Está relacionado con los detalles de implementación interna de printf() en la biblioteca estándar de C? ¿La llamada al sistema write() ? ¿O la programación de procesos en los kernels de Unix?

over 4 years ago · Santiago Trujillo
2 Respuestas
Responde la pregunta

0

Cuando se ejecuta la fork , el proceso que lo ejecuta (el nuevo padre) se está ejecutando (por supuesto), y el hijo recién creado no. Para que el hijo se ejecute, el padre debe detenerse y el hijo debe recibir el procesador, o el hijo debe iniciarse en otro procesador, lo que lleva tiempo. Mientras tanto, el padre continúa la ejecución.

A menos que ocurra algún evento no relacionado, como que el padre agote el intervalo de tiempo que se le dio para compartir el procesador, gana la carrera.

over 4 years ago · Santiago Trujillo Denunciar

0

Cuando ejecuta printf(3) para enviar una cadena a la terminal (a cualquier dispositivo tty, esto se verifica dentro del paquete stdio , por medio de una isatty(3) , el paquete stdio funciona en modo de línea de almacenamiento en búfer , lo que significa que el búfer interno que acumula la salida antes de escribirla en el terminal vacía el búfer:

  • si el búfer se llena por completo (esto no va a suceder, ya que la cadena es demasiado corta, mientras que el búfer suele tener el mejor tamaño de rendimiento o alrededor de 16 kb, este es el valor para los sistemas de archivos ufs2 en BSD Unix), o. ..
  • si la salida contiene un separador de línea \n (esto solo ocurre en el código principal, consulte a continuación), el vaciado se produce en la posición de \n .

Como su código padre (el que recibió la identificación del proceso pid_t del hijo) es el que ejecuta printf(3) con el carácter \n incluido, su búfer se vacía en el momento de la ejecución de la llamada printf() , mientras que el búfer del hijo se vaciará en el momento de la llamada al sistema exit(3) , como parte del atexit(3) . Puede probar esto llamando a _exit(2) (la versión exit(3) que no llama a los controladores de salida) tanto en el padre como en el hijo, y verá que solo se ve la salida principal en el pantalla.

Hay, como usted dice, una condición de carrera, por lo que en caso de que el hijo se ejecute hasta el final, antes de que el padre haya tenido tiempo de ejecutar su printf(3) , entonces puede obtener la salida del padre al final (solo ponga un sleep(3) llame al código principal, antes de printf(3) , y verá el orden correcto. Lo más importante es que el primer proceso que inicie su llamada al sistema write(2) será el ganador (porque el inodo está bloqueado durante la ejecución del sistema write(2) , y la salida está secuenciada). Pero el proceso principal solo ejecuta su código sin ninguna llamada al sistema en el medio, mientras que la secuencia para el proceso secundario es almacenar la cadena en el búfer y vaciarlo cuando se llama a la lista de atexit(3) después de regresar de main() Esto puede implicar varias llamadas al sistema mientras tanto, que incluso pueden bloquear el proceso por un tiempo.

También puede poner un \n en el código secundario y es probable que pueda ver que el proceso secundario se programa y comienza el write() antes que el principal, aunque todavía es probable que el principal siga ganando porque es muy posible que se programe antes de que se permita que comience el hijo (esto se debe a que el padre que inicia la fork(2) ejecuta solo la primera parte, por ejemplo, verifica los permisos para crear un hijo y crea la nueva entrada de la tabla de procesos que le da el el número de identificación del niño necesario para regresar de la bifurcación, lo que permite que la fork(2) regrese tan pronto como se conozca la identificación del proceso secundario, mientras que la asignación de segmentos de memoria al nuevo proceso y prepararlo para ejecutar se realiza en la fork() del niño fork() segunda mitad. Esto significa que lo más probable es que el hijo regrese de la llamada fork() cuando el padre ya se está ejecutando a máxima velocidad a la llamada printf() . Pero no puede controlar esto.

over 4 years ago · Santiago Trujillo Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda