Un proceso zombie es un proceso que ya ha terminado pero cuya entrada sigue en la tabla de procesos porque su padre no ha recogido su código de salida. No consume CPU ni memoria y no se puede matar con kill, porque ya está muerto: la única forma de eliminarlo es actuar sobre su proceso padre. En este tutorial aprenderás a detectar zombies en Ubuntu 24.04, a identificar el proceso que los genera, a eliminarlos sin reiniciar el servidor y a evitarlos en tus propios programas y contenedores.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath. Los comandos funcionan igual en cualquier distribución con systemd.
  • Un usuario no root con privilegios sudo.
  • Docker instalado, solo si quieres seguir la sección sobre contenedores.

Cómo se crea un zombie

Cuando un proceso termina, el núcleo libera su memoria y sus archivos abiertos, pero conserva una pequeña entrada con su PID y su código de salida. Esa entrada existe para que el proceso padre pueda consultar cómo terminó su hijo llamando a wait() o waitpid(). Mientras el padre no lo haga, el hijo aparece en estado Z (zombie) y con la etiqueta <defunct>.

Esto significa que:

  • Un zombie que existe durante unos milisegundos es normal: es el intervalo entre que el hijo termina y el padre lo recoge.
  • Un zombie que persiste indica que el padre no está llamando a wait(), casi siempre por un error de programación o porque el padre está bloqueado.
  • Si el padre termina, sus hijos zombie pasan a depender de PID 1 (systemd) o del proceso designado como recolector, que los recoge de inmediato.

Un zombie no consume memoria ni CPU. El único recurso que ocupa es un PID. El problema aparece cuando un programa defectuoso crea miles de ellos y agota los PID disponibles o el límite de tareas de su servicio, y entonces no se pueden crear procesos nuevos.

Paso 1: Detectar procesos zombie

La cabecera de top muestra el recuento de zombies en la segunda línea:

top -bn1 | head -n 3
top - 10:15:02 up 20 days,  2:41,  1 user,  load average: 0.08, 0.05, 0.01
Tasks: 142 total,   1 running, 138 sleeping,   0 stopped,   3 zombie
%Cpu(s):  1.2 us,  0.6 sy,  0.0 ni, 98.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.1 st

Para listarlos con su PID, el PID del padre (PPID) y el tiempo que llevan en ese estado:

ps -eo pid,ppid,stat,etime,cmd | awk 'NR == 1 || $3 ~ /^Z/'
    PID    PPID STAT     ELAPSED CMD
  48211   48190 Z       02:13:40 [worker.py] <defunct>
  48213   48190 Z       02:13:38 [worker.py] <defunct>
  48240   48190 Z       02:12:55 [worker.py] <defunct>

La columna ELAPSED distingue un zombie transitorio de uno persistente: si sigue ahí al cabo de minutos u horas, el padre no lo está recogiendo. Todos los zombies de este ejemplo comparten el mismo padre, 48190.

Paso 2: Encontrar el proceso padre responsable

Cuando hay muchos zombies, agrúpalos por padre para saber qué proceso los genera:

ps -eo ppid=,stat= | awk '$2 ~ /^Z/ {print $1}' | sort | uniq -c | sort -rn
      3 48190

Consulta qué es ese proceso padre:

ps -o pid,ppid,user,stat,etime,cmd -p 48190
    PID    PPID USER     STAT     ELAPSED CMD
  48190       1 app      Sl      02:14:10 /usr/bin/python3 /opt/app/scheduler.py

systemctl status acepta un PID y muestra a qué servicio de systemd pertenece el proceso, lo que te dice qué reiniciar:

