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.

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.

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