1. Qué es un arbol de procesos
Un arbol de procesos es la estructura jerarquica que forman los procesos del sistema: cada proceso (excepto el 1) tiene un padre, y un proceso puede tener cero o más hijos. La raiz del arbol es el proceso 1 (systemd o init), y todos los demás procesos descienden de el, directa o indirectamente.
El arbol de procesos no es sólo una metafora: es la estructura real que el núcleo mantiene. Cada PCB tiene un puntero al padre y una lista de hijos. El núcleo usa esta estructura para propagar senales, recoger procesos terminados, y gestionar la jerarquia de procesos.
Cuando un proceso crea un hijo con fork, el hijo se agrega como hijo del padre. Si el hijo crea su propio hijo, ese nieto se agrega como hijo del hijo, y así sucesivamente. El resultado es un arbol que puede tener cualquier profundidad.
2. fork en un bucle
Cuando fork se llama dentro de un bucle, el número de procesos crece exponencialmente. Este es uno de los errores más comunes y peligrosos con fork:
#define _POSIX_C_SOURCE 200809L
#include
#include
int main(void)
{
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
/* Hijo: no hace fork, Sólo imprime */
printf("Hijo %d (PID %d)\n", i, (int) getpid());
_exit(0);
}
/* Padre: continua el bucle */
}
return 0;
}
En este ejemplo, el padre crea 3 hijos (uno por iteracion). Cada hijo termina inmediatamente. El padre no espera a los hijos, por lo que quedan como zombis. Pero el número de procesos es sólo 4 (1 padre + 3 hijos).
Pero sí el hijo También hace fork, el número crece exponencialmente:
#define _POSIX_C_SOURCE 200809L
#include
#include
int main(void)
{
for (int i = 0; i < 3; i++) {
fork(); /* Sin comprobar, Sin _exit: TODOS hacen fork */
}
printf("PID %d\n", (int) getpid());
sleep(10);
return 0;
}
Este código crea 8 procesos (2^3), porque cada proceso (padre e hijos) ejecuta el fork en cada iteracion. Es la clasica "fork bomb" en miniatura. Si el número fuera mayor (por ejemplo, 20), crearia más de un millon de procesos.
3. Cuantos procesos se crean
La formula general para contar procesos en un bucle de fork es:
| Codigo | Procesos creados | Formula |
|---|---|---|
| 1 fork (Sin bucle) | 2 (padre + 1 hijo) | 2^1 = 2 |
| fork en bucle de 3, Sólo el padre hace fork | 4 (1 padre + 3 hijos) | 1 + 3 = 4 |
| fork en bucle de 3, todos hacen fork | 8 (2^3) | 2^3 = 8 |
| fork en bucle de n, todos hacen fork | 2^n | 2^n |
| fork anidado: fork dentro de fork | Depende de la estructura | Producto de los niveles |
La regla para el caso "todos hacen fork": Sí hay n iteraciones del bucle, se crean 2^n procesos. Esto es porque en cada iteracion, cada proceso existente se duplica.
Para evitar la explosion exponencial, hay que asegurarse de que sólo el padre haga fork, y que los hijos terminen inmediatamente (con _exit) o hagan algo que no incluya fork.
La fork bomb más famosa es :(){ :|:& };: en bash, Que define una función que se llama a sí misma dos veces en segúndo plano, recursivamente. En C, el equivalente es un bucle de fork dónde todos los procesos hacen fork. Siempre hay que limitar el número de hijos y comprobar el valor de retorno de fork.
4. El patron fork en bucle
El patron correcto para crear varios hijos es: el padre hace fork en un bucle, y cada hijo hace su trabajo y termina. El padre no hace fork en los hijos.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
int main(void)
{
int n_hijos = 5;
pid_t pids[5];
for (int i = 0; i < n_hijos; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork");
return 1;
}
if (pids[i] == 0) {
/* Hijo i: hacer trabajo */
printf("Hijo %d (PID %d) trabajando...\n", i, (int) getpid());
sleep(1);
printf("Hijo %d terminando\n", i);
_exit(i + 1); /* Código de salida = i+1 */
}
/* Padre: Continuar el bucle */
}
/* Padre: esperar a todos los hijos */
for (int i = 0; i < n_hijos; i++) {
int status;
pid_t w = waitpid(pids[i], &status, 0);
if (w == -1) { perror("waitpid"); continue; }
if (WIFEXITED(status))
printf("Padre: hijo %d (PID %d) Terminó con código %d\n",
i, (int) w, WEXITSTATUS(status));
}
printf("Padre: todos los hijos terminaron\n");
return 0;
}
Este patron es el estandar para crear varios hijos: un bucle de fork, Dónde cada hijo hace su trabajo y termina, y el padre espera a todos con waitpid.
5. Controlar el número de hijos
En la práctica, el número de hijos no es fijo: depende de la carga de trabajo, de los recursos disponibles, o de la configuración. Hay varios patrones para controlar el número de hijos:
- Numero fijo: crear N hijos al inicio y esperar a que todos terminen. Simple, pero no se adapta a la carga.
- Pool de procesos: crear N hijos al inicio, y cuándo uno termina, crear otro para reemplazarlo. Se mantiene un número constante de hijos activos.
- Un hijo por tárea: crear un hijo por cada tárea (conexion, archivo, etc.) y esperar a que termine. flexible, pero puede crear demasiados procesos.
- Limitar con semaforos: usar un semaforo para limitar el número de hijos concurrentes. Mas complejo, pero más controlado.
El patron de pool de procesos es el más usado en servidores: se crean N procesos trabajadores al inicio, y se les asignan táreas a medida que llegan. Cuando un trabajador termina una tárea, vuelve a esperar otra. Esto evita el overhead de crear y destruir procesos para cada tárea.
Apache (en modo prefork), PostgreSQL, y muchos otros servidores usan un pool de procesos: crean N hijos al inicio, y cada hijo atiende conexiones en un bucle. Cuando un hijo termina (por error o por limite de conexiones), el padre crea otro para reemplazarlo. Esto mantiene un número constante de procesos activos.
6. Arboles de procesos reales
En un sistema real, los arboles de procesos son más complejos. Un servidor web tipico tiene esta estructura:
$ pstree -p | grep -A5 apache
apache2(1000)-+-apache2(1001)
|-apache2(1002)
|-apache2(1003)
|-apache2(1004)
`-apache2(1005)
El proceso padre (1000) es el que escucha en el puerto 80. Cuando llega una conexion, uno de los hijos (1001-1005) la atiende. Si un hijo termina (por error o por limite de conexiones), el padre crea otro para reemplazarlo.
Otro ejemplo: una shell con varios comandos en segúndo plano:
$ pstree -p | grep bash
bash(402)-+-sleep(500)
|-gcc(501)
|-vim(502)
`-bash(503)---python3(504)
La shell (402) tiene 4 hijos: sleep, gcc, vim y otra shell (503), Que a su vez tiene un hijo (python3). Cada comando que se lanza desde la shell se convierte en un hijo.
7. pstree para visualizar arboles
pstree es la herramienta ideal para visualizar arboles de procesos. Las opciones más útiles son:
-p: muestra el PID entre parentesis.-u: muestra el usuario cuándo difiere del padre.-a: muestra la linea de comandos completa.-h: resalta el proceso actual y sus ancestros.-s PID: muestra sólo los ancestros de un proceso.-T: oculta los hilos, muestra sólo procesos.
$ pstree -p
systemd(1)-+-cron(151)
|-dbus-daemon(152)
|-login(332)---bash(402)-+-sleep(500)
| |-gcc(501)
| `-servidor(502)-+-worker(503)
| |-worker(504)
| `-worker(505)
`-systemd-journal(44)
La indentacion muestra la jerarquia: cada proceso cuelga de su padre. Los nombres entre llaves (Como {rsyslogd}) son hilos, no procesos.
8. Esperar a varios hijos
Cuando un proceso crea varios hijos, necesita esperar a todos. Hay dos formas:
- waitpid() para cada hijo: esperar a cada hijo por su PID. Garantiza que se recogen todos, en el orden que se especifique.
- wait() en un bucle: esperar a cualquier hijo. Mas simple, pero no se sabe que hijo terminó.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
int main(void)
{
int n = 5;
pid_t pids[5];
for (int i = 0; i < n; i++) {
pids[i] = fork();
if (pids[i] < 0) { perror("fork"); return 1; }
if (pids[i] == 0) {
sleep(1 + i); /* cada hijo duerme distinto tiempo */
_exit(i + 1);
}
}
/* Esperar a cualquier hijo, en cualquier orden */
int restantes = n;
while (restantes > 0) {
int status;
pid_t w = wait(&status);
if (w == -1) { perror("wait"); break; }
if (WIFEXITED(status))
printf("Hijo %d terminó con código %d\n", (int) w, WEXITSTATUS(status));
restantes--;
}
printf("Todos los hijos terminaron\n");
return 0;
}
Con wait() en un bucle, los hijos se recogen en el orden en que terminan, no en el orden en que se crearon. El hijo que duerme menos termina primero y se recoge primero.
Si se quiere recoger en un orden especÃfico, se usa waitpid() para cada hijo:
/* Esperar en orden de creacion */
for (int i = 0; i < n; i++) {
int status;
waitpid(pids[i], &status, 0);
if (WIFEXITED(status))
printf("Hijo %d (PID %d) Terminó con código %d\n",
i, (int) pids[i], WEXITSTATUS(status));
}
9. Evitar zombis con varios hijos
Cuando un proceso crea varios hijos y no los espera, todos quedan como zombis. Si son muchos, se agota el limite de procesos. Hay varias estrategias para evitarlo:
- Esperar a todos los hijos: el padre llama a wait() o waitpid() para cada hijo. Es la forma más simple y la más segura.
- Usar SIGCHLD con WNOHANG: el padre instala un manejador de SIGCHLD Que recoge a los hijos terminados sin bloquearse. El tema 25 profundiza en esta tetécnica.
- Ignorar SIGCHLD: en linux, Sí el padre ignora SIGCHLD (con signal(SIGCHLD, SIG_IGN)), los hijos terminados se recogen automaticamente sin quedar como zombis.
- Doble fork: el padre crea un hijo que a su vez crea un nieto y termina. El nieto queda huerfano y es adoptado por el proceso 1, Que lo recoge automaticamente. El tema 25 profundiza en esta tetécnica.
Si un proceso crea hijos y no los espera, cada hijo que termina queda como zombi. Si crea 100 hijos por segúndo, en un minuto hay 6000 zombis. El limite de procesos (ulimit -u) se agota y el sistema no puede crear más procesos. Siempre hay que recoger a los hijos.
10. Un programa que crea varios hijos
El siguiente programa crea un arbol de procesos de dos niveles: el padre crea 3 hijos, y cada hijo crea 2 nietos. Demuestra como se forman arboles de procesos:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
int main(void)
{
printf("Raiz: PID %d\n", (int) getpid());
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
/* Hijo i */
printf(" Hijo %d: PID %d, padre %d\n", i, (int) getpid(), (int) getppid());
for (int j = 0; j < 2; j++) {
pid_t nieto = fork();
if (nieto < 0) { perror("fork"); _exit(1); }
if (nieto == 0) {
printf(" Nieto %d-%d: PID %d, padre %d\n",
i, j, (int) getpid(), (int) getppid());
sleep(2);
_exit(0);
}
}
/* Hijo espera a sus nietos */
while (wait(NULL) > 0);
_exit(0);
}
}
/* Padre espera a todos los hijos */
while (wait(NULL) > 0);
printf("Raiz: todos los hijos terminaron\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o arbol_procesos arbol_procesos.c
$ ./arbol_procesos
Raiz: PID 930
Hijo 0: PID 931, padre 930
Hijo 1: PID 932, padre 930
Hijo 2: PID 933, padre 930
Nieto 0-0: PID 934, padre 931
Nieto 0-1: PID 935, padre 931
Nieto 1-0: PID 936, padre 932
Nieto 1-1: PID 937, padre 932
Nieto 2-0: PID 938, padre 933
Nieto 2-1: PID 939, padre 933
Raiz: todos los hijos terminaron
El programa crea 3 hijos, cada uno crea 2 nietos. Total: 1 + 3 + 6 = 10 procesos. Mientras se ejecuta, puedes observar el arbol con pstree:
$ pstree -p 930
arbol_procesos(930)-+-arbol_procesos(931)-+-arbol_procesos(934)
| `-arbol_procesos(935)
|-arbol_procesos(932)-+-arbol_procesos(936)
| `-arbol_procesos(937)
`-arbol_procesos(933)-+-arbol_procesos(938)
`-arbol_procesos(939)
El arbol muestra claramente la jerarquia: 3 hijos, cada uno con 2 nietos. pstree es la herramienta ideal para visualizar arboles de procesos.
11. Resumen
- Los procesos forman un arbol: cada proceso (excepto el 1) tiene un padre, y puede tener varios hijos.
- fork en un bucle puede crear una explosion exponencial de procesos (2^n) Sí todos los procesos hacen fork.
- El patron correcto es: Sólo el padre hace fork, y cada hijo hace su trabajo y termina.
- Para esperar a varios hijos se usa wait() en un bucle o waitpid() para cada hijo.
- Los zombis se acumulan sí no se recogen los hijos; usar wait, SIGCHLD, o ignorar SIGCHLD.
- pstree visualiza el arbol de procesos del sistema.
- Los servidores usan un pool de procesos: N hijos creados al inicio, reemplazados cuándo terminan.
- Un arbol puede tener cualquier profundidad y cualquier número de ramás.
12. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Qué es un arbol de procesos?
Es la estructura jerarquica que forman los procesos del sistema: cada proceso (excepto el 1) tiene un padre, y un proceso puede tener cero o más hijos. La raiz es el proceso 1.
2. Cuantos procesos se crean con fork en un bucle de n iteraciones sí todos los procesos hacen fork?
2^n procesos. Porque en cada iteracion, cada proceso existente se duplica.
3. Cómo se evita la explosion exponencial?
Asegurandose de que sólo el padre haga fork, y que los hijos terminen inmediatamente (con _exit) o hagan algo que no incluya fork.
4. Cómo se espera a varios hijos?
Con wait() en un bucle (recoge en orden de terminación) o con waitpid() para cada hijo (recoge en orden de creacion).
5. Qué es una fork bomb?
Un programa que crea procesos recursivamente sin limite, agotando los recursos del sistema. El ejemplo clasico es :(){ :|:& };: en bash.
6. Qué es un pool de procesos?
Un conjunto de procesos creados al inicio que se reutilizan para atender táreas. Cuando un proceso termina una tárea, vuelve a esperar otra. Es el patron de los servidores.
7. Cómo se visualiza un arbol de procesos?
Con pstree, Que dibuja la jerarquia de procesos. La opcion -p muestra los PIDs.
8. Qué pasa sí un proceso crea hijos y no los espera?
Los hijos quedan como zombis cuándo terminan. Si son muchos, se agota el limite de procesos.
9. Cómo se puede evitar que los hijos queden como zombis sin esperarlos?
Ignorando SIGCHLD (Signal(SIGCHLD, SIG_IGN)) en linux, Que hace que los hijos terminados se recogen automaticamente.
10. Qué es un subreaper?
Un proceso que toma el rol de recoger a los procesos huerfanos que terminan, en lugar del proceso 1. Se configura con prctl(PR_SET_CHILD_SUBREAPER, 1).
Para prácticar
- Compila y ejecuta patron_fork_bucle.c. Observa que el padre espera a todos los hijos y recoge sus códigos de salida.
- Compila y ejecuta arbol_procesos.c. Mientras se ejecuta, observa el arbol con pstree -p.
- Compila y ejecuta fork_bucle_peor.c. Observa cuántos procesos se crean (deberian ser 8). Matalos con pkill.
- Escribe un programa que cree 10 hijos y los espere en orden inverso de creacion.
- Usa pstree -p para visualizar el arbol de procesos de tu sistema.
- Escribe un programa que cree un arbol de 3 niveles: cada proceso crea 2 hijos, hasta 3 niveles de profundidad.