systemctl status 48190
● app-scheduler.service - Planificador de tareas de la aplicación
     Loaded: loaded (/etc/systemd/system/app-scheduler.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-09-25 08:01:12 UTC; 2h 14min ago
   Main PID: 48190 (python3)

Para ver la cadena completa de antecesores de un zombie, útil cuando el padre es a su vez hijo de otro proceso, usa pstree:

pstree -p -s 48211
systemd(1)───python3(48190)───python3(48211)

Paso 3: Comprobar si los zombies son un problema

Unos pocos zombies estables no afectan al rendimiento. Lo que importa es si su número crece. Cuenta los zombies varias veces a lo largo de unos minutos:

ps -eo stat= | grep -c '^Z'

Si la cifra aumenta de forma constante, el padre acabará agotando sus límites. Compara el número total de procesos con el máximo de PID del sistema:

cat /proc/sys/kernel/pid_max
ps -e --no-headers | wc -l
4194304
151

En un servicio de systemd, el límite que se alcanza antes suele ser el de tareas de la unidad. Consulta cuántas tareas usa y cuál es su máximo:

systemctl show -p TasksCurrent -p TasksMax app-scheduler.service
TasksCurrent=18
TasksMax=4915

Cuando TasksCurrent se acerca a TasksMax, el servicio empieza a fallar al crear procesos o hilos con errores como Resource temporarily unavailable.

Paso 4: Reproducir un zombie para practicar

Antes de actuar sobre un servicio real, conviene ver el comportamiento con un ejemplo inofensivo. Este comando crea un subproceso que lanza sleep 1 en segundo plano y después se sustituye a sí mismo por sleep 120 con exec. El nuevo programa no sabe que tiene un hijo y nunca llama a wait(), así que al cabo de un segundo sleep 1 queda como zombie:

(sleep 1 & exec sleep 120) &

Espera un par de segundos y búscalo:

ps -eo pid,ppid,stat,cmd | awk 'NR == 1 || $3 ~ /^Z/'
    PID    PPID STAT CMD
  51322   51321 Z    [sleep] <defunct>

Intenta matarlo con la señal más drástica:

kill -9 51322
ps -o pid,stat,cmd -p 51322
    PID STAT CMD
  51322 Z    [sleep] <defunct>

El zombie sigue ahí, porque ya no hay ningún proceso vivo que pueda recibir la señal. Ahora termina el padre (51321, el sleep 120):

kill 51321
ps -o pid,stat,cmd -p 51322
    PID STAT CMD

Al morir el padre, el zombie pasó a depender de systemd, que lo recogió al instante. Este es el mecanismo que usan todos los métodos del paso siguiente.

Paso 5: Eliminar los zombies de un servicio

Aplica los métodos en este orden, del menos al más disruptivo.

Método 1: Pedir al padre que recoja a sus hijos

Algunos programas recogen a sus hijos en el manejador de la señal SIGCHLD. Si la señal se perdió, enviarla de nuevo puede bastar:

sudo kill -s SIGCHLD 48190

Vuelve a contar los zombies. Si no cambian, el programa no gestiona esa señal y tienes que pasar al método siguiente.

Método 2: Reiniciar el servicio

Si el padre pertenece a un servicio de systemd, reiniciarlo termina el proceso padre de forma ordenada, y systemd recoge los zombies huérfanos:

sudo systemctl restart app-scheduler.service

Comprueba que el recuento vuelve a cero:

ps -eo stat= | grep -c '^Z'
0

Método 3: Terminar el proceso padre

Si el padre no es un servicio (por ejemplo, un script lanzado a mano o por cron), termínalo con SIGTERM, que le permite cerrar de forma limpia:

sudo kill 48190

Si al cabo de unos segundos sigue vivo, fuerza el cierre:

sudo kill -9 48190

Si el padre es PID 1

Si el PPID de los zombies es 1, systemd debería recogerlos en cuanto aparecen. Unos zombies persistentes colgando de PID 1 indican un problema grave del sistema. Revisa sudo journalctl -b -p err y planifica un reinicio del servidor.

Paso 6: Zombies dentro de contenedores Docker

Dentro de un contenedor, el proceso principal de la aplicación se ejecuta como PID 1 del contenedor. El núcleo le asigna los huérfanos de ese espacio de nombres, así que es él quien debe recogerlos. La mayoría de aplicaciones (Node.js, Python, Java) no están escritas para hacer de init, y si lanzan subprocesos (por ejemplo, un navegador sin interfaz o comandos de shell) los zombies se acumulan dentro del contenedor.

Desde el host, los zombies de un contenedor aparecen como hijos del proceso principal del contenedor. Para ver los procesos de un contenedor concreto:

docker top nombre_contenedor -eo pid,ppid,stat,cmd

La solución es ejecutar un init mínimo como PID 1 del contenedor. Docker incluye uno (tini) que se activa con --init:

docker run -d --init --name nombre_contenedor tu_imagen

En Docker Compose, añade init: true al servicio:

services:
  app:
    image: tu_imagen
    init: true

Recrea el contenedor para aplicarlo (docker compose up -d) y comprueba que el PID 1 del contenedor ya es el init:

docker exec nombre_contenedor cat /proc/1/cmdline | tr '\0' ' '; echo
/sbin/docker-init -- node server.js

Paso 7: Evitar zombies en tu propio código

Si el proceso padre es una aplicación tuya, la solución definitiva es que recoja a sus hijos.

En Bash, usa wait para esperar a los procesos lanzados en segundo plano:

#!/usr/bin/env bash
set -euo pipefail

procesar lote1.csv &
procesar lote2.csv &
wait
echo "Todos los lotes terminados"

En Python, usa subprocess.run(), que espera al hijo. Si necesitas subprocess.Popen para no bloquear, llama a wait() o communicate() cuando el hijo termine:

import subprocess

proc = subprocess.Popen(["/usr/local/bin/tarea", "--lote", "1"])
# ... otro trabajo ...
codigo = proc.wait()

En C, recoge a los hijos en un manejador de SIGCHLD con waitpid() y WNOHANG en bucle, porque varias terminaciones pueden llegar como una sola señal:

#include <errno.h>
#include <signal.h>
#include <sys/wait.h>

static void recoger_hijos(int sig)
{
    (void)sig;
    int errno_guardado = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0) {
    }
    errno = errno_guardado;
}

