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 foo
se 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 -h
ejecute 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 stdout
tambié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, df
el proceso hijo no pensará en nada stdout
y 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 grep
conexec()
. 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 source
comando. 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