tag kid3 easytag linux process fork

linux - kid3 - Por qué fork() funciona de la forma en que lo hace



easytag (14)

"fork ()" fue una innovación brillante que resolvió toda una clase de problemas con una sola API. Se inventó en un momento en que el multiprocesamiento NO era común (y precedió el tipo de multiprocesamiento que usted y yo usamos hoy por cerca de veinte años).

Entonces, he usado fork() y sé lo que hace. Como principiante, le tenía bastante miedo (y todavía no lo entiendo del todo). La descripción general de fork() que puede encontrar en línea es que copia el proceso actual y asigna diferentes PID, PID padre y el proceso tendrá un espacio de direcciones diferente. Todo está bien, sin embargo, dada esta descripción de la funcionalidad, un principiante se preguntaría "¿Por qué es esta función tan importante ... por qué querría copiar mi proceso?". Así que me pregunté y finalmente descubrí que es así como puedes llamar a otros procesos desde tu proceso actual a través de la familia execve() .

Lo que todavía no entiendo es por qué tienes que hacer eso de esta manera? Lo más lógico sería tener una función a la que puedas llamar como

create_process("executable_path+name",params..., more params);

que generaría un nuevo proceso y comenzaría a funcionar al principio de main () y devolvería el nuevo PID.

Lo que me molesta es la sensación de que la solución fork / execve está haciendo un trabajo potencialmente innecesario. ¿Qué sucede si mi proceso usa toneladas de memoria? ¿El kernel copia mis tablas de páginas y demás? Estoy seguro de que realmente no asigna memoria real a menos que lo haya tocado. Además, ¿qué sucede si tengo hilos? Me parece que es demasiado complicado.

Casi toda la descripción de lo que hace el tenedor, digamos que simplemente copia el proceso y el nuevo proceso comienza a ejecutarse después de la llamada del fork() . De hecho, esto es lo que sucede, pero ¿por qué ocurre de esta manera y por qué es fork / execve la única forma de engendrar nuevos procesos y cuál es la forma más general de Unix de crear un nuevo proceso a partir del actual? ¿Hay alguna otra manera más efectiva de generar el proceso? ** Lo cual no requeriría copiar más memoria.

This hilo habla sobre el mismo problema, pero encontré que no es del todo satisfactorio:

Gracias.


Bueno, en términos de paginación / memoria virtual, hay técnicas en las que fork () no siempre copia todo el espacio de direcciones de un proceso. Hay una copia en escritura en la que un proceso bifurcado obtiene el mismo espacio de direcciones que su elemento primario y luego solo copia una parte del espacio que se cambia (por cualquiera de los procesos).


Cuando fork crea un nuevo proceso copiando el proceso actual, realiza una copia por escritura. Esto significa que la memoria del nuevo proceso se comparte con el proceso principal hasta que se modifique. Cuando se cambia la memoria, la memoria se copia para asegurarse de que cada proceso tenga su propia copia válida de la memoria. Al hacer una execve después de la fork , no hay copia de la memoria, ya que el nuevo proceso simplemente carga un nuevo ejecutable y, por lo tanto, un nuevo espacio de memoria.

En cuanto a la pregunta de por qué se hace esto, no estoy seguro, pero parece ser parte de la ruta de Unix: hacer una cosa bien. En lugar de crear una función que crea un nuevo proceso y carga un nuevo ejecutable, la operación se divide en dos funciones. Esto le da al desarrollador la máxima flexibilidad. Aunque todavía no he usado ninguna función por sí mismo ...



Entonces, como dijeron los demás, fork se implementa para ser muy rápido, así que no hay problema. ¿Pero por qué no una función como create_process() ? La respuesta es: simplicidad para la flexibilidad. Todas las llamadas al sistema en Unix están programadas para hacer solo una cosa. Una función como create_process haría dos cosas: crear un proceso y cargar un binario en eso.

Siempre que intentes paralelizar cosas, puedes usar hilos o procesos abiertos con fork() . En la mayoría de los casos, usted abre n procesos a través de fork() y luego utiliza un mecanismo de IPC para comunicarse y sincronizar entre estos procesos. Algunos IPC insisten en tener variables en el espacio global.

