viernes, 20 de noviembre de 2015

Zombie Processes - Procesos Zombies

No, no se trata de procesos que se mueven de manera errática y que convierten a otros procesos si los muerden (aunque sería muy divertido).

Cuando hablamos de procesos zombies nos referimos a procesos que han completado su ejecución (estado terminated) pero aun se encuentran registrados en la tabla de procesos del kernel. ¿Por qué sucede esto? Básicamente sucede para poder garantizar que el proceso padre pueda obtener el estado final de sus procesos hijos, para saber cuál fue el resultado. Una vez que el proceso padre lee el estado de salida del hijo (a través de la syscall wait) este último será removido de la tabla de procesos y podrá descansar finalmente en paz, e ir al cielo de los procesos.

Algunos puntos para resaltar:
  • Un proceso zombie es un proceso cuya ejecución ha finalizado y el estado del proceso es TERMINATED. Puede haber terminado por las buenas o por las malas (con kill por ejemplo).
  • La memoria ocupada por el proceso ha sido liberada.
  • Podemos ver procesos zombies utilizando ps aux, los reconoceremos por la Z en la columna de STAT.
  • Todo proceso que termina su ejecución se vuelve zombie, aunque raras veces lo notaremos dado que por lo general el proceso padre estará esperándolo con la llamada wait.

Creando zombies


A modo de prueba de concepto veamos el siguiente código, que será nuestro generador de zombies:

#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
        pid_t pid;
        int pid_status;
        pid = fork();
        if (pid == 0) {
                sleep(10);
                exit(9);
        }
        printf("PID del hijo %d\n",pid);
        sleep(40);
        waitpid(pid, &pid_status, 0);
        printf("Estado de salida %d\n",WEXITSTATUS(pid_status));
        return 0;
}

El código es sencillo, luego de la llamada a fork tendremos en el sistema corriendo dos procesos idénticos. Uno de ellos, el hijo, ejecutará el código dentro del if, mientras que el padre irá directamente al primer printf, luego  dormirá por 40 segundos y ejecutará waitpid para obtener el estado de terminación del hijo e imprimirlo.

Veamos un poco qué sucede durante la ejecución:

[ec2-user@ip-172-31-16-177 ~]$ gcc -o zombies zombies.c
[ec2-user@ip-172-31-16-177 ~]$ ./zombies
PID del hijo 23037
Estado de salida 9
[ec2-user@ip-172-31-16-177 ~]$

El proceso padre imprimió el PID del hijo (23037), luego durmió por 40 segundos, ejecutó waitpid para recojer el valor de salida del proceso hijo (9) y lo imprimió en pantalla. Usando ps antes de que termine la ejecución del proceso hijo podemos ver un poco mas en detalle lo que sucede:

La relación entre los procesos (23036 es el proceso padre y 23037 es el hijo):

[ec2-user@ip-172-31-16-177 ~]$  ps -eo pid,ppid,cmd|grep zombie
23036  2407 ./zombies
23037  23036 ./zombies
23293 22781 grep --color=auto zombie
[ec2-user@ip-172-31-16-177 ~]$

Luego aun dentro de los primeros 10 segundos vemos lo siguiente:

[ec2-user@ip-172-31-16-177 ~]$ ps aux|grep zombies
ec2-user 23036  0.0  0.0   4176   620 pts/0    S+   22:22   0:00 ./zombies
ec2-user 23037  0.0  0.0   4172    80 pts/0    S+   22:22   0:00 ./zombies
ec2-user 23039  0.0  0.2 110460  2196 pts/1    S+   22:22   0:00 grep --color=auto zombies
[ec2-user@ip-172-31-16-177 ~]$

ambos procesos se encuentran en estado Sleeping (S+), y podemos ver también que ambos tienen el mismo valor de VSZ (virtual memory size), lo cual tiene sentido dado que son idénticos luego de la llamada fork. Pasados los 10 segundos nos encontramos con la siguiente situación:

[ec2-user@ip-172-31-16-177 ~]$ ps aux|grep zombies
ec2-user 23036  0.0  0.0   4176   620 pts/0    S+   22:22   0:00 ./zombies
ec2-user 23037  0.0  0.0      0     0 pts/0    Z+   22:22   0:00 [zombies] <defunct>
ec2-user 23041  0.0  0.2 110460  2124 pts/1    S+   22:22   0:00 grep --color=auto zombies
[ec2-user@ip-172-31-16-177 ~]$

el proceso hijo se encuentra ahora en Zombie state (Z+) y su VSZ es 0 (dado que toda la memoria ocupada fue liberada).

Una vez que se cumplen los 40 segundos, el proceso padre y el hijo desaparecen del sistema.


[ec2-user@ip-172-31-16-177 ~]$ ps aux|grep zombies
[ec2-user@ip-172-31-16-177 ~]$

