Varios hijos y arboles de procesos - Procesos e Hilos
Volver a Procesos e Hilos Bloque 3 - Crear procesos con fork - Tema 14 de 43

Tema 14

Varios hijos y arboles de procesos

Un proceso puede crear no sólo un hijo, Sino muchos. Y cada hijo puede crear sus propios hijos, formando un arbol. En este tema veras que pasa cuándo fork se llama en un bucle, Como contar cuántos procesos se crean, Como esperar a varios hijos, y como visualizar el arbol con pstree.

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.

Arbol de procesos. La raiz es el proceso 1. Cada proceso puede tener varios hijos, y cada hijo puede tener sus propios hijos. proceso 1 hijo A hijo B hijo C nieto 1 nieto 2 nieto 3
Un arbol de procesos: cada proceso puede tener varios hijos, y cada hijo puede tener sus propios hijos. El arbol 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:

fork_bucle_malo.c

#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:

fork_bucle_peor.c

#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:

CodigoProcesos creadosFormula
1 fork (Sin bucle)2 (padre + 1 hijo)2^1 = 2
fork en bucle de 3, Sólo el padre hace fork4 (1 padre + 3 hijos)1 + 3 = 4
fork en bucle de 3, todos hacen fork8 (2^3)2^3 = 8
fork en bucle de n, todos hacen fork2^n2^n
fork anidado: fork dentro de forkDepende de la estructuraProducto 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 clasica

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.

patron_fork_bucle.c

#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:

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.

El pool de procesos es el patron de los servidores

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:

terminal - arbol de un servidor web

$ 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:

terminal - arbol de una shell

$ 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:

terminal - pstree completo

$ 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:

esperar_varios.c

#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_orden.c

    /* 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:

Los zombis se acumulan

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:

arbol_procesos.c

#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;
}
terminal - ejecutar arbol_procesos

$ 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:

terminal - 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

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

  1. Compila y ejecuta patron_fork_bucle.c. Observa que el padre espera a todos los hijos y recoge sus códigos de salida.
  2. Compila y ejecuta arbol_procesos.c. Mientras se ejecuta, observa el arbol con pstree -p.
  3. Compila y ejecuta fork_bucle_peor.c. Observa cuántos procesos se crean (deberian ser 8). Matalos con pkill.
  4. Escribe un programa que cree 10 hijos y los espere en orden inverso de creacion.
  5. Usa pstree -p para visualizar el arbol de procesos de tu sistema.
  6. Escribe un programa que cree un arbol de 3 niveles: cada proceso crea 2 hijos, hasta 3 niveles de profundidad.