¿Dónde está el tenedor () en la bomba tenedor: () {: |: &};:?

25

Advertencia: ejecutar este comando en la mayoría de los shells dará como resultado un sistema dañado que necesitará un apagado forzado para solucionarlo

Entiendo la función recursiva :(){ :|: & };:y lo que hace. Pero no sé dónde está la llamada del sistema fork. No estoy seguro, pero sospecho que en la tubería |.

mavillan
fuente
Relacionado (y vale la pena leer): ¿Cómo funciona una bomba tenedor?
terdon

Respuestas:

30

Como resultado de la canalización x | y, se crea una subshell para contener la canalización como parte del grupo de procesos en primer plano. Esto continúa creando subcapas (vía fork()) indefinidamente, creando así una bomba tenedor.

$ 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

La bifurcación en realidad no ocurre hasta que se ejecuta el código, sin embargo, que es la invocación final de :su código.

Para desmontar cómo funciona la bomba tenedor:

  • :() - define una nueva función llamada :
  • { :|: & } - una definición de función que canaliza recursivamente la función de llamada en otra instancia de la función de llamada en segundo plano
  • : - llama a la función de bomba tenedor

Esto tiende a no consumir demasiado memoria, pero absorberá los PID y consumirá ciclos de CPU.

Chris Down
fuente
En x | y, ¿por qué hay un sub-shell creado? A mi entender, cuando bash ve un pipe, ejecuta una pipe()llamada al sistema, que devuelve dos fds. Ahora, command_left se edita execy la salida se alimenta a command_right como entrada. Ahora, command_right es execed. Entonces, ¿por qué es BASHPIDdiferente cada vez?
Abhijeet Rastogi
2
@shadyabhi Es simple, xy yhay 2 comandos separados ejecutándose en 2 procesos separados, por lo que tiene 2 subcapas separadas. Si se xejecuta en el mismo proceso que el shell, eso significa que xdebe estar integrado.
jw013
24

El último bit del código, ;:está ejecutando la función :(){ ... }. Aquí es donde está ocurriendo la bifurcación.

El punto y coma termina el primer comando, y estamos comenzando otro, es decir, invocando la función :. La definición de esta función incluye una llamada a sí mismo ( :) y la salida de esta llamada se canaliza a una versión en segundo plano :. Esto apuntala el proceso indefinidamente.

Cada vez que usted está llamando a la función :()que está llamando la función C fork(). Finalmente, esto agotará todas las ID de proceso (PID) en el sistema.

Ejemplo

Puede intercambiarlo |:&con otra cosa para tener una idea de lo que está sucediendo.

Configurar un observador

En una ventana de terminal, haga esto:

$ watch "ps -eaf|grep \"[s]leep 61\""

Configurar la bomba de horquilla "fusible retrasado"

En otra ventana, ejecutaremos una versión ligeramente modificada de la bomba tenedor. Esta versión intentará estrangularse para que podamos estudiar lo que está haciendo. Nuestra versión dormirá durante 61 segundos antes de llamar a la función :().

También haremos un fondo de la llamada inicial, después de que se invoque. Ctrl+ z, luego escriba bg.

$ :(){ sleep 61; : | : & };:

# control + z
[1]+  Stopped                 sleep 61
[2] 5845
$ bg
[1]+ sleep 61 &

Ahora, si ejecutamos el jobscomando 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                    : | :

Registrarse con el observador

Mientras tanto, en la otra ventana donde estamos corriendo 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 a ps -auxfmuestra 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

A killall bashdetendrá 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 y gentil que no desgarrará cada bashcaparazón sería hacer lo siguiente:

  1. Determine en qué pseudo terminal se ejecutará la bomba tenedor

    $ tty
    /dev/pts/4
  2. Mata el pseudo terminal

    $ pkill -t pts/4

Entonces, ¿qué está pasando?

Bueno, cada invocación de bashy sleepes una llamada a la función C fork()desde el bashshell desde donde se ejecutó el comando.

slm
fuente
77
bashpodría estar ejecutándose en terminales separadas. Mejor sería usarlo pkill -t pts/2.
Maciej Piechotka
@MaciejPiechotka: gracias por la sugerencia. Nunca había visto ese antes, ¡lo he agregado a la respuesta!
slm