¿Por qué los shells llaman fork ()?

32

Cuando se inicia un proceso desde un shell, ¿por qué el shell se bifurca antes de ejecutar el proceso?

Por ejemplo, cuando el usuario ingresa grep blabla foo, ¿por qué el shell no puede invocar exec()grep sin un shell hijo?

Además, cuando un shell se bifurca dentro de un emulador de terminal GUI, ¿inicia otro emulador de terminal? (como pts/13comenzar pts/14)

usuario3122885
fuente

Respuestas:

34

Cuando llama a un execmétodo familiar, no crea un nuevo proceso, sino que execreemplaza la memoria del proceso actual y el conjunto de instrucciones, etc. con el proceso que desea ejecutar.

Como ejemplo, desea ejecutar grepusando exec. bashes un proceso (que tiene memoria separada, espacio de direcciones). Ahora, cuando llame exec(grep), exec reemplazará la memoria del proceso actual, el espacio de direcciones, el conjunto de instrucciones, etc. con grep'sdatos. Eso significa que el bashproceso ya no existirá. Como resultado, no puede volver a la terminal después de completar el grepcomando. Es por eso que los métodos familiares exec nunca regresan. No puede ejecutar ningún código después de exec; Es inalcanzable.

shantanu
fuente
Casi bien --- sustituí Terminal con bash. ;-)
Rmano
2
Por cierto, usted puede decirle a bash para ejecutar grep sin que se bifurcan en primer lugar, mediante el comando exec grep blabla foo. Por supuesto, en este caso particular, no será muy útil (ya que la ventana de su terminal se cerrará tan pronto como termine el grep), pero puede ser útil ocasionalmente (por ejemplo, si está iniciando otro shell, tal vez a través de ssh / sudo / screen, y no tengo la intención de volver al original, o si el proceso de shell en el que está ejecutando esto es un sub-shell que nunca debe ejecutar más de un comando de todos modos).
Ilmari Karonen
77
El conjunto de instrucciones tiene un significado muy específico. Y no es el significado en el que lo estás usando.
Andrew Savinykh
@IlmariKaronen Sería útil en scripts de envoltura, donde desea preparar argumentos y entorno para un comando. Y el caso que mencionó, donde bash nunca debe ejecutar más de un comando, eso es en realidad bash -c 'grep foo bar'y llamar a exec hay una forma de optimización que bash hace por usted automáticamente
Sergiy Kolodyazhnyy el
3

Según el pts, compruébelo usted mismo: en un shell, ejecute

echo $$ 

para conocer su ID de proceso (PID), tengo por ejemplo

echo $$
29296

Luego ejecuta por ejemplo sleep 60y luego, en otra terminal

(0)samsung-romano:~% ps -edao pid,ppid,tty,command | grep 29296 | grep -v grep
29296  2343 pts/11   zsh
29499 29296 pts/11   sleep 60

Entonces no, en general tiene el mismo tty asociado al proceso. (Tenga en cuenta que esto es sleepporque tiene su shell como padre).

Rmano
fuente
2

TL; DR : porque este es el método óptimo para crear nuevos procesos y mantener el control en el shell interactivo

fork () es necesario para procesos y tuberías

Para responder a la parte específica de esta pregunta, si grep blabla foose llamara exec()directamente a través de parent, parent aprovecharía para existir, y su PID con todos los recursos sería asumido por grep blabla foo.

Sin embargo, hablemos en general sobre exec()y fork(). La razón clave de este comportamiento es porque fork()/exec()es el método estándar para crear un nuevo proceso en Unix / Linux, y esto no es algo específico de bash; Este método ha estado en funcionamiento desde el principio e influenciado por este mismo método de los sistemas operativos ya existentes de la época. Parafraseando un poco la respuesta de Ricitos de Oro en una pregunta relacionada, fork()crear un proceso nuevo es más fácil ya que el núcleo tiene menos trabajo que hacer en lo que respecta a la asignación de recursos y muchas de las propiedades (como descriptores de archivos, entorno, etc.). ser heredado del proceso padre (en este caso de bash).

