Cuando fork falla - Procesos e Hilos
Volver a Procesos e Hilos Bloque 3 - Crear procesos con fork - Tema 15 de 43

Tema 15

Cuando fork falla

fork no siempre tiene exito. Puede fallar por falta de memoria, por limite de procesos por usuario, o por limite del sistema. En este tema veras las causas de fallo, los errores que devuelve, Como se defiende el sistema de una fork bomb, y que debe hacer un programa cuándo fork falla.

1. Qué significa que fork falle

Cuando fork() no puede crear un proceso nuevo, devuelve -1 y establece errno con el código de error. No se crea ningun hijo. El proceso padre continua ejecutandose como sí fork no se hubiera llamado (excepto que errno se modifico).

Fork puede fallar por varias razones, pero todas se reducen a dos: falta de memoria (no hay suficiente RAM o espacio de swap para crear el proceso) o limite de procesos (se alcanzo el maximo de procesos permitidos para el usuario o para el sistema).

El fallo de fork es una condicion que todo programa robusto debe manejar. Ignorar el valor de retorno de fork y asumir que siempre tiene exito es un error que puede causar comportamientos inesperados o vulnerabilidades de seguridad.

fork puede fallar en cualquier momento

Aunque en un sistema normal con pocos procesos fork casí nunca falla, en un sistema bajo carga (muchos procesos, poca memoria) puede fallar. Un programa que no maneja el fallo de fork puede comportarse de forma impredecible: ejecutar código del hijo en el padre, o no crear procesos que necesita.

2. Las causas de fallo

fork puede fallar por dos causas principales, cada una con su código de error:

ErrorCausaCuando ocurre
EAGAINLimite de procesos alcanzadoEl usuario alcanzo su limite (ulimit -u) o el sistema alcanzo pid_max. Tambien puede ocurrir sí no hay suficientes recursos para crear el PCB.
ENOMEMSin memoriaNo hay suficiente memoria para crear el proceso (tabla de páginas, PCB, stack). Tambien puede ocurrir sí no hay espacio de swap.

EAGAIN es el error más comun en la práctica: ocurre cuándo un usuario o el sistema tiene demasiados procesos. ENOMEM es menos comun pero puede ocurrir en sistemás con poca memoria o con procesos muy grandes.

Hay también otros errores menos comunes, Como EPERM (Sin permiso para crear procesos con ciertas propiedades) o ENOSYS (fork no esta implementado en este sistema, aunque es raro en linux).

3. EAGAIN: limite de procesos

EAGAIN Significa que el sistema no puede crear más procesos porque se alcanzo un limite. Hay dos limites que pueden causar EAGAIN:

Cuando fork falla por EAGAIN, el programa debe decidir que hacer: reintentar después de un tiempo, abortar la operación, o terminar. La decision depende del contexto: un servidor que no puede crear un proceso para atender una conexion puede rechazar la conexion; un programa que necesita un proceso para funciónar puede terminar con error.

manejo_eagain.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 
#include 

int main(void)
{
    pid_t pid;
    int intentos = 0;

    while (intentos < 3) {
        pid = fork();
        if (pid >= 0) break;  /* exito */

        if (errno == EAGAIN) {
            printf("Intento %d: EAGAIN, reintentando...\n", intentos + 1);
            sleep(1);
            intentos++;
        } else {
            perror("fork");
            return 1;
        }
    }

    if (pid < 0) {
        fprintf(stderr, "No se pudo crear el proceso\n");
        return 1;
    }

    if (pid == 0) {
        printf("Hijo creado exitosamente\n");
        _exit(0);
    }

    wait(NULL);
    printf("Hijo terminado\n");
    return 0;
}

4. ENOMEM: Sin memoria

ENOMEM Significa que el núcleo no pudo reservar la memoria necesaria para crear el proceso. Esto puede ocurrir porque:

ENOMEM es menos comun que EAGAIN en la práctica, porque los sistemás suelen tener suficiente memoria virtual (RAM + swap). Pero puede ocurrir en sistemás con poca memoria o con procesos muy grandes.