Ejemplo con tubos:

  • Creando el tubo
  • Tenedor de un niño que hereda el mango de la tubería
  • El niño cierra el lado de entrada
  • El padre cierra el lado de salida

Imposible sin fork() ...

Otro hecho importante es que toda la API de Unix tiene solo unas pocas funciones. Cada programador podría recordar fácilmente en funciones usadas. Pero vea la API de Windows: más de miles de funciones que nadie puede recordar.

Para resumir y decirlo de nuevo: simplicidad para la flexibilidad


Es posible que fork () se implemente con muy poca asignación de memoria, suponiendo que la implementación subyacente utiliza un sistema de direccionamiento de escritura por copia. Es imposible que una función create_process se implemente con esa optimización.


Esta es una gran pregunta. Tuve que cavar un poco en la fuente para ver exactamente lo que estaba sucediendo.

fork () crea un nuevo proceso al duplicar el proceso de llamada.

En Linux, fork () se implementa utilizando páginas de copia sobre escritura, por lo que la única penalización en la que incurre es el tiempo y la memoria necesarios para duplicar las tablas de página de los padres y crear una estructura de tareas única para el niño.

El nuevo proceso, denominado hijo, es un duplicado exacto del proceso de llamada (al que se hace referencia como padre). Excepto por:

  • El niño tiene su propio ID de proceso único, y este PID no coincide con el ID de ningún grupo de proceso existente.
  • El ID del proceso del padre del niño es el mismo que el ID del proceso del padre.
  • El niño no hereda los bloqueos de memoria de sus padres.
  • Las utilizaciones de recursos de proceso y los contadores de tiempo de CPU se restablecen a cero en el elemento secundario.
  • El conjunto de señales pendientes del niño está inicialmente vacío.
  • El niño no hereda los ajustes del semáforo de su padre.
  • El niño no hereda los bloqueos de registros de su padre.
  • El niño no hereda los temporizadores de su padre.
  • El elemento secundario no hereda las operaciones de E / S asíncronas sobresalientes de su elemento primario, ni hereda ningún contexto de E / S asíncronas de su elemento primario.

Conclusión

El objetivo principal de fork es dividir las tareas del proceso de los padres en subtareas más pequeñas sin afectar la estructura de tarea única del padre. Es por eso que fork clona el proceso existente.

Fuentes:

http://www.quora.com/Linux-Kernel/After-a-fork-where-exactly-does-the-childs-execution-start http://learnlinuxconcepts.blogspot.in/2014/03/process-management.html


Esto se debe a razones históricas. Como se explica en https://www.bell-labs.com/usr/dmr/www/hist.html , Unix muy antiguo no tenía ni fork() ni exec*() , y la forma en que el shell ejecutaba los comandos era:

  • Haga la inicialización necesaria (apertura stdin / stdout ).
  • Lee una línea de comando.
  • Abra el comando, cargue un código de arranque y salte a él.
  • El código de arranque leyó el comando abierto (sobrescribiendo la memoria del shell) y saltó a él.
  • Una vez que el comando finalizaba, se llamaba a exit() , que luego funcionaba volviendo a cargar el shell (sobrescribiendo la memoria del comando), y saltando hacia él, volviendo al paso 1.

A partir de ahí, fork() fue una adición fácil (27 líneas de ensamblaje), reutilizando el resto del código.

En esa etapa del desarrollo de Unix, la ejecución de un comando se convirtió en:

  • Lee una línea de comando.
  • fork() un proceso hijo, y espéralo (enviándole un mensaje).
  • El proceso hijo cargó el comando (sobrescribiendo la memoria del niño) y saltó a él.
  • Una vez que el comando terminaba, llamaría a exit() , que ahora era más simple. Simplemente limpió su entrada al proceso y dejó el control.

