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.