Cuando fork falla por ENOMEM, reintentar después de un tiempo puede funciónar (Sí se libera memoria), pero no es garantizado. Lo más seguro es abortar la operación o terminar el programa.

5. ulimit -u: limite por usuario

El comando ulimit -u muestra el número maximo de procesos que un usuario puede tener a la vez. Este limite se aplica por usuario, no por proceso: todos los procesos de un usuario comparten el mismo limite.

terminal - ulimit -u

$ ulimit -u
31263
$ ulimit -u 100  # bajar el limite a 100 (Sólo root puede subirlo)
$ ulimit -u
100

El limite se configura en el archivo /etc/security/limits.conf (o en archivos de /etc/security/limits.d/) para que se aplique al iniciar sesion:

terminal - configurar limites en limits.conf

$ cat /etc/security/limits.conf
#    
ana    soft    nproc    1000
ana    hard    nproc    2000
@users soft    nproc    500
@users hard    nproc    1000

El limite soft es el que se aplica por omision y puede ser cambiado por el usuario hasta el hard. El limite hard sólo puede ser cambiado por root. Esto permite que el administrador establezca un techo que los usuarios no pueden superar.

En C, el limite se consulta y modifica con getrlimit() y setrlimit() con RLIMIT_NPROC:

C - consultar limite de procesos

#define _POSIX_C_SOURCE 200809L
#include 
#include 

int main(void)
{
    struct rlimit rl;
    if (getrlimit(RLIMIT_NPROC, &rl) == 0) {
        printf("NPROC: soft=%lu hard=%lu\n",
               (unsigned long) rl.rlim_cur,
               (unsigned long) rl.rlim_max);
    }
    return 0;
}

6. pid_max: limite del sistema

El sistema tiene un limite global de PIDs, configurado en /proc/sys/kernel/pid_max. Cuando el núcleo asigna PIDs, Sí llega a este limite, vuelve a empezar por el número más bajo libre. Si todos los números estan en uso, fork falla con EAGAIN.

terminal - pid_max

$ cat /proc/sys/kernel/pid_max
4194304
$ echo 100000 | sudo tee /proc/sys/kernel/pid_max
100000

El valor por omision es 4194304 (2^22) en sistemás de 64 bits y 32768 en sistemás de 32 bits. El valor maximo es 2^22 (4194304) en 64 bits. No se puede superar este limite.

Hay también un limite relacionado: /proc/sys/kernel/threads-max, Que limita el número total de hilos (y procesos, ya que los hilos son procesos ligeros en linux) en el sistema. Si se alcanza, fork falla con EAGAIN.

terminal - threads-max

$ cat /proc/sys/kernel/threads-max
125682

7. La fork bomb

Una fork bomb es un programa que crea procesos recursivamente sin limite, agotando los recursos del sistema. El ejemplo clasico en bash es:

terminal - fork bomb en bash

:(){ :|:& };:

Esta linea define una función llamada : Que se llama a sí misma dos veces (una en primer plano, una en segúndo plano) y se ejecuta. Cada proceso crea dos más, y el número de procesos crece exponencialmente. En segúndos, el sistema se satura y deja de responder.

En C, el equivalente es:

fork_bomb.c

#include 

int main(void)
{
    while (1) {
        fork();  /* crea un proceso en cada iteracion */
    }
    return 0;
}

Este programa crea procesos sin limite. En un sistema sin protección, agotaria los recursos en segúndos. Pero los sistemás modernos tienen varias defensas.

No ejecutes una fork bomb

Una fork bomb puede hacer que el sistema deje de responder, y puede requerir un reinicio. Si quieres experimentar, hazlo en una maquína virtual o en un contenedor con limites estrictos. Nunca en una maquína de produccion.

8. Cómo se defiende el sistema

