En informática , particularmente en el contexto del sistema operativo Unix y sus similares , fork es una operación mediante la cual un proceso crea una copia de sí mismo. Es una interfaz que se requiere para cumplir con los estándares POSIX y Single UNIX Specification . Por lo general, se implementa como un contenedor de biblioteca estándar de C para fork, clone u otras llamadas del sistema del núcleo . Fork es el método principal de creación de procesos en sistemas operativos similares a Unix.
En los sistemas operativos multitarea, los procesos (programas en ejecución) necesitan una forma de crear nuevos procesos, por ejemplo, para ejecutar otros programas. Fork y sus variantes son normalmente la única forma de hacerlo en sistemas tipo Unix. Para que un proceso inicie la ejecución de un programa diferente, primero se bifurca para crear una copia de sí mismo. Luego, la copia, llamada " proceso hijo ", realiza la llamada al sistema exec para superponerse con el otro programa: detiene la ejecución de su programa anterior a favor del otro.
La operación fork crea un espacio de direcciones independiente para el proceso hijo. El proceso hijo tiene una copia exacta de todos los segmentos de memoria del proceso padre. En las variantes modernas de UNIX que siguen el modelo de memoria virtual de SunOS -4.0, se implementa la semántica de copia en escritura y no es necesario copiar realmente la memoria física. En cambio, las páginas de memoria virtual en ambos procesos pueden hacer referencia a las mismas páginas de memoria física hasta que uno de ellos escriba en dicha página: entonces se copia. Esta optimización es importante en el caso común en el que fork se utiliza junto con exec para ejecutar un nuevo programa: normalmente, el proceso hijo realiza solo un pequeño conjunto de acciones antes de que cese la ejecución de su programa a favor del programa que se va a iniciar, y requiere muy pocas, si es que requiere alguna, de las estructuras de datos de su padre .
Cuando un proceso llama a fork, se lo considera el proceso padre y el proceso recién creado es su hijo. Después de la bifurcación, ambos procesos no solo ejecutan el mismo programa, sino que reanudan la ejecución como si ambos hubieran llamado a la llamada del sistema. Luego pueden inspeccionar el valor de retorno de la llamada para determinar su estado, hijo o padre, y actuar en consecuencia.
Una de las primeras referencias al concepto de fork apareció en A Multiprocessor System Design de Melvin Conway , publicado en 1962. [1] El artículo de Conway motivó la implementación por parte de L. Peter Deutsch de fork en el sistema de tiempo compartido GENIE , donde el concepto fue tomado prestado por Ken Thompson para su primera aparición [2] en Research Unix . [3] [4] Fork luego se convirtió en una interfaz estándar en POSIX . [5]
El proceso hijo comienza con una copia de los descriptores de archivos de su padre . [5] Para la comunicación entre procesos, el proceso padre a menudo creará una o varias tuberías y luego, después de bifurcarse, los procesos cerrarán los extremos de las tuberías que no necesitan. [6]
Vfork es una variante de fork con la misma convención de llamada y la misma semántica, pero que solo se utiliza en situaciones restringidas. Se originó en la versión 3BSD de Unix, [7] [8] [9] el primer Unix que admitió memoria virtual. Fue estandarizado por POSIX, lo que permitió que vfork tuviera exactamente el mismo comportamiento que fork, pero se marcó como obsoleto en la edición de 2004 [10] y fue reemplazado por posix_spawn () (que generalmente se implementa a través de vfork) en ediciones posteriores.
Cuando se emite una llamada al sistema vfork, el proceso padre se suspenderá hasta que el proceso hijo haya completado la ejecución o haya sido reemplazado por una nueva imagen ejecutable a través de una de las llamadas al sistema de la familia " exec ". El hijo toma prestada la configuración de la unidad de gestión de memoria del padre y las páginas de memoria se comparten entre el proceso padre y el hijo sin realizar ninguna copia y, en particular, sin semántica de copia en escritura ; [10] por lo tanto, si el proceso hijo realiza una modificación en cualquiera de las páginas compartidas, no se creará ninguna página nueva y las páginas modificadas también son visibles para el proceso padre. Dado que no hay absolutamente ninguna copia de página involucrada (consumiendo memoria adicional), esta técnica es una optimización sobre fork simple en entornos de copia completa cuando se usa con exec. En POSIX, usar vfork para cualquier propósito excepto como preludio a una llamada inmediata a una función de la familia exec (y algunas otras operaciones seleccionadas) da lugar a un comportamiento indefinido . [10] Al igual que con vfork, el hijo toma prestadas las estructuras de datos en lugar de copiarlas. vfork sigue siendo más rápido que una bifurcación que utiliza semántica de copia en escritura.
El Sistema V no admitía esta llamada de función antes de que se introdujera el Sistema VR4, [ cita requerida ] porque el uso compartido de memoria que provoca es propenso a errores:
Vfork no copia tablas de páginas, por lo que es más rápido que la implementación de la bifurcación de System V. Pero el proceso hijo se ejecuta en el mismo espacio de direcciones físicas que el proceso padre (hasta que se ejecuta un exec o exit ) y, por lo tanto, puede sobrescribir los datos y la pila del proceso padre. Podría surgir una situación peligrosa si un programador utiliza vfork de forma incorrecta, por lo que la responsabilidad de llamar a vfork recae en el programador. La diferencia entre el enfoque de System V y el enfoque de BSD es filosófica: ¿debería el núcleo ocultar las idiosincrasias de su implementación a los usuarios, o debería permitir a los usuarios sofisticados la oportunidad de aprovechar la implementación para realizar una función lógica de forma más eficiente?
—Maurice J. Bach [11]
De manera similar, la página del manual de Linux para vfork desaconseja enfáticamente su uso: [7] [ verificación fallida ] [ discusión ]
Es bastante desafortunado que Linux haya revivido este espectro del pasado. La página del manual de BSD afirma: "Esta llamada al sistema se eliminará cuando se implementen los mecanismos adecuados de compartición del sistema. Los usuarios no deberían depender de la semántica de compartición de memoria de vfork() ya que, en ese caso, se convertirá en sinónimo de fork(2)".
Otros problemas con vfork incluyen bloqueos que pueden ocurrir en programas multihilo debido a interacciones con enlaces dinámicos . [12] Como reemplazo de la interfaz vfork , POSIX introdujo la familia de funciones posix_spawn que combinan las acciones de fork y exec. Estas funciones pueden implementarse como rutinas de biblioteca en términos de fork , como se hace en Linux, [12] o en términos de vfork para un mejor rendimiento, como se hace en Solaris, [12] [13] pero la especificación POSIX señala que fueron "diseñadas como operaciones de núcleo ", especialmente para sistemas operativos que se ejecutan en hardware restringido y sistemas de tiempo real . [14]
Si bien la implementación de 4.4BSD eliminó la implementación de vfork, lo que provocó que vfork tuviera el mismo comportamiento que fork, luego se restableció en el sistema operativo NetBSD por razones de rendimiento. [8]
Algunos sistemas operativos integrados como uClinux omiten fork y solo implementan vfork, porque necesitan operar en dispositivos donde la copia en escritura es imposible de implementar debido a la falta de una unidad de administración de memoria.
El sistema operativo Plan 9 , creado por los diseñadores de Unix, incluye fork pero también una variante llamada "rfork" que permite compartir recursos de forma detallada entre procesos padre e hijos, incluyendo el espacio de direcciones (excepto un segmento de pila , que es único para cada proceso), variables de entorno y el espacio de nombres del sistema de archivos; [15] esto lo convierte en una interfaz unificada para la creación tanto de procesos como de subprocesos dentro de ellos. [16] Tanto FreeBSD [17] como IRIX adoptaron la llamada al sistema rfork de Plan 9, este último renombrándolo como "sproc". [18]
clone
es una llamada al sistema en el núcleo de Linux que crea un proceso hijo que puede compartir partes de su contexto de ejecución con el padre. Al igual que rfork de FreeBSD y sproc de IRIX, el clon de Linux se inspiró en rfork de Plan 9 y se puede utilizar para implementar subprocesos (aunque los programadores de aplicaciones normalmente utilizarán una interfaz de nivel superior como pthreads , implementada sobre el clon). La característica de "pilas separadas" de Plan 9 e IRIX se ha omitido porque (según Linus Torvalds ) causa demasiada sobrecarga. [18]
En el diseño original del sistema operativo VMS (1977), una operación de copia con posterior mutación del contenido de unas pocas direcciones específicas para el nuevo proceso, como en una bifurcación, se consideraba arriesgada. [ cita requerida ] Los errores en el estado actual del proceso pueden copiarse a un proceso hijo. Aquí se utiliza la metáfora de la generación de procesos: cada componente de la disposición de memoria del nuevo proceso se construye desde cero. La metáfora de la generación se adoptó posteriormente en los sistemas operativos de Microsoft (1993).
El componente de compatibilidad con POSIX de VM/CMS (OpenExtensions) proporciona una implementación muy limitada de fork, en la que el padre se suspende mientras el hijo se ejecuta, y el hijo y el padre comparten el mismo espacio de direcciones. [19] Esto es esencialmente un vfork etiquetado como fork . (Esto se aplica solo al sistema operativo invitado CMS; otros sistemas operativos invitados de VM, como Linux, proporcionan una funcionalidad de fork estándar).
La siguiente variante del programa "¡Hola, mundo!" demuestra la mecánica de la llamada al sistema fork en el lenguaje de programación C. El programa se bifurca en dos procesos, cada uno de los cuales decide qué funcionalidad realizar en función del valor de retorno de la llamada al sistema fork. Se ha omitido el código repetitivo, como las inclusiones de encabezados .
int principal ( vacío ) { pid_t pid = bifurcación (); if ( pid == -1 ) { perror ( "fork falló" ); exit ( EXIT_FAILURE ); } else if ( pid == 0 ) { printf ( "Hola desde el proceso hijo! \n " ); _exit ( EXIT_SUCCESS ); } else { int status ; ( void ) waitpid ( pid , & status , 0 ); } return EXIT_SUCCESS ; }
Lo que sigue es una disección de este programa.
pid_t pid = bifurcación ();
La primera instrucción de main invoca la llamada al sistema fork para dividir la ejecución en dos procesos. El valor de retorno de fork se registra en una variable de tipo pid_t , que es el tipo POSIX para identificadores de proceso (PID).
si ( pid == -1 ) { perror ( "fork falló" ); salir ( EXIT_FAILURE ); }
Menos uno indica un error en fork : no se creó ningún proceso nuevo, por lo que se imprime un mensaje de error.
Si fork se ejecutó correctamente, entonces ahora hay dos procesos, ambos ejecutando la función principal desde el punto al que fork regresó. Para que los procesos realicen tareas diferentes, el programa debe ramificarse en el valor de retorno de fork para determinar si se está ejecutando como proceso secundario o como proceso primario .
de lo contrario si ( pid == 0 ) { printf ( "Hola desde el proceso hijo! \n " ); _exit ( EXIT_SUCCESS ); }
En el proceso secundario, el valor de retorno aparece como cero (que es un identificador de proceso no válido). El proceso secundario imprime el mensaje de saludo deseado y luego sale. (Por razones técnicas, aquí se debe utilizar la función POSIX _exit en lugar de la función de salida estándar de C ).
de lo contrario { int estado ; ( void ) waitpid ( pid , & estado , 0 ); }
El otro proceso, el padre, recibe de fork el identificador de proceso del hijo, que siempre es un número positivo. El proceso padre pasa este identificador a la llamada al sistema waitpid para suspender la ejecución hasta que el hijo haya salido. Cuando esto ha sucedido, el padre reanuda la ejecución y sale mediante la sentencia return .