Originalmente, fork() no copió en escritura. Como esto hacía que fork() caro, y fork() se utilizaba a menudo para engendrar nuevos procesos (a menudo seguían inmediatamente a exec*() ), apareció una versión optimizada de fork() : vfork() que compartía la memoria entre padres y niño En esas implementaciones de vfork() el padre se suspendería hasta que el niño exec*() ''ed o _exit() '' ed, renunciando así a la memoria del padre. Más tarde, fork() se optimizó para copiar en escritura, haciendo copias de páginas de memoria solo cuando comenzaron a diferir entre padre e hijo. vfork() más tarde vio renovado interés en los puertos a los sistemas! MMU (por ejemplo: si tiene un enrutador ADSL, probablemente ejecuta Linux en una! MMU MIPS CPU), lo que no podría hacer la optimización COW, y además no podría soportar el fork() ''ed procesos de manera eficiente.

Otra fuente de ineficiencias en fork() es que inicialmente duplica el espacio de direcciones (y tablas de páginas) del padre, lo que puede hacer que ejecutar programas cortos de grandes programas sea relativamente lento, o puede hacer que el sistema operativo niegue un fork() pensando que puede no hay suficiente memoria para ello (para solucionar este problema, podría aumentar el espacio de intercambio o cambiar la configuración de sobrecarga de la memoria del sistema operativo). Como anécdota, Java 7 usa vfork()/posix_spawn() para evitar estos problemas.

Por otro lado, fork() hace que la creación de varias instancias de un mismo proceso sea muy eficiente: por ejemplo, un servidor web puede tener varios procesos idénticos que prestan servicios a clientes diferentes. Otras plataformas favorecen los hilos, porque el costo de generar un proceso diferente es mucho mayor que el costo de duplicar el proceso actual, que puede ser un poco más grande que el de generar un nuevo hilo. Lo cual es desafortunado, ya que los hilos compartidos son un imán para los errores.


Históricamente, Unix se estaba ejecutando en sistemas bastante pequeños que no permitían la ejecución de más de un proceso en la RAM (todos corrieron en el mismo espacio de direcciones, sin MMU). fork simplemente cambiaba el proceso actual al disco (u otro almacenamiento secundario) sin molestarse en cambiar un proceso diferente. Puede continuar ejecutando la copia en memoria o usar exec para cargar y continuar con un ejecutable diferente.

La gente se acostumbró a poder configurar un nuevo ambiente de trabajo (abrir descriptores de archivos, tuberías y demás) antes de llamar a exec , para que se fork .


La razón principal para usar la horquilla es la velocidad de ejecución.

Si, como sugirió, comenzó una nueva copia del proceso con un conjunto de parámetros, el nuevo proceso necesitaría analizar esos parámetros y repetir la mayor parte del proceso que el proceso principal había realizado. Con "fork ()" la copia completa de la pila de procesos padres está disponible para el niño inmediatamente, con todo analizado y formateado como debería ser.

Además, en la mayoría de los casos, el programa será un ".so" o ".dll", por lo que las instrucciones ejecutables no se copiarán, solo se copiará la pila y el almacenamiento en heap.


Las otras respuestas han hecho un buen trabajo al explicar por qué fork es más rápido de lo que parece, y cómo llegó a existir originalmente. Pero también hay un argumento sólido para mantener el combo fork + exec , y esa es la flexibilidad que ofrece.

A menudo, cuando se genera un proceso secundario, se deben tomar pasos preparatorios antes de ejecutar al niño. Por ejemplo: puede crear un par de tuberías usando un pipe (un lector y un escritor), luego redirigir el proceso stdin o el proceso al escritor, o usar el lector como el archivo del proceso o cualquier otro descriptor de archivo, para el caso . O bien, es posible que desee establecer variables de entorno (pero solo en el elemento secundario). O establezca límites de recursos con el límite de setrlimit para restringir la cantidad de recursos que el niño podría usar (sin limitar al padre). O cambie los usuarios con setuid / seteuid (sin cambiar el elemento principal). Etcétera etcétera.

Claro, puedes hacer todo esto con una hipotética función create_process . ¡Pero eso es un montón de cosas para cubrir! ¿Por qué no ofrecer la flexibilidad de ejecutar fork , hacer lo que quieras para configurar al niño, y luego ejecutar el exec ?