En segundo lugar, en lo que respecta a los shells interactivos, no puede ejecutar un comando externo sin bifurcación. Para iniciar un ejecutable que vive en el disco (por ejemplo /bin/df -h), debe llamar a una de las exec()funciones familiares, como execve(), que reemplazará al padre con el nuevo proceso, se hará cargo de su PID y los descriptores de archivo existentes, etc. Para el shell interactivo, desea que el control regrese al usuario y permita que el shell interactivo principal continúe. Por lo tanto, la mejor manera es crear un subproceso vía fork()y dejar que ese proceso se haga cargo de vía execve(). Entonces, el shell interactivo PID 1156 generaría un hijo a través fork()de PID 1157, luego llamaría execve("/bin/df",["df","-h"],&environment), lo que hace que se /bin/df -hejecute con PID 1157. Ahora el shell solo tiene que esperar a que el proceso salga y devolverle el control.

En caso de que tenga que crear una tubería entre dos o más comandos, por ejemplo df | grep, necesita una forma de crear dos descriptores de archivo (que es el final de la tubería de lectura y escritura que proviene de pipe()syscall), de alguna manera deje que dos procesos nuevos los hereden. Esto se hace bifurcando un nuevo proceso y luego copiando el extremo de escritura de la tubería a través de la dup2()llamada en su stdouttambién conocido como fd 1 (por lo tanto, si el final de escritura es fd 4, lo hacemos dup2(4,1)). Cuando ocurre el exec()engendro, dfel proceso hijo no pensará en nada stdouty le escribirá sin darse cuenta (a menos que verifique activamente) que su salida realmente se convierte en una tubería. El mismo proceso ocurre grep, excepto nosotros fork(), tomamos el extremo de lectura de la tubería con fd 3 y dup(3,0)antes de desovar grepconexec(). Todo este tiempo el proceso padre sigue ahí, esperando recuperar el control una vez que se complete la canalización.

En el caso de los comandos integrados, generalmente Shell no fork(), con la excepción del sourcecomando. Se requieren subcapas fork().

En resumen, este es un mecanismo necesario y útil.

Desventajas de bifurcación y optimizaciones

Ahora, esto es diferente para shells no interactivos , como bash -c '<simple command>'. A pesar de fork()/exec()ser un método óptimo en el que tiene que procesar muchos comandos, es un desperdicio de recursos cuando tiene un solo comando. Para citar a Stéphane Chazelas de esta publicación :

La bifurcación es costosa, en tiempo de CPU, memoria, descriptores de archivo asignados ... Tener un proceso de shell mintiendo sobre solo esperar otro proceso antes de salir es solo un desperdicio de recursos. Además, hace que sea difícil informar correctamente el estado de salida del proceso separado que ejecutaría el comando (por ejemplo, cuando se cierra el proceso).

Por lo tanto, muchos shells (no solo bash) se usan exec()para permitir que eso bash -c ''sea ​​asumido por ese simple comando simple. Y exactamente por las razones indicadas anteriormente, es mejor minimizar las canalizaciones en los scripts de shell. A menudo puedes ver a los principiantes hacer algo como esto:

cat /etc/passwd | cut -d ':' -f 6 | grep '/home'

Por supuesto, esto tendrá fork()3 procesos. Este es un ejemplo simple, pero considere un archivo grande, en el rango de Gigabytes. Sería mucho más eficiente con un proceso:

awk -F':' '$6~"/home"{print $6}' /etc/passwd

El desperdicio de recursos en realidad puede ser una forma de ataque de denegación de servicio, y en particular las bombas de horquilla se crean a través de funciones de shell que se llaman a sí mismas en la tubería, que bifurca múltiples copias de sí mismos. Hoy en día, esto se mitiga limitando el número máximo de procesos en cgroups en systemd , que Ubuntu también usa desde la versión 15.04.

Por supuesto, eso no significa que el tenedor sea malo. Todavía es un mecanismo útil como se discutió anteriormente, pero en caso de que pueda salirse con la suya con menos procesos y consecutivamente menos recursos y, por lo tanto, un mejor rendimiento, debe evitarlo fork()si es posible.

Ver también

Sergiy Kolodyazhnyy
fuente
1

Para cada comando (ejemplo: grep) que emita en el indicador de bash, realmente tiene la intención de comenzar un nuevo proceso y luego volver al indicador de bash después de la ejecución.

Si el proceso de shell (bash) llama a exec () para ejecutar grep, el proceso de shell será reemplazado por grep. Grep funcionará bien, pero después de la ejecución, el control no puede volver al shell porque el proceso bash ya está reemplazado.

Por esta razón, bash llama a fork (), que no reemplaza el proceso actual.

FlowRaja
fuente