linux unix operating-system multiprocessing fork

linux - Tiempo perdido de execv() y fork()



unix operating-system (5)

¿Cuál es la ventaja que se logra al usar este combo (en lugar de alguna otra solución) que hace que las personas sigan usando esto aunque tengamos desperdicio?

Tienes que crear un nuevo proceso de alguna manera. Hay muy pocas formas en que un programa de espacio de usuario puede lograr eso. POSIX solía tener vfork() alognside fork() , y algunos sistemas pueden tener sus propios mecanismos, como clone() específico de Linux clone() , pero desde 2008, POSIX especifica solo fork() y la familia posix_spawn() . La ruta fork + exec es más tradicional, se entiende bien y tiene pocos inconvenientes (ver más abajo). La familia posix_spawn está diseñada como un sustituto de uso especial para el uso en contextos que presentan dificultades para fork() ; Puede encontrar detalles en la sección "Justificación" de su especificación .

Este extracto de la página man de Linux para vfork() puede ser esclarecedor:

En Linux, fork (2) se implementa utilizando páginas copy-on-write, por lo que la única penalización incurrida por fork (2) es el tiempo y la memoria necesarios para duplicar las tablas de páginas principales y crear una estructura de tareas única para el niño . Sin embargo, en los malos viejos tiempos un fork (2) requeriría hacer una copia completa del espacio de datos de la persona que llama, a menudo innecesariamente, ya que generalmente inmediatamente después se hace un exec (3). Por lo tanto, para una mayor eficiencia, BSD introdujo la llamada al sistema vfork () que no copió por completo el espacio de direcciones del proceso principal, sino que tomó prestada la memoria del padre y el hilo de control hasta que se produjo una llamada a execve (2). El proceso principal se suspendió mientras el niño usaba sus recursos. El uso de vfork () fue complicado: por ejemplo, no modificar los datos en el proceso principal depende de saber qué variables se mantienen en un registro.

(Énfasis añadido)

Por lo tanto, su preocupación por el desperdicio no está bien fundada para los sistemas modernos (no se limita a Linux), pero de hecho fue un problema histórico, y de hecho hubo mecanismos diseñados para evitarlo. En estos días, la mayoría de esos mecanismos están obsoletos.

Actualmente estoy aprendiendo sobre fork() y execv() y tenía una pregunta sobre la eficiencia de la combinación.

Me mostraron el siguiente código estándar:

pid = fork(); if(pid < 0){ //handle fork error } else if (pid == 0){ execv("son_prog", argv_son); //do father code

Sé que fork() clona todo el proceso (copiando todo el montón, etc.) y que execv() reemplaza el espacio de direcciones actual con el del nuevo programa. Con esto en mente, ¿no es muy ineficiente usar esta combinación? Copiamos todo el espacio de direcciones de un proceso y luego lo sobreescribimos inmediatamente.

Entonces mi pregunta:
¿Cuál es la ventaja que se logra al usar este combo (en lugar de alguna otra solución) que hace que la gente todavía use esto, a pesar de que tenemos desperdicio?


Otra respuesta dice:

Sin embargo, en los malos viejos tiempos un tenedor (2) requeriría hacer una copia completa del espacio de datos de la persona que llama, a menudo innecesariamente, ya que generalmente inmediatamente después se hace un ejecutivo (3).

Obviamente, los viejos tiempos malos de una persona son mucho más jóvenes que los que otros recuerdan.

Los sistemas UNIX originales no tenían la memoria para ejecutar múltiples procesos y no tenían una MMU para mantener varios procesos en la memoria física listos para ejecutarse en el mismo espacio de direcciones lógicas: intercambiaban procesos en el disco que no era actualmente en ejecución.

La llamada al sistema de la horquilla fue casi la misma que la de cambiar el proceso actual al disco, excepto por el valor de retorno y por no reemplazar la copia restante en la memoria mediante el intercambio en otro proceso. Como tenía que cambiar el proceso principal de todos modos para ejecutar el elemento secundario, fork + exec no incurría en gastos generales.

Es cierto que hubo un período de tiempo en el que fork + exec era incómodo: cuando había MMU que proporcionaban una asignación entre el espacio de direcciones lógico y físico, pero las fallas de las páginas no conservaban suficiente información como copy-on-write y varias otras virtuales -los esquemas de memoria / demanda-paginación eran factibles.

Esta situación fue lo suficientemente dolorosa, no solo para UNIX, que el manejo de fallas de página del hardware se adaptó para volverse "reutilizable" bastante rápido.


Resulta que todas esas fallas de página COW no son nada baratas cuando el proceso tiene unos pocos gigabytes de RAM grabable. Todos van a fallar una vez, incluso si el niño hace mucho tiempo que ha llamado a exec (). Debido a que el elemento secundario de fork () ya no puede asignar memoria incluso para el caso de una sola hebra (puede agradecer a Apple por esa), organizar llamar a vfork () / exec () en cambio ahora no es más difícil.

La ventaja real del modelo vfork () / exec () es que puede configurar el elemento secundario con un directorio actual arbitrario, variables de entorno arbitrarias y manejadores fs arbitrarios (no solo stdin / stdout / stderr), una máscara de señal arbitraria y una memoria compartida arbitraria (utilizando las llamadas de memoria compartidas) sin tener una API CreateProcess () de veinte argumentos que obtiene algunos argumentos más cada pocos años.

Resultó ser que "las fallas de las manijas filtradas se abrían por otro hilo", la metedura de pata de los primeros días del roscado se podía reparar en el espacio de usuario sin bloqueo de todo el proceso gracias a / proc. Lo mismo no estaría en el modelo gigante de CreateProcess () sin una nueva versión del sistema operativo, y convencer a todos para que llamen a la nueva API.

Entonces ahí lo tienes. Un accidente de diseño terminó mucho mejor que la solución diseñada directamente.


Un proceso creado por exec () y otros, heredará sus identificadores de archivo del proceso principal (incluidos stdin, stdout, stderr). Si el padre los cambia después de llamar a fork () pero antes de llamar a exec (), entonces puede controlar las transmisiones estándar del niño.


Ya no más. Hay algo llamado COW (Copiar al escribir), solo cuando uno de los dos procesos (principal / secundario) intenta escribir datos compartidos, se copia.

En el pasado:
La llamada al sistema fork() copió el espacio de direcciones del proceso de llamada (el principal) para crear un nuevo proceso (el secundario). La copia del espacio de direcciones de los padres en el hijo fue la parte más cara de la operación fork() .

Ahora:
Una llamada a fork() es seguido casi de inmediato por una llamada a exec() en el proceso secundario, que reemplaza la memoria del niño con un nuevo programa. Esto es lo que típicamente hace el caparazón, por ejemplo. En este caso, el tiempo dedicado a copiar el espacio de direcciones de los padres se desperdicia en gran medida, porque el proceso secundario usará muy poca memoria antes de llamar a exec() .

Por esta razón, las versiones posteriores de Unix aprovecharon el hardware de memoria virtual para permitir que el padre y el niño compartan la memoria asignada en sus respectivos espacios de direcciones hasta que uno de los procesos realmente lo modifique. Esta técnica se conoce como copy-on-write . Para hacer esto, en fork() el núcleo copiará las asignaciones de espacio de direcciones del padre al hijo en lugar de los contenidos de las páginas asignadas, y al mismo tiempo marcará las páginas ahora compartidas de solo lectura. Cuando uno de los dos procesos intenta escribir en una de estas páginas compartidas, el proceso toma un error de página. En este punto, el kernel de Unix se da cuenta de que la página era realmente una copia "virtual" o "copy-on-write", por lo que crea una nueva copia privada de la página para el proceso de fallas. De esta forma, los contenidos de las páginas individuales no se copian realmente hasta que realmente se escriben. Esta optimización hace que un fork() seguido de un exec() en el niño sea mucho más barato: el niño probablemente solo necesite copiar una página (la página actual de su pila) antes de llamar a exec() .