Crear, eliminar y entender de forma práctica los procesos zombie / fantasma en Unix
En sistemas operativos Unix, un proceso fantasma, zombie (zombi) o “defunct” (difunto) es un proceso que ha completado su ejecución pero aún tiene una entrada en la tabla de procesos, permitiendo al proceso que lo ha creado leer el estado de su salida. Metafóricamente, el proceso hijo ha muerto pero su “alma” aún no ha sido recogida.
Cuando un proceso finaliza en sistemas Unix, toda su memoria y recursos asociados a él se desreferencian (típico exit), para que puedan ser usados por otros procesos. En ese espacio de tiempo, la entrada del proceso hijo en la tabla de procesos permanece un mínimo tiempo, hasta que el padre conoce que el estado de su proceso hijo es finalizado y entonces lo saca de la tabla de procesos.
Para que el proceso padre sepa el estado de su hijo, se le envía una señal SIGCHLD indicando que el proceso hijo a finalizado. Esa señal es generada gracias a llamadas al sistema como wait() / waitpid() / waitid().
¿Qué pasa cuando no se usa esos manejadores para conocer el estado de los hijos (función wait() / waitpid() / waitid())? Pues que el padre no sabe que su hijo ha terminado y por lo tanto sigue en la lista de procesos. Los procesos zombie se generan por tanto, cuando el padre no recibe esa señal o bien la ignora, generalmente por bugs o aplicaciones mal programadas
Es posible, aunque algo poco común, que el padre esté muy ocupado y no pueda en ese momento matar al proceso. También podría ser que el padre decida tener un proceso zombie en la tabla para reservar ese PID, o que el padre esté interesado en eliminar los procesos hijos en un determinado orden,…
El tener procesos zombies en la tabla no suele ser un problema, al no ser que su número crezca exponencialmente y se ocupen todos los identificadores de procesos que el sistema operativo puede utilizar o bien se necesite el PID que el proceso fantasma ocupa.
Aclaraciones sobre el código utilizado en los ejemplos de creación de procesos zombie.
- Se define el tipo de dato pid_t el cual representa un identificador de proceso.
- Se genera un proceso hijo con fork(), este proceso es igual al padre.
- Los valores de child_pid entre padre e hijo son diferentes. -1 (Error), 0 (es el proceso hijo), distinto de 0 es el proceso padre.
Ejemplo de programa mal programado (no usa wait()).
- zombie.c
#include <stdlib.h> #include <sys/types.h> #include <unistd.h> int main () { pid_t child_pid; child_pid = fork (); if (child_pid > 0) { printf("Soy el proceso padre y espero 60 segundos antes de terminar, de mi hijo no se nada.\n"); sleep (60); } else { exit (0); } return 0; }
Si atendemos al código propuesto, como el PID del proceso hijo no es 0, este termina inmediatamente (exit) mientras el padre sigue en funcionamiento (pausa de 60 segundos) ignorando que su hijo ya terminó (está muerto), es decir, no se le ha enviado al padre esa señal de estado. Lógicamente después de esos 60 segundos, el padre finaliza y sus hijos zombies desaparecen también de la lista de procesos gracias al nuevo padre que heredan, init.
Si ejecutamos ese código y ejecutamos el comando top o ps, podremos ver que existe un proceso zombie en el sistema.
Ahora creamos el mismo código pero con una variación, la función wait(). Esto hará que el proceso padre espere al hijo antes de proseguir y retirarlo de la lista de procesos.
Ejemplo de programa sin las fallas del anterior usando la función wait().
- nozombie.c
#include <stdlib.h> #include <sys/types.h> #include <unistd.h> int main () { int estado; pid_t child_pid; child_pid = fork (); if (child_pid > 0) { wait(estado); printf("Soy el proceso padre, mi proceso hijo ha terminado y yo ahora espero 60 segundos antes de terminar\n"); sleep (60); } else { exit (0); } return 0; }
Compilar código de los ejemplos..
cc zombie.c -o zombie cc nozombie.c -o nozombie
Localizar procesos zombie. Los zombis pueden ser identificados en la salida del comando “ps” Unix por la presencia de una “Z” en la columna “STAT”.
Con este comando podemos averiguar los procesos zombies (661) y sus padres (ppid 660).
ps -A -ostat,ppid,pid,cmd | grep defunct Z+ 660 661 [zombie] <defunct> ps aux | grep -i 660 root 660 0.0 0.0 3924 404 pts/1 S+ 15:25 0:00 ./zombie ps aux | grep -i 661 root 661 0.0 0.0 0 0 pts/1 Z+ 15:25 0:00 [zombie] <defunct>
Eliminar procesos zombie de la tabla de procesos.
No se puede hablar de matar procesos zombies, lo correcto sería limpiar de la tabla un proceso zombi. Como se ha dicho antes, el padre del proceso zombie es el encargado de hacerlo. Si el padre muere teniendo zombies en la tabla de procesos, estos son heredados por init, el cual ejecuta periódicamente la llamada wait() para eliminar zombies que le tengan como padre.
Una de las maneras de limpiar de la tabla de procesos un zombie es enviar un “SIGCHLD” o bien “SIGHUP” si la anterior fue ignorada.
- Opción 1: Esperar si es algo temporal, matar al padre o reiniciar (Poco recomendable y elegante, solo si es necesario).
- Opción 2: Indicando al padre que su proceso hijo ha terminado (SIGCHLD).
Enviar la senal SIGCHLD al proceso padre es la primera medida recomendada y sensata.
kill -s SIGCHLD <ppid>
Si no funciona, podemos matar el proceso padre y los hijos activos pasarán a depender del proceso init.
kill -s SIGHUP <ppid>
Eliminar todos los procesos zombies.
kill -s SIGCHLD $(ps -A -ostat,ppid | grep -e '[zZ]'| awk '{ print $2 }')
Si SIGCHLD no ha tenido éxito, podemos ejecutar el siguiente comando para matar todos los procesos padre.
kill -s SIGHUP $(ps -A -ostat,ppid | grep -e '[zZ]'| awk '{ print $2 }')
Listado de señales para matar un proceso con Kill.
Signal Value Action Comment ────────────────────────────────────────────────────────────────────── SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process SIGINT 2 Term Interrupt from keyboard SIGQUIT 3 Core Quit from keyboard SIGILL 4 Core Illegal Instruction SIGABRT 6 Core Abort signal from abort(3) SIGFPE 8 Core Floating-point exception SIGKILL 9 Term Kill signal SIGSEGV 11 Core Invalid memory reference SIGPIPE 13 Term Broken pipe: write to pipe with no readers; see pipe(7) SIGALRM 14 Term Timer signal from alarm(2) SIGTERM 15 Term Termination signal SIGUSR1 30,10,16 Term User-defined signal 1 SIGUSR2 31,12,17 Term User-defined signal 2 SIGCHLD 20,17,18 Ign Child stopped or terminated SIGCONT 19,18,25 Cont Continue if stopped SIGSTOP 17,19,23 Stop Stop process SIGTSTP 18,20,24 Stop Stop typed at terminal SIGTTIN 21,21,26 Stop Terminal input for background process SIGTTOU 22,22,27 Stop Terminal output for background process
Ejemplo generando un proceso zombie al usar “kill -9”.
ps axo lstart,etime,bsdtime,euser,pid,ppid,%mem,%cpu,cmd | grep -i XXX Thu Apr 26 15:00:06 2018 00:04 0:00 usuario 1871 13567 0.0 0.0 timeout 30 sh -c while ! nc -z -v -w30 XXX 65; do sleep 1; done Thu Apr 26 15:00:06 2018 00:04 0:00 usuario 1872 1871 0.0 0.0 sh -c while ! nc -z -v -w30 XXX 65; do sleep 1; done kill -9 1871 # ZOMBIE (PPID -> 1) ps axo lstart,etime,bsdtime,euser,pid,ppid,%mem,%cpu,cmd | grep -i XXX Thu Apr 26 15:00:06 2018 00:18 0:00 usuario 1872 1 0.0 0.0 sh -c while ! nc -z -v -w30 XXX 65; do sleep 1; done <----- ZOMBIE