Como resultado de la canalización en x | y
, se crea una subcapa para contener la canalización como parte del grupo de procesos en primer plano. Esto continúa creando subcapas (a través de fork()
) indefinidamente, creando así una bomba de horquilla.
$ for (( i=0; i<3; i++ )); do
> echo "$BASHPID"
> done
16907
16907
16907
$ for (( i=0; i<3; i++ )); do
> echo "$BASHPID" | cat
> done
17195
17197
17199
Sin embargo, la bifurcación en realidad no ocurre hasta que se ejecuta el código, que es la invocación final de :
en tu código.
Para desmontar cómo funciona la bomba de horquilla:
:()
- definir una nueva función llamada:
{ :|: & }
- una definición de función que canaliza recursivamente la función de llamada a otra instancia de la función de llamada en segundo plano:
- llamar a la función de bomba de horquilla
Esto tiende a no hacer un uso intensivo de la memoria, pero absorberá los PID y consumirá ciclos de CPU.
El último bit del código, ;:
está ejecutando la función :(){ ... }
. Aquí es donde se produce la bifurcación.
El punto y coma termina el primer comando y estamos iniciando otro, es decir, invocando la función :
. La definición de esta función incluye una llamada a sí misma (:
) y la salida de esta llamada se canaliza a una versión en segundo plano :
. Esto apuntala el proceso indefinidamente.
Cada vez que llamas a la función :()
estás llamando a la función C fork()
. Eventualmente, esto agotará todos los ID de proceso (PID) en el sistema.
Ejemplo
Puedes cambiar el |:&
con algo más para que puedas tener una idea de lo que está pasando.
Configurar un observador
En una ventana de terminal, haga esto:
$ watch "ps -eaf|grep \"[s]leep 61\""
Configurar la bomba de horquilla "fusible retardado"
En otra ventana ejecutaremos una versión ligeramente modificada de la bomba de horquilla. Esta versión intentará acelerarse a sí misma para que podamos estudiar lo que está haciendo. Nuestra versión dormirá durante 61 segundos antes de llamar a la función :()
.
También pondremos en segundo plano la llamada inicial, después de que se invoque. Ctrl + z , luego escribe bg
.
$ :(){ sleep 61; : | : & };:
# control + z
[1]+ Stopped sleep 61
[2] 5845
$ bg
[1]+ sleep 61 &
Ahora si ejecutamos el jobs
comando en la ventana inicial veremos esto:
$ jobs
[1]- Running sleep 61 &
[2]+ Running : | : &
Después de un par de minutos:
$ jobs
[1]- Done sleep 61
[2]+ Done : | :
Comuníquese con el observador
Mientras tanto, en la otra ventana donde estamos ejecutando watch
:
Every 2.0s: ps -eaf|grep "[s]leep 61" Sat Aug 31 12:48:14 2013
saml 6112 6108 0 12:47 pts/2 00:00:00 sleep 61
saml 6115 6110 0 12:47 pts/2 00:00:00 sleep 61
saml 6116 6111 0 12:47 pts/2 00:00:00 sleep 61
saml 6117 6109 0 12:47 pts/2 00:00:00 sleep 61
saml 6119 6114 0 12:47 pts/2 00:00:00 sleep 61
saml 6120 6113 0 12:47 pts/2 00:00:00 sleep 61
saml 6122 6118 0 12:47 pts/2 00:00:00 sleep 61
saml 6123 6121 0 12:47 pts/2 00:00:00 sleep 61
Jerarquía de procesos
Y un ps -auxf
muestra esta jerarquía de procesos:
$ ps -auxf
saml 6245 0.0 0.0 115184 5316 pts/2 S 12:48 0:00 bash
saml 6247 0.0 0.0 100988 468 pts/2 S 12:48 0:00 \_ sleep 61
....
....
saml 6250 0.0 0.0 115184 5328 pts/2 S 12:48 0:00 bash
saml 6268 0.0 0.0 100988 468 pts/2 S 12:48 0:00 \_ sleep 61
saml 6251 0.0 0.0 115184 5320 pts/2 S 12:48 0:00 bash
saml 6272 0.0 0.0 100988 468 pts/2 S 12:48 0:00 \_ sleep 61
saml 6252 0.0 0.0 115184 5324 pts/2 S 12:48 0:00 bash
saml 6269 0.0 0.0 100988 464 pts/2 S 12:48 0:00 \_ sleep 61
...
...
Tiempo de limpieza
Un killall bash
detendrá las cosas antes de que se salgan de control. Hacer la limpieza de esta manera puede ser un poco pesado, una forma más amable que no romperá potencialmente cada bash
shell down, sería hacer lo siguiente:
-
Determine en qué pseudo terminal se ejecutará la bomba de horquilla
$ tty /dev/pts/4
-
Mata al pseudo terminal
$ pkill -t pts/4
Entonces, ¿qué está pasando?
Pues cada invocación de bash
y sleep
es una llamada a la función C fork()
del bash
shell desde donde se ejecutó el comando.