Cómo matar un proceso zombie?


Como se imaginaran, cortarle la cabeza al proceso no parece ser una opción viable en este contexto. El equivalente en el mundo de los procesos es la señal SIGKILL (9), pero veamos qué sucede cuando la usamos para matar un proceso zombie:

Ejecución del binario zombie:

[ec2-user@ip-172-31-22-1 ~]$ ./zombie
PID del hijo 2765
Estado de salida 9
[ec2-user@ip-172-31-22-1 ~]$


Intentos fallidos de acabar con la existencia del proceso 2765:

[ec2-user@ip-172-31-22-1 ~]$ ps aux|grep zombie
ec2-user  2764  0.0  0.0   4176   692 pts/1    S+   21:06   0:00 ./zombie
ec2-user  2765  0.0  0.0   4172    80 pts/1    S+   21:06   0:00 ./zombie
ec2-user  2769  0.0  0.0 110460  2196 pts/0    S+   21:06   0:00 grep --color=auto zombie
[ec2-user@ip-172-31-22-1 ~]$ ps aux|grep zombie
ec2-user  2764  0.0  0.0   4176   692 pts/1    S+   21:06   0:00 ./zombie
ec2-user  2765  0.0  0.0      0     0 pts/1    Z+   21:06   0:00 [zombie]
ec2-user  2771  0.0  0.0 110460  2152 pts/0    S+   21:06   0:00 grep --color=auto zombie
[ec2-user@ip-172-31-22-1 ~]$
[ec2-user@ip-172-31-22-1 ~]$ kill -9 2765
[ec2-user@ip-172-31-22-1 ~]$ ps aux|grep zombie
ec2-user  2764  0.0  0.0   4176   692 pts/1    S+   21:06   0:00 ./zombie
ec2-user  2765  0.0  0.0      0     0 pts/1    Z+   21:06   0:00 [zombie]
ec2-user  2773  0.0  0.0 110460  2140 pts/0    S+   21:06   0:00 grep --color=auto zombie
[ec2-user@ip-172-31-22-1 ~]$ kill -9 2765
[ec2-user@ip-172-31-22-1 ~]$ ps aux|grep zombie
ec2-user  2764  0.0  0.0   4176   692 pts/1    S+   21:06   0:00 ./zombie
ec2-user  2765  0.0  0.0      0     0 pts/1    Z+   21:06   0:00 [zombie]
ec2-user  2775  0.0  0.0 110460  2192 pts/0    S+   21:07   0:00 grep --color=auto zombie
[ec2-user@ip-172-31-22-1 ~]$


Claramente kill -9 no esta siendo capaz de terminar el proceso, la señal está siendo enviada al proceso  y no recibimos ningun mensaje de error o algo similar.

Cómo podríamos deshacernos de ellos?


Lamentablemente, la única manera de deshacernos de ellos es matando el proceso padre. Dando muerte al proceso padre, los procesos zombies se convierten en procesos huérfanos y serán adoptados por init, luego init ejecutará waitpid y los procesos descansarán finalmente en paz.

A continuación un pequeño ejemplo:

[ec2-user@ip-172-31-16-177 ~]$  ps -eo pid,ppid,cmd|grep zombie
23346  2407 ./zombies1
23347 23346 ./zombies1
23348 23346 ./zombies1
23349 23346 ./zombies1
23350 23346 ./zombies1
23351 23346 ./zombies1
23353 22781 grep --color=auto zombie
[ec2-user@ip-172-31-16-177 ~]$ ps aux|grep zombies
ec2-user 23346  0.0  0.0   4172   600 pts/0    S+   23:40   0:00 ./zombies1
ec2-user 23347  0.0  0.0      0     0 pts/0    Z+   23:40   0:00 [zombies1]
ec2-user 23348  0.0  0.0      0     0 pts/0    Z+   23:40   0:00 [zombies1]

ec2-user 23349  0.0  0.0      0     0 pts/0    Z+   23:40   0:00 [zombies1]

ec2-user 23350  0.0  0.0      0     0 pts/0    Z+   23:40   0:00 [zombies1]

ec2-user 23351  0.0  0.0      0     0 pts/0    Z+   23:40   0:00 [zombies1]

ec2-user 23355  0.0  0.2 110460  2124 pts/1    S+   23:41   0:00 grep --color=auto zombies
[ec2-user@ip-172-31-16-177 ~]$ kill -9 23346
[ec2-user@ip-172-31-16-177 ~]$ ps aux|grep zombies
ec2-user 23357  0.0  0.2 110460  2156 pts/1    S+   23:41   0:00 grep --color=auto zombies
[ec2-user@ip-172-31-16-177 ~]$