Además, a veces no necesitas un proceso secundario en absoluto. Si su programa actual (o secuencia de comandos) existe únicamente para hacer algunos de esos pasos de configuración, y lo último que se va a hacer es ejecutar el nuevo proceso, entonces ¿por qué tener dos procesos en absoluto? Puede usar exec para simplemente reemplazar el proceso actual, liberando su propia memoria y PID.

La bifurcación también permite algún comportamiento útil con respecto a los conjuntos de datos de solo lectura. Por ejemplo, podría tener un proceso primario que recopile e indexe una gran cantidad de datos, luego se desvía de los niños trabajadores para realizar recorridos y cálculos basados ​​en esos datos. El padre no necesita guardarlo en ningún lado, los niños no necesitan leerlo y no es necesario que realice ningún trabajo complejo con la memoria compartida. (Como ejemplo: algunas bases de datos usan esto como un medio para hacer que un proceso hijo descargue la base de datos en memoria al disco, sin bloquear el proceso principal).

Lo anterior también incluye cualquier programa que lea una configuración, una base de datos y / o un conjunto de archivos de código, luego procede a desviar los procesos secundarios para manejar las solicitudes y hacer un mejor uso de las CPU multinúcleo. Esto incluye servidores web, pero también aplicaciones web (u otras), en particular si esas aplicaciones pasan una cantidad significativa de tiempo de inicio solo leyendo y / o compilando un código de nivel superior.

La bifurcación también puede ser una forma útil de administrar la memoria y evitar la fragmentación, especialmente para los lenguajes de nivel superior que utilizan administración automática de memoria (recolección de basura) y no tienen control directo sobre el diseño de la memoria. Si su proceso necesita brevemente una gran cantidad de memoria para una operación en particular, puede bifurcar y realizar esa operación, luego salir, liberando toda la memoria que acaba de asignar. Por el contrario, si realizó la operación en el elemento principal, es posible que tenga una fragmentación de memoria significativa que podría persistir durante el proceso, lo que no es ideal para un proceso de larga ejecución.

Y finalmente: una vez que acepta que el fork y el exec tienen sus propios usos, independientemente el uno del otro, la pregunta es: ¿por qué molestarse en crear una función separada que combine los dos? Se ha dicho que la filosofía de Unix era tener sus herramientas "hacer una cosa y hacerlo bien". Al ofrecerle fork y exec como componentes independientes, y al hacer que cada uno sea lo más rápido y eficiente posible, permite una flexibilidad mucho mayor que la que create_process una sola función create_process .


Por lo tanto, su principal preocupación es: fork () conduce a una copia de memoria innecesaria.

La respuesta es: no, no hay desperdicio de memoria. En resumen, fork () nació cuando la memoria era un recurso muy limitado, por lo que nadie pensaría en desperdiciarlo así.

Aunque cada proceso tiene su propio espacio de direcciones, no hay una correspondencia de uno a uno entre la página de memoria física y la página de proceso de memoria virtual. En su lugar, una página de memoria física se puede asignar a varias páginas virtuales (busque CPU TLB para obtener más detalles).

Por lo tanto, cuando crea un nuevo proceso con fork (), sus espacios de direcciones virtuales se asignan a las mismas páginas de memoria física. No se requiere copia de memoria. También significa que no hay duplicados de bibliotecas usadas porque sus secciones de código están marcadas como de solo lectura.

La copia de la memoria real se produce solo cuando el proceso primario o secundario modifica alguna página de memoria. En ese caso, la nueva página de memoria física se asigna y se asigna al espacio de direcciones virtuales del proceso que modificó la página.


Puede pensar que esto es similar a engendrar un hilo en Windows, excepto que los procesos no comparten recursos, excepto los identificadores de archivos, la memoria compartida y otros elementos que son heredables explícitamente. Entonces, si tiene una tarea nueva que hacer, puede bifurcar y un proceso continúa en su trabajo original mientras el clon se ocupa de la nueva asignación.

