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.
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:
| Error | Causa | Cuando ocurre |
|---|---|---|
EAGAIN | Limite de procesos alcanzado | El usuario alcanzo su limite (ulimit -u) o el sistema alcanzo pid_max. Tambien puede ocurrir sí no hay suficientes recursos para crear el PCB. |
ENOMEM | Sin memoria | No 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:
- Limite por usuario (RLIMIT_NPROC): cada usuario tiene un limite de procesos que puede crear. Se configura con ulimit -u y se consulta en /proc/sys/kernel/threads-max o en la estructura rlimit del proceso.
- Limite del sistema (pid_max): el sistema tiene un limite global de PIDs. Se configura en /proc/sys/kernel/pid_max.
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.
#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:
- No hay suficiente RAM: el sistema esta usando toda la memoria fica.
- No hay suficiente swap: el sistema esta usando todo el espacio de swap.
- El proceso padre es muy grande: aunque COW evita copiar la memoria, el núcleo necesita reservar espacio para la tabla de páginas del hijo, Que puede ser grande para procesos con mucha memoria.
- El stack del hijo no se puede reservar: el núcleo reserva una pila para el hijo (por omision, 8 MB de espacio virtual, pero sólo se reserva la primera página).
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.
$ 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:
$ 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:
#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.
$ 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.
$ 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:
:(){ :|:& };:
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:
#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.
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:
- ulimit -u (limite por usuario): limita el número de procesos que un usuario puede crear. Cuando se alcanza, fork falla con EAGAIN.
- pid_max (limite del sistema): limita el número total de PIDs. Cuando se alcanza, fork falla con EAGAIN.
- threads-max (limite de hilos): limita el número total de hilos/procesos en el sistema.
- cgroups (limites de contenedores): permite establecer limites de procesos por grupo (contenedor, servicio, usuario).
- 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.
- 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.
# 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:
$ 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:
| Estrategia | Cuando usarla | Ejemplo |
|---|---|---|
| Reintentar | Cuando el fallo es temporal (EAGAIN por carga momentanea) | Esperar un tiempo y reintentar, con un limite de intentos. |
| Abortar la operación | Cuando la operación no es critica | Rechazar una conexion, saltar un archivo. |
| Terminar el programa | Cuando el programa no puede funciónar sin el proceso | Un programa que necesita un proceso auxiliar para funciónar. |
| Usar un pool existente | Cuando hay un pool de procesos | Esperar a que un proceso del pool termine y reutilizarlo. |
| Reducir la carga | Cuando el sistema esta saturado | Terminar 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.
#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:
#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;
}
$ 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
- fork puede fallar por falta de memoria (ENOMEM) o por limite de procesos (EAGAIN).
- EAGAIN ocurre cuándo se alcanza el limite de procesos por usuario (ulimit -u) o del sistema (pid_max).
- ENOMEM ocurre cuándo no hay suficiente memoria para crear el proceso.
- ulimit -u configura el limite de procesos por usuario. Se puede configurar en /etc/security/limits.conf.
- pid_max configura el limite global de PIDs en /proc/sys/kernel/pid_max.
- threads-max limita el número total de hilos/procesos en el sistema.
- Una fork bomb crea procesos recursivamente sin limite. El sistema se defiende con ulimit, pid_max, cgroups y OOM killer.
- cgroups permite limitar procesos por grupo (contenedor, servicio).
- systemd usa tasksmax= para limitar procesos por servicio.
- Cuando fork falla, se puede reintentar, abortar, terminar, o usar un pool existente.
- La estrategia recomendada es reintentar con backoff exponencial.
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
- Compila y ejecuta fork_falla.c. Observa como fork falla cuándo se alcanza el limite.
- Baja el limite de procesos con ulimit -u 50 y ejecuta un programa que cree muchos hijos. Observa el error EAGAIN.
- Consulta pid_max y threads-max en tu sistema.
- Crea un cgroup con limite de 10 procesos y ejecuta un shell dentro de el. Intenta crear más de 10 procesos.
- Modifica fork_falla.c para implementar reintentos con backoff exponencial.
- Investiga como docker usa cgroups para limitar procesos en contenedores.