El sistema tiene varias capas de defensa contra una fork bomb:

  1. ulimit -u (limite por usuario): limita el número de procesos que un usuario puede crear. Cuando se alcanza, fork falla con EAGAIN.
  2. pid_max (limite del sistema): limita el número total de PIDs. Cuando se alcanza, fork falla con EAGAIN.
  3. threads-max (limite de hilos): limita el número total de hilos/procesos en el sistema.
  4. cgroups (limites de contenedores): permite establecer limites de procesos por grupo (contenedor, servicio, usuario).
  5. OOM killer: Sí el sistema se queda sin memoria, el OOM killer mata procesos para liberar memoria. Prioriza procesos que usan mucha memoria y tienen bajo oom_score_adj.
  6. systemd: systemd puede limitar el número de procesos por servicio con tasksmax= en la unidad de servicio.

Con estas defensas, una fork bomb tipica sólo afecta al usuario que la lanzo (por ulimit -u) o al contenedor (por cgroups), y no al sistema completo. El administrador puede matar los procesos del usuario o reiniciar el contenedor.

9. cgroups y limites de procesos

Los cgroups (control groups) son un mecanismo del núcleo que permite agrupar procesos y aplicarles limites de recursos. En la version 2 (cgroups v2), se puede limitar el número de procesos de un grupo con el controlador pids.

terminal - usar cgroups para limitar procesos

# Crear un cgroup
$ mkdir /sys/fs/cgroup/mi_grupo

# Limitar a 50 procesos
$ echo 50 > /sys/fs/cgroup/mi_grupo/pids.max

# Mover el shell actual al cgroup
$ echo $$ > /sys/fs/cgroup/mi_grupo/cgroup.procs

# Ahora este shell y sus hijos no pueden crear más de 50 procesos
$ for i in $(seq 1 100); do (sleep 100 &) ; done
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable

Los cgroups son la base de los contenedores (Docker, Podman, LXC). Cada contenedor tiene su propio cgroup con limites de procesos, Memoria, CPU, etc. Esto aisla a los contenedores entre sí y del sistema host.

systemd también usa cgroups para los servicios. Con Tasksmax= en la unidad de servicio, se puede limitar el número de procesos de un servicio:

terminal - limitar procesos con systemd

$ systemctl show --property=Tasksmax sshd
Tasksmax=512
$ cat /etc/systemd/system/mi_servicio.service
[Service]
Tasksmax=100
Execstart=/usr/bin/mi_servicio

10. Qué hacer cuándo fork falla

Cuando fork falla, el programa debe tomar una decision. Las opciones son:

EstrategiaCuando usarlaEjemplo
ReintentarCuando el fallo es temporal (EAGAIN por carga momentanea)Esperar un tiempo y reintentar, con un limite de intentos.
Abortar la operaciónCuando la operación no es criticaRechazar una conexion, saltar un archivo.
Terminar el programaCuando el programa no puede funciónar sin el procesoUn programa que necesita un proceso auxiliar para funciónar.
Usar un pool existenteCuando hay un pool de procesosEsperar a que un proceso del pool termine y reutilizarlo.
Reducir la cargaCuando el sistema esta saturadoTerminar procesos menos importantes, liberar memoria.

La estrategia más comun es reintentar con backoff exponencial: esperar un tiempo, reintentar, y sí falla de nuevo, esperar más tiempo. Esto da tiempo al sistema para liberar recursos.

C - reintentar con backoff exponencial

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 

pid_t fork_con_reintento(int max_intentos)
{
    for (int i = 0; i < max_intentos; i++) {
        pid_t pid = fork();
        if (pid >= 0) return pid;  /* exito */

        if (errno != EAGAIN) {
            perror("fork");
            return -1;  /* error no recuperable */
        }

        /* Backoff exponencial: 1s, 2s, 4s, ... */
        unsigned int espera = 1 << i;
        if (espera > 16) espera = 16;
        fprintf(stderr, "fork fallo (intento %d), esperando %u s\n", i + 1, espera);
        sleep(espera);
    }
    fprintf(stderr, "fork fallo después de %d intentos\n", max_intentos);
    return -1;
}

11. Un programa que demuestra fork fallando

El siguiente programa baja el limite de procesos a un valor muy bajo y luego intenta crear varios hijos, demostrando como fork falla con EAGAIN:

fork_falla.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 
#include 
#include 

