El escenario y dónde aplica
Un servicio de Linux inicia repetidamente procesos auxiliares de corta duración. Los nuevos trabajos ahora fallan intermitentemente con fork: Resource temporarily unavailable. La CPU y la memoria residente parecen normales, pero ps muestra miles de hijos en estado Z bajo un mismo proceso padre. En un contenedor, pids.current está cerca de pids.max y el contador max en pids.events se ha incrementado.
Explique qué es un zombi, demuestre si los hijos no recolectados causaron las fallas, restaure el servicio sin reiniciar si es posible y prevenga la recurrencia. Cubra tanto un host normal como un contenedor cuya aplicación puede ejecutarse como PID 1. Los recuentos y el despliegue son supuestos de entrevista; una respuesta sólida aún debe evaluar otros límites que pueden hacer que fork() devuelva EAGAIN.
Esta es una pregunta de general porque la habilidad central radica en el ciclo de vida de los procesos de Linux y el diagnóstico de incidentes. Las guías públicas actuales de entrevistas de Linux incluyen preguntas sobre procesos zombi frente a huérfanos, mientras que la documentación de Linux y Docker suministra los límites operativos. Esto sustenta una pregunta representativa y duradera sin dar a entender que se trata de un escenario específico de una empresa o de una frecuencia de entrevista determinada.
Qué evalúa el entrevistador
La primera señal es si el candidato separa el estado del proceso de los síntomas de recursos. Un zombi ya ha terminado su ejecución. El kernel retiene un PID, el estado de terminación y la información contable hasta que su padre los recopila mediante una llamada de la familia wait. No consume CPU normal ni la memoria del espacio de usuario del proceso terminado, pero sigue ocupando una entrada finita en la tabla de procesos/PID.
La segunda señal es la propiedad o pertenencia (ownership). El componente defectuoso suele ser el proceso padre activo que inició a los hijos pero no los recolectó. Enviar SIGKILL a un zombi no puede hacer que finalice de nuevo. Un shell tampoco puede llamar a wait() para el hijo de un proceso no relacionado. El candidato debe identificar al padre, su supervisor, su unidad de despliegue y su código de administración de hijos antes de tomar medidas.
La tercera señal es el diagnóstico causal. EAGAIN proveniente de fork() puede significar un RLIMIT_NPROC por usuario, el threads-max a nivel de todo el sistema, pid_max o un límite de pids.max de cgroup. Ver procesos zombi es una fuerte evidencia, no un permiso para omitir estas comprobaciones. Los hilos y los procesos no relacionados pueden contribuir al mismo límite máximo.
La señal final es una solución que sobreviva a ráfagas de tráfico, errores, apagados y contenedores. Una llamada a waitpid() por cada SIGCHLD es insuficiente porque las señales pueden agruparse (coalesce). El padre debe procesar y vaciar todos los hijos que hayan finalizado, y un contenedor debe tener un PID 1 que adopte y recolecte correctamente a los descendientes huérfanos.
Preguntas a clarificar antes de responder
- ¿Dónde se observa la falla? Una interrupción a nivel de todo el host, una sola cuenta de usuario y un solo contenedor apuntan a diferentes límites y radios de impacto.
- ¿Cuál es el error exacto y la llamada al sistema (syscall)?
EAGAINsugiere un límite de procesos/hilos;ENOMEMo el rechazo en una cola de la aplicación sigue otra vía. - ¿Están las entradas de
Zconcentradas bajo un solo PPID? Un solo PPID dominante identifica una vía de propiedad. Muchos PPID pueden indicar un wrapper compartido o un patrón de init de contenedor defectuoso. - ¿El padre está vivo, en buen estado y supervisado? Un padre activo puede repararse o reiniciarse de manera segura. Si termina, los hijos son readoptados por el subreaper más cercano o el init del espacio de nombres (namespace), que luego debe recolectarlos.
- ¿La aplicación se ejecuta como PID 1 en un contenedor? PID 1 tiene la responsabilidad adicional de recolectar huérfanos; el entorno de ejecución (runtime) podría necesitar un proceso init ligero.
- ¿Se puede drenar el tráfico o la admisión de trabajos? Un reinicio controlado es más seguro después de detener nuevos forks y preservar el trabajo en curso.
- ¿Qué se debe preservar de las salidas de los hijos? Los códigos de salida, la salida de errores y las decisiones de reintento determinan si la recolección corresponde a una llamada bloqueante, un bucle de eventos, un grupo de trabajadores (worker pool) o la API de procesos específica del runtime.
Estructura de respuesta de 30 segundos
“Un zombi ya está muerto; su padre no ha recopilado el estado de salida. Yo contaría los estados Z, los agruparía por PPID, inspeccionaría a ese padre y correlacionaría la serie temporal con los forks fallidos. También verificaría RLIMIT_NPROC, threads-max, pid_max y los archivos pids.current, pids.max y pids.events del cgroup, porque EAGAIN tiene varios límites posibles. No puedo arreglar un zombi con kill -9; detendría la creación de nuevos procesos, drenaría el tráfico y luego reiniciaría o repararía al padre de forma segura para que un subreaper o PID 1 que funcione adopte y recolecte a los hijos. Como solución permanente, el padre debe vaciar waitpid(-1, ..., WNOHANG) hasta que no quede ningún hijo terminado, incluyendo las rutas de error y apagado. En un contenedor también usaría un init adecuado cuando la aplicación no pueda cumplir las tareas de PID 1, para luego hacer pruebas de carga con ráfagas y verificar que el recuento de zombis y el uso de PID permanezcan acotados.”
Análisis detallado paso a paso
Paso 1: Establecer el estado del proceso y su propietario
Comience con comandos que no creen una tubería (pipeline) grande cuando el margen de PID ya es escaso:
ps -eo pid=,ppid=,stat=,etime=,comm= | awk '$3 ~ /^Z/'
ps -eo ppid=,stat= | awk '$2 ~ /^Z/ { count[$1]++ } END { for (p in count) print count[p], p }' | sort -nrEn ps, un estado que comience con Z es un zombi. /proc/PID/stat también expone el estado Z y el PPID. Confirme el PPID con más que solo un nombre de proceso truncado y luego inspeccione al padre activo:
ps -o pid=,ppid=,stat=,lstart=,etime=,cmd= -p PARENT_PID
cat /proc/PARENT_PID/status
cat /proc/PARENT_PID/limitsRegistre el recuento de zombis, la tasa de nuevos zombis, la versión de despliegue del padre y la hora de la primera falla. Un recuento estable que queda brevemente durante un apagado controlado es diferente de un recuento que aumenta con cada trabajo.
Paso 2: Demostrar qué límite rechazó a los nuevos hijos
No infiera "falta de memoria" de la frase orientada al usuario "Resource temporarily unavailable". Linux documenta varias rutas de fork() que devuelven EAGAIN:
- el
RLIMIT_NPROCdel usuario real; /proc/sys/kernel/threads-max;/proc/sys/kernel/pid_max;- el límite efectivo de PIDs de cgroup.
Para cgroup v2, examine los archivos en el cgroup real del servicio en lugar de asumir la ruta raíz:
cat /proc/PARENT_PID/cgroup
cat /sys/fs/cgroup/SERVICE_CGROUP/pids.current
cat /sys/fs/cgroup/SERVICE_CGROUP/pids.max
cat /sys/fs/cgroup/SERVICE_CGROUP/pids.eventsUn contador de eventos max en aumento demuestra directamente que los forks alcanzaron el techo del controlador de PIDs. Compare pids.current con el recuento de zombis y los recuentos de hilos/procesos activos; el controlador cuenta tareas en todos los descendientes, por lo que los zombis pueden no ser los únicos consumidores. En un host, compare también el recuento de tareas por usuario y los totales del sistema con sus límites. La cadena causal es más sólida cuando el crecimiento de zombis, el margen restante de PID, EAGAIN y la tasa de creación del padre se alinean en el tiempo.
Paso 3: Explicar por qué fallan los "arreglos" comunes
kill -9 ZOMBIE_PID no puede ejecutar lógica de salida porque el proceso ya ha salido. El registro residual del kernel desaparece solo cuando su padre, o un adoptador posterior, espera por él. El comando integrado wait del shell solo administra los propios hijos de ese shell.
Aumentar a ciegas pids.max, pid_max o RLIMIT_NPROC puede ganar tiempo para la recuperación, pero mantiene activa la fuga y puede ampliar el siguiente radio de impacto. Matar procesos vivos al azar libera ranuras pero no repara al padre. Reiniciar el sistema funciona al destruir todo el árbol de procesos, pero descarta evidencia y genera un tiempo de inactividad evitable.
Distinga también el estado D: un proceso en suspensión ininterrumpible está vivo y esperando en el kernel. Requiere un diagnóstico diferente, incluso si SIGKILL parece ineficaz. Tratar a cada PID rebelde como un zombi orienta la investigación en la dirección equivocada.
Paso 4: Recuperar el servicio con una transición controlada del proceso padre
Primero detenga o limite la admisión de nuevos trabajos para que el padre defectuoso no pueda consumir las ranuras restantes. Conserve una sesión administrativa y recolecte registros del padre, evidencia de /proc, límites e información de versión antes de cambiar el estado.
Si el padre tiene una recarga documentada que recrea su bucle de administración de hijos de manera segura, utilícela. De lo contrario, drene el trabajo en curso y reinicie ese padre a través de su supervisor. Cuando el padre finaliza, los descendientes no recolectados son adoptados por el subreaper de hijos más cercano o por el init del espacio de nombres de PID; un adoptador correcto espera por ellos. Verifique que el recuento de zombis disminuya antes de reabrir la admisión.
Si el adoptador no los recolecta, reiniciar únicamente el proceso trabajador no completará la recuperación. En un host, inspeccione el supervisor/subreaper del servicio. En un contenedor, puede requerirse un reemplazo controlado del contenedor porque el PID 1 del espacio de nombres tiene la responsabilidad final. Aumentar un límite de PID es aceptable solo como una medida documentada y temporal de margen, combinada con la limitación de tasa y un despliegue corregido.
Paso 5: Implementar una recolección que maneje ráfagas y errores
Para un padre que debe bloquearse hasta que un hijo conocido finalice, llame a waitpid(child_pid, ...) y maneje la interrupción. Para un padre asíncrono, organice que SIGCHLD despierte el bucle de eventos y luego vacíe todos los estados disponibles:
for (;;) {
pid_t pid = waitpid(-1, &status, WNOHANG);
if (pid > 0) {
record_child_result(pid, status);
continue;
}
if (pid == 0) break;
if (errno == EINTR) continue;
if (errno == ECHILD) break;
report_wait_error(errno);
break;
}Este bucle corresponde al contexto normal del bucle de eventos; un manejador de señales puro solo debe realizar operaciones permitidas por las reglas de seguridad para señales (signal-safety) del runtime, a menudo estableciendo un flag o escribiendo en un self-pipe. Vaciar es crucial porque varias salidas de hijos pueden producir una sola notificación observable. Cubra las fallas entre la creación y el registro, los tiempos de espera (timeouts), la cancelación, el apagado del padre y cada ruta de reintento. En un runtime administrado, la regla equivalente sigue siendo esperar o consumir de algún modo la finalización de cada proceso hijo.
Ignorar explícitamente SIGCHLD o usar SA_NOCLDWAIT puede solicitar la limpieza automática en sistemas compatibles, pero también elimina la recopilación normal del estado de salida y tiene consecuencias de portabilidad/API. Es una decisión de diseño deliberada, no un atajo para un gestor de trabajadores que necesita resultados.
Paso 6: Hacer que el PID 1 del contenedor y la verificación formen parte de la solución
El proceso principal de un contenedor es responsable de los procesos que inicia y puede convertirse en el adoptador de los descendientes. Si la aplicación no puede reenviar señales y recolectar correctamente como PID 1, ejecútela bajo la funcionalidad init ligera del runtime, como --init de Docker o la configuración equivalente de Compose. Un proceso init no exime a la aplicación de esperar a sus hijos directos cuyos resultados le pertenecen; cierra la brecha de adopción de huérfanos.
Verifique la solución con una ráfaga mayor que la concurrencia original, además de fallas de hijos y salidas rápidas. Las condiciones de aceptación deben incluir:
- cada hijo iniciado produce un estado de finalización consumido;
- el recuento de zombis vuelve a cero o a un límite breve documentado después de cada ráfaga;
pids.currentse estabiliza en lugar de subir ypids.eventsno registra nuevos alcances de límite;- no ocurre ningún
fork()/spawnEAGAINcon la carga esperada; - el apagado drena o termina a los hijos y luego los recolecta;
- una caída del padre deja a los descendientes bajo un subreaper o PID 1 probado;
- las alertas se disparan ante la tasa de crecimiento de zombis y el margen restante de PID antes de que falle la creación de trabajos.
Respuesta de muestra de alta calidad
“Primero verificaría que las entradas comiencen realmente con el estado Z, las agruparía por PPID e inspeccionaría al padre activo dominante. Un zombi ha completado su ejecución, por lo que se espera un uso normal de CPU y RSS. El kernel conserva su PID y el estado de salida hasta que el padre llama a una función de la familia wait. Es por eso que kill -9 sobre el zombi es ineficaz y por lo que el padre es el objetivo de reparación.
“Luego demostraría el límite que provocó el fallo de fork. Para el padre verificaría RLIMIT_NPROC; en el host verificaría threads-max y pid_max; en el cgroup del servicio leería pids.current, pids.max y pids.events. Si el contador max sube al mismo tiempo que el error y el grupo está cerca de su techo, eso demuestra la parte del cgroup. De todos modos contaría los hilos activos y las tareas descendientes para no atribuir cada ranura a los zombis.
“Para la recuperación, limitaría los nuevos trabajos, preservaría diagnósticos, drenaría el trabajo y reiniciaría o recargaría al padre a través de su supervisor. Sus zombis deberían ser adoptados y recolectados por un subreaper en funcionamiento o por el PID 1 del espacio de nombres. Si el PID 1 del contenedor es el adoptador dañado, reemplazaría el contenedor con una configuración habilitada con init. Elevar el límite es solo un margen temporal.
“La solución permanente a nivel de código consiste en consumir cada resultado de los hijos. Un propietario síncrono espera por el PID exacto. Un propietario basado en eventos trata a SIGCHLD como un aviso para despertar y ejecuta un bucle no bloqueante con waitpid() hasta que no quede ningún hijo finalizado, manejando a la vez fallas de creación, cancelaciones y apagado. Haría pruebas de carga con salidas rápidas, fallas forzadas, caídas del padre y apagado controlado. El pase a producción se aprueba solo cuando la contabilidad de finalización es uno a uno, los zombis permanecen acotados, el uso de PID se estabiliza y no ocurren nuevos eventos de límite alcanzado.”
Errores comunes
- Enviar
kill -9a cada zombi → el hijo ya ha terminado, por lo que la señal no puede recopilar su estado de salida → identifique y repare, recargue o reinicie al padre/adoptador. - Llamarlo fuga de memoria → los zombis retienen registros contables del kernel en lugar del espacio de direcciones normal del proceso finalizado → descríbalo como agotamiento de la tabla de procesos/PID y mida el recurso limitante real.
- Asumir el agotamiento de cgroup a partir de una sola instantánea de
ps→EAGAINtiene varios límites y los hilos activos también consumen capacidad de tareas → correlacione límites, contadores, recuentos de tareas y marcas de tiempo. - Llamar a
waitpid()una sola vez por señal → las señales de salida de hijos pueden agruparse, dejando estados adicionales sin recolectar → vacíe mediante esperas no bloqueantes hasta que la llamada reporte que no hay más listos. - Aumentar
pids.maxcomo solución definitiva → el padre defectuoso sigue fugando ranuras → use margen adicional solo para la recuperación, con control de tasa y un despliegue corregido programado. - Agregar un proceso init e ignorar a los hijos directos → la aplicación sigue siendo dueña de los resultados de sus hijos directos y de la semántica de reintento → espere por los hijos directos; use init para manejar las tareas de PID 1 y los descendientes adoptados.
- Reiniciar antes de reunir evidencia → el incidente desaparece sin demostrar qué límite o ruta de código falló → registre primero los PPID, límites, contadores, versión, tasa y logs cuando el margen lo permita.
Preguntas de seguimiento y respuestas
¿Qué pasa si el proceso padre no se puede reiniciar durante el horario laboral?
Primero detenga la ruta con fugas: deshabilite el tipo de trabajo, reduzca la concurrencia o enrute el trabajo a réplicas en buen estado. Si la política lo permite, aumente solo el límite efectivo de PIDs lo suficiente como para preservar la capacidad administrativa y de health-check. Monitoree la pendiente en lugar del recuento aislado y calcule un plazo conservador de agotamiento. Un padre activo puede recolectar a sus propios hijos solo si expone o puede recibir una acción de reparación soportada; un proceso externo no puede esperar por ellos. Programe una transición controlada del padre antes de que se consuma el margen temporal.
¿Qué pasa si los zombis aparecen solo dentro de un contenedor?
Entre a la vista del espacio de nombres de PID e identifique el PID 1 del espacio de nombres más los PPID de los zombis. Confirme la ruta de cgroup del servicio y los contadores de PIDs desde el host o el orquestador. Si la aplicación es PID 1 y no implementa recolección/reenvío de señales, despliegue un proceso init ligero. Si un wrapper es PID 1, verifique que no finalice antes de tiempo ni espere solo a un hijo. Pruebe la terminación del contenedor para que las señales lleguen a la aplicación, los hijos terminen y se recolecten todos los estados antes de que finalice el período de gracia.
¿Por qué una invocación del manejador de SIGCHLD no equivale a la salida de un solo hijo?
Las señales tradicionales son notificaciones, no una cola duradera de un evento por hijo. Varios hijos pueden salir antes de que el proceso procese la señal y las notificaciones pueden fusionarse o agruparse. El contrato robusto es "una notificación significa inspeccionar el estado de los hijos", seguido de llamadas repetidas de espera no bloqueante hasta que no quede ningún hijo terminado. La aplicación registra cada PID devuelto exactamente una vez.
¿Configurar SIGCHLD en SIG_IGN resolvería el problema?
En Linux, ignorar explícitamente SIGCHLD o configurar SA_NOCLDWAIT cambia el comportamiento de los zombis, pero la aplicación ya no podrá depender de recopilar estados de salida ordinarios mediante wait(). Que la disposición por defecto se describa como "ignorar" no es lo mismo que instalar explícitamente SIG_IGN. Use este modo solo cuando los resultados de los hijos realmente no tengan valor de negocio y el contrato del lenguaje/runtime esté verificado; los administradores de procesos trabajadores normalmente necesitan una contabilidad de finalización explícita.
¿Cómo alertaría antes de que los usuarios experimenten forks fallidos?
Genere alertas ante una tasa de crecimiento positiva y sostenida de zombis agrupada por padre, un margen bajo restante de PID en el cgroup, aumentos en pids.events:max y fallas al crear procesos. Combine esas señales con el rendimiento de procesamiento de trabajos (throughput) para que una ráfaga corta legítima no envíe avisos basándose únicamente en el recuento. Reserve suficiente margen para el supervisor, la telemetría y los comandos de recuperación, y luego pruebe la alerta frente a una fuga controlada en un espacio de nombres que no sea de producción.