1. Qué es un proceso huerfano
Un proceso huerfano es un proceso cuyo padre terminó antes que el. Cuando el padre muere, el hijo queda "huerfano" porque ya no tiene padre. El núcleo resuelve esto adoptando al hijo: le asigna un nuevo padre (tipicamente el proceso 1, o un subreaper).
El huerfano no se entera de que fue adoptado: su PPID cambia, pero el proceso sigue ejecutandose normalmente. Solo cuándo el huerfano termina, su nuevo padre (el adoptante) lo recoge con wait().
Los huerfanos son comunes y normales en unix. Ocurren cuándo:
- Un proceso padre termina antes que sus hijos (por error o por diseno).
- Un demonio crea procesos hijos y termina (el hijo sigue como demonio).
- Un proceso se desvincula de su padre (con setsid o doble fork).
A diferencia de los zombis (Que son un problema), los huerfanos son un mecanismo normal y necesario. Son la base de los demonios y de los procesos independientes. El sistema los maneja automaticamente.
2. Por que existe la adopción
La adopción existe porque todo proceso debe tener un padre. El núcleo necesita saber a quién notificar cuándo un proceso termina (SIGCHLD) y a quién entregar el código de salida (wait). Si el padre muere, alguien debe tomar esa responsabilidad.
Sin adopción, los procesos huerfanos quedarian sin padre, y cuándo terminaran, no habria nadie para recogerlos (Quedarian como zombis permanentes). La adopción asegura que siempre hay alguien para recoger a los procesos terminados.
La adopción también es necesaria para la gestion de recursos: el padre adoptante (proceso 1 o subreaper) puede aplicar limites de recursos, registrar la terminación, etc.
3. La adopción por el proceso 1
Cuando un proceso padre termina, el núcleo recorre la lista de hijos del padre y reasigna cada uno al proceso 1 (systemd o init). Esto ocurre automaticamente, Sin intervencion del usuario.
El proceso 1 esta disenado para manejar esta responsabilidad:
- Recoge a los huerfanos que terminan: el proceso 1 llama a wait() (o equivalente) para recoger a los hijos adoptados que terminan, evitando que queden como zombis.
- No termina nunca: el proceso 1 es el único proceso que no puede terminar (Sí termina, el sistema se cae).
- Es el antepasado de todos: todos los procesos descienden del proceso 1, directa o indirectamente.
En sistemás modernos con systemd, el proceso 1 También gestiona servicios, montajes, y otras táreas del sistema. Pero su rol como "recolector de huerfanos" Sigue siendo fundamental.
4. Subreapers
Un subreaper es un proceso que toma el rol de recoger a los huerfanos que terminan, en lugar del proceso 1. Se configura con prctl(PR_SET_CHILD_SUBREAPER, 1).
Los subreapers son útiles en contenedores y en sistemás de init alternativos. En un contenedor, el proceso 1 del contenedor es el subreaper: recoge a los huerfanos del contenedor, en lugar del proceso 1 del host.
Cuando un proceso es subreaper, los hijos huerfanos (cuyo padre terminó) son adoptados por el en lugar del proceso 1. Esto permite que un contenedor gestione sus propios procesos sin depender del host.
systemd usa subreapers para los servicios: cada servicio puede tener su propio subreaper que recoge a los huerfanos del servicio.
#define _GNU_SOURCE
#include
/* Convertir este proceso en subreaper */
if (prctl(PR_SET_CHILD_SUBREAPER, 1) == -1) {
perror("prctl");
return 1;
}
/* Verificar sí este proceso es subreaper */
int is_subreaper;
prctl(PR_GET_CHILD_SUBREAPER, &is_subreaper);
printf("Es subreaper: %d\n", is_subreaper);
5. Huerfanos y zombis
Los huerfanos y los zombis son conceptos distintos pero relacionados:
| Concepto | Que es | Cuando ocurre | Quien lo recoge |
|---|---|---|---|
| Huerfano | Proceso vivo cuyo padre terminó. | El padre termina antes que el hijo. | Nadie (Sigue ejecutandose). |
| Zombi | Proceso terminado cuyo padre no lo recogio. | El hijo termina y el padre no llama a wait(). | El padre (o el adoptante). |
| Huerfano que termina | Proceso que fue huerfano y luego terminó. | El padre terminó, luego el hijo termina. | El adoptante (proceso 1 o subreaper). |
Un proceso puede ser huerfano y luego zombi: Sí el padre muere, el hijo es adoptado. Cuando el hijo termina, el adoptante lo recoge. Si el adoptante no lo recoge (porque también murio), el zombi persiste.
En la práctica, el proceso 1 y los subreapers siempre recogen a los huerfanos terminados, por lo que los zombis huerfanos son raros.
6. El problema de los demonios
Un demonio (daemon) es un proceso que se ejecuta en segúndo plano, Sin terminal de control, y que proporciona un servicio al sistema. Los demonios deben ser independientes de la sesion del usuario: Sí el usuario cierra la sesion, el demonio debe seguir ejecutandose.
Para crear un demonio, el proceso debe:
- Desvincularse de la terminal: para que no reciba senales de la terminal (SIGINT, SIGTSTP, etc.).
- Desvincularse del padre: para que no sea terminado cuándo el padre termina.
- Cambiar de directorio: tipicamente a /, para no bloquear el desmontaje de sistemás de archivos.
- Cerrar descriptores: cerrar stdin, stdout, stderr y redirigirlos a /dev/null.
- Cambiar de usuario: bajar privilegios sí se ejecuto como root.
El doble fork + setsid es la tetécnica clasica para crear un demonio. El tema 27 profundiza en demonios.
7. Crear un demonio: doble fork + setsid
El patron clasico de unix para crear un demonio es:
- Primer fork: el padre crea un hijo y termina. El hijo queda huerfano y es adoptado por el proceso 1.
- setsid(): el hijo crea una nueva sesion y se convierte en lider de sesion. Esto lo desvincula de la terminal de control.
- Segundo fork: el hijo (ahora lider de sesion) crea un nieto y termina. El nieto no es lider de sesion (no puede adquirir una terminal de control).
- El nieto es el demonio: se desvincula de la terminal, cambia de directorio, cierra descriptores, y ejecuta el servicio.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
int main(void)
{
/* Primer fork */
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid > 0) _exit(0); /* padre termina */
/* Crear nueva sesion */
if (setsid() < 0) { perror("setsid"); return 1; }
/* Segundo fork */
pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid > 0) _exit(0); /* hijo termina */
/* Nieto: el demonio */
/* Cambiar de directorio */
chdir("/");
/* Cerrar descriptores estandar */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
/* Redirigir a /dev/null */
open("/dev/null", O_RDONLY); /* stdin */
open("/dev/null", O_WRONLY); /* stdout */
open("/dev/null", O_WRONLY); /* stderr */
/* Bajar privilegios (Sí se ejecuto como root) */
/* setuid(getuid()); */
/* El demonio hace su trabajo */
while (1) {
/* servicio... */
sleep(10);
}
return 0;
}
El doble fork + setsid es la tetécnica clasica. El tema 27 profundiza en demonios y grupos/Sesiones.
setsid() crea una nueva sesion y convierte al proceso en lider de sesion. Esto lo desvincula de la terminal de control: el proceso ya no recibe senales de la terminal. El segúndo fork asegura que el demonio no sea lider de sesion (y por lo tanto no pueda adquirir una terminal de control).
8. Huerfanos en sistemás reales
Los huerfanos son comunes en sistemás reales. Ejemplos:
- Demonios: un demonio se crea con doble fork, y el proceso final es huerfano (adoptado por el proceso 1).
- Servidores web: Apache crea procesos hijos para atender conexiones. Si el proceso padre se reinicia, los hijos quedan huerfanos y son adoptados.
- Scripts de shell: Cuándo un script lanza un comando en segúndo plano y termina, el comando queda huerfano.
- Contenedores: en un contenedor, el proceso 1 del contenedor es el subreaper. Los procesos huerfanos son adoptados por el.
En todos estos casos, la adopción por el proceso 1 (o subreaper) asegura que los huerfanos se recogen cuándo terminan, evitando zombis.
9. Observar huerfanos en /proc
Cuando un proceso se convierte en huerfano, su PPID cambia al PID del adoptante (1 o subreaper). Se puede observar en /proc:
$ grep PPid /proc/101/status # antes de la adopción
PPid: 100
$ grep PPid /proc/101/status # Después de la adopción
PPid: 1
El cambio de PPID es la evidencia de la adopción. El proceso sigue ejecutandose, pero ahora su padre es el proceso 1.
Tambien se puede observar con pstree: el proceso huerfano aparece como hijo del proceso 1 (o del subreaper).
10. Un programa que crea huerfanos
El siguiente programa crea un huerfano y demuestra la adopción:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
int main(void)
{
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
/* Hijo: imprimir PPID antes y después */
printf("Hijo (PID %d): PPID inicial = %d\n", (int) getpid(), (int) getppid());
sleep(3); /* dar tiempo al padre para terminar */
printf("Hijo (PID %d): PPID Después = %d (adoptado)\n", (int) getpid(), (int) getppid());
sleep(2);
printf("Hijo terminando\n");
_exit(0);
}
/* Padre: terminar inmediatamente */
printf("Padre (PID %d) terminando\n", (int) getpid());
return 0; /* el hijo queda huerfano */
}
$ gcc -std=c17 -Wall -Wextra -o crear_huerfano crear_huerfano.c
$ ./crear_huerfano
Padre (PID 1070) terminando
Hijo (PID 1071): PPID inicial = 1070
Hijo (PID 1071): PPID Después = 1 (adoptado)
Hijo terminando
El programa demuestra la adopción: el hijo imprime su PPID antes (1070, el padre) y después (1, el proceso adoptante). El proceso 1 recoge al hijo cuándo termina.
11. Resumen
- Un huerfano es un proceso vivo cuyo padre terminó. El núcleo lo adopta asignandole un nuevo padre.
- El adoptante por omision es el proceso 1 (systemd/init).
- Los subreapers permiten que un proceso gestione sus propios huerfanos.
- Los huerfanos son la base de los demonios y los procesos independientes.
- El doble fork + setsid es la tetécnica clasica para crear demonios.
- El proceso adoptante recoge a los huerfanos terminados, evitando zombis.
- En contenedores, el proceso 1 del contenedor es el subreaper.
12. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Qué es un proceso huerfano?
Un proceso vivo cuyo padre terminó. El núcleo lo adopta asignandole un nuevo padre (tipicamente el proceso 1).
2. Por que existe la adopción?
Porque todo proceso debe tener un padre para que alguien lo recoga cuándo termine. Sin adopción, los huerfanos quedarian sin padre y se convertirian en zombis permanentes.
3. Quien adopta a los huerfanos?
El proceso 1 (systemd/init) por omision, o un subreaper sí hay uno configurado.
4. Qué es un subreaper?
Un proceso que toma el rol de recoger a los huerfanos que terminan, en lugar del proceso 1. Se configura con prctl(PR_SET_CHILD_SUBREAPER, 1).
5. Por que los huerfanos son útiles para demonios?
Porque un demonio debe ser independiente de la sesion del usuario. Al quedar huerfano y ser adoptado por el proceso 1, el demonio se desvincula del padre y puede seguir ejecutandose indefinidamente.
6. Qué es setsid?
Una syscall que crea una nueva sesion y convierte al proceso en lider de sesion. Lo desvincula de la terminal de control.
7. Por que el doble fork en la creacion de demonios?
El primer fork desvincula del padre. setsid crea una nueva sesion. El segúndo fork asegura que el demonio no sea lider de sesion (y por lo tanto no pueda adquirir una terminal de control).
8. Cómo se observa la adopción en /proc?
El PPID del proceso cambia al PID del adoptante (1 o subreaper). Se puede verificar con grep PPid /proc/PID/status.
9. Qué pasa cuándo un huerfano termina?
El adoptante (proceso 1 o subreaper) lo recoge con wait(), evitando que quede como zombi.
10. Por que los subreapers son importantes en contenedores?
Porque en un contenedor, el proceso 1 del contenedor es el subreaper. Los procesos huerfanos del contenedor son adoptados por el, no por el proceso 1 del host.
Para prácticar
- Compila y ejecuta crear_huerfano.c. Observa que el PPID del hijo cambia de 1070 a 1.
- Compila y ejecuta crear_demonio.c. Observa que el proceso se desvincula de la terminal.
- Mientras el demonio se ejecuta, verifica con ps que su PPID es 1.
- Escribe un programa que se convierta en subreaper y adopte a huerfanos.
- Usa pstree para observar el arbol de procesos antes y después de la adopción.
- Investiga como systemd crea y gestiona demonios.