Si desea hacer cálculos en paralelo, sus procesos pueden dividirse en múltiples clones justo encima del bucle. Cada uno de los clones hace un subconjunto del cálculo mientras el padre espera que se complete. Los sistemas operativos se aseguran de que puedan ejecutarse en paralelo. En Windows, por ejemplo, necesitaría usar OpenMP para obtener la misma capacidad de expresión.

Si necesita leer o escribir desde un archivo pero no puede esperar, puede simplemente hacer un fork y su clon hará la E / S mientras continúa con su tarea original. En Windows, puede considerar el desarrollo de hilos o el uso de E / S superpuestas en muchas situaciones en las que un tenedor simple funcionará en Unix. En particular, los procesos no tienen los mismos problemas de escalabilidad que los hilos. Esto es particularmente cierto en sistemas de 32 bits. Simplemente bifurcar es mucho más conveniente que tener que lidiar con las complejidades de las E / S superpuestas. Si bien los procesos tienen su propio espacio de memoria, los hilos viven en el mismo y, por lo tanto, existe un límite en la cantidad de hilos que debe considerar para poner en un proceso de 32 bits. Hacer una aplicación de servidor de 32 bits con fork es muy simple, mientras que crear una aplicación de servidor de 32 bits con hilos puede ser una pesadilla. Y si estuvieras programando en Windows de 32 bits, tendrías que recurrir a otras soluciones como E / S superpuestas, que es un PITA con el que trabajar.

Debido a que los procesos no comparten recursos globales como los hilos a (por ejemplo, un bloqueo global en malloc), esto es mucho más escalable. Mientras que los hilos a menudo se bloquean entre sí, los procesos se ejecutan de forma independiente.

En Unix porque fork hace un clon de copia de escritura de su proceso, no es más pesado que generar un nuevo hilo en Windows.

Si trabajas con lenguajes interpretados, donde normalmente hay un bloqueo de intérprete global (Python, Ruby, PHP ...), es indispensable un SO que te permita bifurcar. De lo contrario, su capacidad para explotar múltiples procesadores es mucho más limitada.

Otra cosa es que hay un isse de seguridad aquí. Los procesos no comparten espacio de memoria y no pueden estropear los detalles internos de los demás. Esto conduce a una mayor estabilidad. Si tiene un servidor que utiliza subprocesos, un bloqueo en un subproceso eliminará toda la aplicación del servidor. Al bifurcar un choque, solo se derribará el clon bifurcado. Esto también hace que el manejo de errores sea más simple. A menudo es suficiente permitir que el clon bifurcado se cancele, ya que no tiene importancia para la aplicación original.

También hay un problema de seguridad. Si un proceso bifurcado se inyecta con código malicioso, no puede afectar aún más al padre. Los navegadores web modernos hacen uso de esto, por ejemplo, para proteger una pestaña de otra. Todo esto es mucho más conveniente para programar si tiene una llamada del sistema de bifurcación.


Recuerde que la fork se inventó muy temprano en Unix (y tal vez antes) en máquinas que hoy parece ridículamente pequeña (por ejemplo, 64 KB de memoria).

Y está más en línea con la filosofía general (original) de proporcionar mecanismos básicos, no políticas, a través de las acciones más elementales posibles.

fork solo crea un nuevo proceso, y la manera más simple de pensar es clonar el proceso actual. Entonces la semántica de la fork es muy natural, y es el mecanismo más simple posible.

Otras llamadas al sistema ( execve ) se encargan de cargar un nuevo archivo ejecutable, etc.

dup2 (y proporcionar también dup2 pipe y dup2) brinda mucha flexibilidad.

Y en los sistemas actuales, fork se implementa de manera muy eficiente (a través de una copia diferida en técnicas de paginación de escritura). Se sabe que el mecanismo de fork hace que la creación de procesos de Unix sea bastante rápida (por ejemplo, más rápida que en Windows o en VAX / VMS, que tienen llamadas al sistema creando procesos más similares a los que propone).

También está el vfork syscall, que no me molesto en usar.

Y la API posix_spawn es mucho más compleja que fork o execve solo, así que ilustra que fork es más simple ...