int instalar_manejador(void)
{
    struct sigaction sa = {0};
    sa.sa_handler = recoger_hijos;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    return sigaction(SIGCHLD, &sa, NULL);
}

Si no necesitas el código de salida de los hijos, basta con ignorar la señal con signal(SIGCHLD, SIG_IGN);: Linux descartará a los hijos al terminar sin dejarlos como zombies.

Solución de problemas

Un proceso en estado D tampoco muere con kill -9: no es un zombie. El estado D (espera no interrumpible) indica un proceso bloqueado esperando al disco o a la red, típicamente un montaje NFS caído o un disco con errores. Revisa sudo dmesg -T | tail -n 50; el proceso terminará cuando la operación de entrada y salida se complete o falle.

Los zombies reaparecen después de reiniciar el servicio: el reinicio solo limpia los existentes. El error sigue en el código del padre; aplica las técnicas del paso 7 o, si es software de terceros, busca una actualización o reporta el fallo.

El padre de los zombies es containerd-shim: el contenedor ha terminado de forma anómala o su proceso principal ha salido dejando hijos. Reinicia el contenedor y, si se repite, añade --init como en el paso 6.

Conclusión

Un proceso zombie es un proceso ya terminado que espera a que su padre recoja su estado. No se elimina con kill sobre el zombie, sino haciendo que el padre llame a wait(), reiniciando su servicio o terminándolo para que systemd recoja a los huérfanos. Como siguientes pasos, revisa los contenedores que lanzan subprocesos y actívales --init, incluye el recuento de zombies en la monitorización de tus servidores y corrige el manejo de procesos hijos en las aplicaciones que los generen.