int main(void)
{
    /* Bajar el limite de procesos a 5 */
    struct rlimit rl;
    if (getrlimit(RLIMIT_NPROC, &rl) == 0) {
        rl.rlim_cur = 5;
        if (setrlimit(RLIMIT_NPROC, &rl) != 0) {
            perror("setrlimit");
            /* Continuar de todas formás */
        }
    }

    printf("Limite de procesos: %lu\n", (unsigned long) rl.rlim_cur);
    printf("PID: %d\n", (int) getpid());

    for (int i = 0; i < 10; i++) {
        pid_t pid = fork();
        if (pid < 0) {
            printf("fork %d fallo: %s (errno=%d)\n", i, strerror(errno), errno);
        } else if (pid == 0) {
            sleep(5);
            _exit(0);
        } else {
            printf("fork %d exitoso: hijo PID %d\n", i, (int) pid);
        }
    }

    /* Esperar a los hijos que se crearon */
    while (wait(NULL) > 0);
    printf("Todos los hijos terminaron\n");
    return 0;
}
terminal - ejecutar fork_falla

$ gcc -std=c17 -Wall -Wextra -o fork_falla fork_falla.c
$ ./fork_falla
Limite de procesos: 5
PID: 950
fork 0 Exitoso: hijo PID 951
fork 1 Exitoso: hijo PID 952
fork 2 Exitoso: hijo PID 953
fork 3 fallo: Resource temporarily unavailable (errno=11)
fork 4 fallo: Resource temporarily unavailable (errno=11)
...
fork 9 fallo: Resource temporarily unavailable (errno=11)
Todos los hijos terminaron

El programa baja el limite a 5 procesos. El padre cuenta como 1, así que sólo puede crear 4 hijos más. Los forks 3 a 9 fallan con EAGAIN (errno 11). Esto demuestra como el sistema protege contra la creacion excesiva de procesos.

Nota: el limite de 5 puede ser demasiado bajo para que el sistema funcióne correctamente (la shell, el editor, etc. También son procesos del usuario). En la práctica, los limites son más altos (miles).

12. Resumen

13. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. Qué devuelve fork cuándo falla?

-1, y establece errno con el código de error (EAGAIN o ENOMEM).

2. Qué significa EAGAIN?

Que se alcanzo el limite de procesos: por usuario (ulimit -u) o por el sistema (pid_max).

3. Qué significa ENOMEM?

Que no hay suficiente memoria para crear el proceso (RAM, swap, o tabla de páginas).

4. Cómo se consulta el limite de procesos por usuario?

Con ulimit -u en la shell, o con getrlimit(RLIMIT_NPROC) en C.

5. Cómo se consulta el limite de PIDs del sistema?

Con cat /proc/sys/kernel/pid_max.

6. Qué es una fork bomb?

Un programa que crea procesos recursivamente sin limite, agotando los recursos del sistema.

7. Cómo se defiende el sistema de una fork bomb?

Con ulimit -u (limite por usuario), pid_max (limite del sistema), cgroups (limites por grupo), y OOM killer (mata procesos sí se queda sin memoria).

8. Qué son los cgroups?

Control groups: un mecanismo del núcleo que permite agrupar procesos y aplicarles limites de recursos (procesos, Memoria, CPU, etc.).

9. Qué es tasksmax en systemd?

Una directiva que limita el número de procesos (táreas) de un servicio gestionado por systemd.

10. Qué estrategia se recomienda cuándo fork falla?

Reintentar con backoff exponencial: esperar un tiempo, reintentar, y sí falla de nuevo, esperar más tiempo.

Para prácticar

  1. Compila y ejecuta fork_falla.c. Observa como fork falla cuándo se alcanza el limite.
  2. Baja el limite de procesos con ulimit -u 50 y ejecuta un programa que cree muchos hijos. Observa el error EAGAIN.
  3. Consulta pid_max y threads-max en tu sistema.
  4. Crea un cgroup con limite de 10 procesos y ejecuta un shell dentro de el. Intenta crear más de 10 procesos.
  5. Modifica fork_falla.c para implementar reintentos con backoff exponencial.
  6. Investiga como docker usa cgroups para limitar procesos en contenedores.