Podemos ver que tenemos 5 procesos en estado zombie y cuyo padre es el proceso 23346. Una vez que enviamos la señal (con kill) para matar el proceso padre, todos los hijos desaparecen.

El código utilizado fue el siguiente:

#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
        pid_t pid;
        int pid_status=0;
        int i;
        for(i=0;i<5;i++)
        {
                pid = fork();
                if (pid == 0) {
                        sleep(3);
                        exit(9);
                }
        }
        sleep(600);
        return 0;
}

Por suerte, dado que los procesos Zombies son proceso que técnicamente ya no se encuentran consumiendo recursos (salvo por las estructuras de kernel donde se encuentran representados) no deberíamos preocuparnos demasiado. Sin embargo podríamos caer en una situación donde tenemos muchos de ellos acumulados...

Zombie fork bomb!


Ok, los procesos en estado zombie en teoría no consumen memoria, pero siguen estando representados dentro del kernel de alguna manera (de lo contrario no serian visibles para ps por ejemplo) por lo que algo de memoria deben consumir. Qué sucede si creamos tantos zombies como procesos podemos crear según ulimit?

[ec2-user@ip-172-31-22-1 ~]$ ulimit -u
31877
[ec2-user@ip-172-31-22-1 ~]$


Según ulimit este usuario puede crear hasta 31877 procesos,  de los cuales ya hay:

[ec2-user@ip-172-31-22-1 ~]$ ps aux | grep ^ec2-user | wc -l
6[ec2-user@ip-172-31-22-1 ~]$


Entonces con el proceso padre hacemos 7 y por lo tanto veamos qué pasa si ocupamos todos los procesos, intentemos crear 31870 zombies (seguro me van a llamar de The walking dead después de esto), usando el siguiente código:

#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
        pid_t pid;
        int bomb;
        for(bomb=0;bomb<31870;bomb++)
        {
                pid = fork();
                if (pid == 0) {
                        sleep(30);
                        exit(9);
                }
        }
        getchar();
        return 0;
}

Ejecutamos (abrí una consola como root, just in case):

[ec2-user@ip-172-31-22-1 ~]$ gcc -o zombie_fork_bomb zombie_fork_bomb.c
[ec2-user@ip-172-31-22-1 ~]$ ./zombie_fork_bomb

Y desde la consola de root vemos lo siguiente:

[root@ip-172-31-22-1 ec2-user]# free -m
             total       used       free     shared    buffers     cached
Mem:          7987        417       7569          0         13        320
-/+ buffers/cache:         83       7903
Swap:            0          0          0

[root@ip-172-31-22-1 ec2-user]# ps aux|grep ^ec2-user|wc -l
31877
[root@ip-172-31-22-1 ec2-user]# ps aux|
grep ^ec2-user|grep " Z+ "|wc -l
31870
[root@ip-172-31-22-1 ec2-user]# free -m
             total       used       free     shared    buffers     cached
Mem:          7987       1445       6541          0         13        320
-/+ buffers/cache:       1111       6875
Swap:            0          0          0
[root@ip-172-31-22-1 ec2-user]#


Se pudieron los 31870 zombies y con eso alcanzamos el limite de los 31877 procesos disponibles para el usuario ec2-user. Eso lo podemos comprobar con la segunda consola del usuario:

[ec2-user@ip-172-31-22-1 ~]$ ls
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
^C-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: Resource temporarily unavailable


[ec2-user@ip-172-31-22-1 ~]$


Claramente bash no esta pudiendo hacer el fork para ejecutar ls. Por otro lado también podemos ver que el consumo de memoria aumento considerablemente de 417MBytes a 1445MBytes, prácticamente triplicado, pero claro, estamos hablando de 31871 procesos en estado zombie.

Con esto podemos inferir que cada proceso en estado zombie nos cuesta alrededor de 33Kbytes de memoria ((1445-417)/31871*1024 Kbytes) y lo que es tal vez mas importante, cuenta como un proceso más en ejecución a la hora de controlar los límites del usuario.

Conclusión:

Los procesos zombies solo pueden ser eliminados si su padre o init ejecutan la función wait para ese process ID. A pesar de que los procesos zombies son proceso cuya ejecución ha finalizado, siguen ocupando lugar en el kernel y podrían ocasionar problemas mayores si se tratara de un gran numero de ellos.

Como breve comentario final, no hay que confundir los proceso en estado Z (zombie) con los procesos en estado D (uninterruptible). Estos últimos por lo general se encuentran esperando alguna operación de E/S y por lo tanto no hay manera de interrumpirlos. Deshacerse de uno de estos procesos es BASTANTE mas complicado y podría llegar a ser necesario reiniciar el sistema. En otra entrada vamos a ver de que se trata eso.


No hay comentarios:

Publicar un comentario