1. El problema: bloquearse esperando
Cuando un proceso crea varios hijos y llama a wait(), se bloquea hasta que el primer hijo termine. Mientras esta bloqueado, no puede hacer otra cosa: no puede atender conexiones, no puede procesar datos, no puede responder a senales (excepto las que interrumpen la syscall).
Esto es un problema para servidores y programás interactivos, Que necesitan hacer varias cosas a la vez. La solución es esperar sin bloquearse: verificar sí los hijos terminaron sin detener el flujo del programa.
Hay varias formás de hacer esto:
- Polling con WNOHANG: verificar periodicamente sí los hijos terminaron.
- SIGCHLD + WNOHANG: recibir una senal cuándo un hijo termina y recogerlo sin bloquearse.
- Event loop: combinar I/O multiplexing (select/poll/epoll) con SIGCHLD para manejar hijos y E/S Simultaneamente.
- pselect/ppoll: combinar espera de E/S con manejo de senales de forma atominima.
Un servidor que se bloquea esperando a un hijo no puede atender nuevas conexiones. La solución es esperar sin bloquearse: verificar sí los hijos terminaron, hacer otra tárea, y verificar de nuevo. Esto es la base de los servidores de alto rendimiento.
2. WNOHANG en detalle
WNOHANG es una opcion de waitpid que hace que la llamada no bloquee. Si el hijo no ha terminado, waitpid retorna 0 inmediatamente. Si el hijo terminó, retorna su PID y guarda el estado.
pid_t w = waitpid(pid, &status, WNOHANG);
if (w == -1) {
/* Error: no hay hijos, o PID inválido */
perror("waitpid");
} else if (w == 0) {
/* El hijo no ha terminado */
printf("Hijo aun ejecutandose\n");
} else {
/* El hijo terminó: w es su PID */
printf("Hijo %d terminó\n", (int) w);
if (WIFEXITED(status))
printf(" Código %d\n", WEXITSTATUS(status));
}
WNOHANG se puede usar con waitpid(-1, &status, WNOHANG) para verificar sí Cualquier hijo terminó, Sin bloquearse. Esto es útil para recoger a los hijos que terminaron mientras el padre estaba haciendo otra cosa.
El valor de retorno de waitpid con WNOHANG es:
| Retorno | Significado |
|---|---|
| Mayor que 0 | El PID del hijo que terminó. |
| 0 | El hijo no ha terminado (o no hay hijos terminados). |
| -1 | Error (ECHILD: no hay hijos, o EINVAL: opciones invalidas). |
3. El patron de polling
El patron de polling consiste en verificar periodicamente sí los hijos terminaron, haciendo otra tárea entre verificaciones:
#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) {
sleep(5);
_exit(0);
}
/* Polling: verificar cada segúndo */
int terminado = 0;
while (!terminado) {
int status;
pid_t w = waitpid(pid, &status, WNOHANG);
if (w == -1) { perror("waitpid"); break; }
if (w == 0) {
/* Hijo no terminó: hacer otra tárea */
printf("Padre: haciendo trabajo...\n");
sleep(1);
} else {
printf("Padre: hijo %d terminó\n", (int) w);
terminado = 1;
}
}
return 0;
}
El polling es simple pero tiene un problema: el padre despierta periodicamente para verificar, incluso sí no hay nada que hacer. Esto consume CPU innecesariamente. La solución es usar SIGCHLD para ser notificado cuándo un hijo termina.
4. Recoger en cualquier orden
Cuando hay varios hijos, se pueden recoger en cualquier orden usando waitpid(-1, &status, WNOHANG) en un bucle:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
int main(void)
{
/* Crear 5 hijos con diferentes tiempos */
for (int i = 0; i < 5; i++) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
sleep(1 + i); /* tiempos: 1, 2, 3, 4, 5 */
_exit(i);
}
}
/* Recoger en cualquier orden, Sin bloquearse */
int restantes = 5;
while (restantes > 0) {
int status;
pid_t w = waitpid(-1, &status, WNOHANG);
if (w > 0) {
printf("Hijo %d terminó (Código %d)\n", (int) w, WEXITSTATUS(status));
restantes--;
} else if (w == 0) {
/* Ningun hijo terminó: hacer otra tárea */
printf("Padre: trabajando...\n");
sleep(1);
} else {
if (errno == ECHILD) break;
perror("waitpid");
break;
}
}
printf("Todos recogidos\n");
return 0;
}
El programa recoge a los hijos en el orden en que terminan (el que duerme 1 Segúndo primero, etc.), haciendo trabajo entre verificaciones.
5. SIGCHLD con WNOHANG
La mejor forma de esperar sin bloquearse es usar SIGCHLD: el núcleo envia SIGCHLD al padre cuándo un hijo termina. El padre instala un manejador que recoge al hijo con waitpid(-1, &status, WNOHANG).
Esto es más eficiente que el polling porque el padre no necesita despertar periodicamente: Sólo actua cuándo un hijo termina.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
static volatile int hijos_restantes = 0;
void manejador_Sigchld(int sig)
{
(void) Sig;
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("SIGCHLD: hijo %d terminó", (int) pid);
if (WIFEXITED(status))
printf(" (Código %d)", WEXITSTATUS(status));
printf("\n");
hijos_restantes--;
}
}
int main(void)
{
struct sigaction sa;
sa.sa_handler = manejador_Sigchld;
Sigemptyset(&sa.sa_Mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
Sigaction(SIGCHLD, &sa, NULL);
/* Crear 5 hijos */
hijos_restantes = 5;
for (int i = 0; i < 5; i++) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
sleep(1 + i);
_exit(i);
}
}
/* El padre puede hacer otras cosas */
while (hijos_restantes > 0) {
printf("Padre: trabajando...\n");
sleep(1);
}
printf("Padre: todos los hijos terminaron\n");
return 0;
}
El manejador de SIGCHLD recoge a los hijos terminados con WNOHANG. El padre sigue trabajando (imprimiendo "trabajando...") mientras los hijos terminan. Cuando todos terminan, el bucle termina.
Si varios hijos terminan casí al mismo tiempo, el núcleo puede enviar un sólo SIGCHLD. Por eso el manejador debe llamar a waitpid en un bucle (while), para recoger a todos los hijos terminados, no sólo al primero.
6. El patron de event loop
Un event loop es un bucle que espera eventos (E/S, senales, terminación de hijos) y los maneja. Es el patron fundamental de los servidores y las aplicaciones interactivas.
Un event loop tipico combina:
- I/O multiplexing: select, poll o epoll para esperar E/S en multiples descriptores.
- SIGCHLD: para ser notificado cuándo un hijo termina.
- Timers: para ejecutar táreas periodicas.
El problema es que select/poll se interrumpen cuándo llega una senal (Como SIGCHLD). Si el manejador de SIGCHLD no es seguro para senales (async-Signal-safe), el programa puede corromperse. La solución es usar pselect o ppoll, Que permiten bloquear senales durante la espera y desbloquearlas atominimamente.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
#include
static volatile sig_atomic_t hijo_terminado = 0;
void manejador_Sigchld(int sig)
{
(void) Sig;
hijo_terminado = 1;
}
int main(void)
{
/* Instalar manejador */
struct sigaction sa;
sa.sa_handler = manejador_Sigchld;
Sigemptyset(&sa.sa_Mask);
sa.sa_flags = SA_RESTART;
Sigaction(SIGCHLD, &sa, NULL);
/* Crear un hijo */
pid_t pid = fork();
if (pid == 0) { sleep(3); _exit(0); }
/* Event loop */
while (1) {
if (hijo_terminado) {
hijo_terminado = 0;
int status;
while (waitpid(-1, &status, WNOHANG) > 0)
printf("Hijo terminó\n");
}
/* Esperar E/S con timeout de 1 Segúndo */
struct timeval tv = {1, 0};
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(STDIN_FILENO, &rfds);
int ret = select(STDIN_FILENO + 1, &rfds, NULL, NULL, &tv);
if (ret > 0 && FD_ISSET(STDIN_FILENO, &rfds)) {
char buf[256];
ssize_t n = read(STDIN_FILENO, buf, Sizeof buf - 1);
if (n > 0) { buf[n] = 0; printf("Entrada: %s", buf); }
}
if (hijo_terminado == 0 && restantes == 0) break;
}
return 0;
}
El event loop maneja SIGCHLD (recogiendo hijos) y E/S (leyendo de stdin) Simultaneamente. El manejador de SIGCHLD Sólo establece un flag (async-Signal-safe), y el bucle principal recoge a los hijos cuándo el flag esta activo.
7. pselect y ppoll con SIGCHLD
pselect() y ppoll() son versiones de select/poll que permiten bloquear senales durante la espera y desbloquearlas atominimamente. Esto evita la condicion de carrera entre la senal y la espera:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
static volatile sig_atomic_t hijo_terminado = 0;
void manejador_Sigchld(int sig) { (void) Sig; hijo_terminado = 1; }
int main(void)
{
struct sigaction sa;
sa.sa_handler = manejador_Sigchld;
Sigemptyset(&sa.sa_Mask);
sa.sa_flags = SA_RESTART;
Sigaction(SIGCHLD, &sa, NULL);
/* Bloquear SIGCHLD durante la espera */
Sigset_t mask, oldmask;
Sigemptyset(&Mask);
Sigaddset(&Mask, SIGCHLD);
Sigprocmask(SIG_BLOCK, &Mask, &oldmask);
pid_t pid = fork();
if (pid == 0) { sleep(3); _exit(0); }
/* Event loop con pselect */
while (1) {
if (hijo_terminado) {
hijo_terminado = 0;
int status;
while (waitpid(-1, &status, WNOHANG) > 0)
printf("Hijo terminó\n");
}
/* pselect: espera E/S o senales desbloqueadas */
struct timespec ts = {1, 0};
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(STDIN_FILENO, &rfds);
/* Desbloquea SIGCHLD durante la espera */
int ret = pselect(STDIN_FILENO + 1, &rfds, NULL, NULL, &ts, &oldmask);
if (ret > 0 && FD_ISSET(STDIN_FILENO, &rfds)) {
char buf[256];
ssize_t n = read(STDIN_FILENO, buf, Sizeof buf - 1);
if (n > 0) { buf[n] = 0; printf("Entrada: %s", buf); }
}
/* Verificar sí el hijo terminó */
int status;
if (waitpid(pid, &status, WNOHANG) > 0) {
printf("Hijo terminó, saliendo\n");
break;
}
}
return 0;
}
pselect desbloquea SIGCHLD durante la espera de E/S. Si llega SIGCHLD, pselect retorna y el manejador se ejecuta. Esto evita la condicion de carrera entre la senal y la espera.
8. Esperar con timeout
A veces se quiere esperar a un hijo pero con un limite de tiempo. Si el hijo no termina en el tiempo dado, el padre puede tomar otra accion (matar al hijo, Continuar sin el, etc.).
Con WNOHANG y un bucle de polling, se puede implementar un timeout:
#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) {
sleep(10); /* el hijo tarda 10 Segúndos */
_exit(0);
}
/* Esperar con timeout de 3 Segúndos */
int timeout = 3;
int terminado = 0;
for (int i = 0; i < timeout; i++) {
int status;
pid_t w = waitpid(pid, &status, WNOHANG);
if (w == pid) {
printf("Hijo terminó a tiempo\n");
terminado = 1;
break;
}
if (w == -1) { perror("waitpid"); break; }
sleep(1);
}
if (!terminado) {
printf("Timeout: matando al hijo\n");
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
}
return 0;
}
El programa espera hasta 3 Segúndos. Si el hijo no termina, lo mata con SIGTERM y lo recoge. Esto es útil para evitar que un hijo bloqueado indefinidamente cuelgue al padre.
9. El patron de trabajador
El patron de trabajador (worker) es un servidor que crea un pool de procesos para atender táreas. Cada trabajador atiende una tárea y luego vuelve a esperar otra. El padre gestiona el pool y recoge a los trabajadores que terminan.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
static int trabajadores_activos = 0;
static int max_trabajadores = 3;
void manejador_Sigchld(int sig)
{
(void) Sig;
int status;
while (waitpid(-1, &status, WNOHANG) > 0)
trabajadores_activos--;
}
void crear_trabajador(int id)
{
pid_t pid = fork();
if (pid < 0) { perror("fork"); return; }
if (pid == 0) {
printf("Trabajador %d (PID %d) atendiendo tárea\n", id, (int) getpid());
sleep(2 + id); /* Simular trabajo */
printf("Trabajador %d terminando\n", id);
_exit(0);
}
trabajadores_activos++;
printf("Creado trabajador %d (PID %d)\n", id, (int) pid);
}
int main(void)
{
struct sigaction sa;
sa.sa_handler = manejador_Sigchld;
Sigemptyset(&sa.sa_Mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
Sigaction(SIGCHLD, &sa, NULL);
/* Crear pool inicial */
for (int i = 0; i < max_trabajadores; i++)
crear_trabajador(i);
/* El padre monitorea y reemplaza trabajadores */
int siguiente_id = max_trabajadores;
while (Sigchld_count < 10) { /* Simulacion */
/* Si hay espacio, crear un nuevo trabajador */
if (trabajadores_activos < max_trabajadores) {
crear_trabajador(Siguiente_id++);
}
sleep(1);
}
/* Esperar a todos */
while (trabajadores_activos > 0) pause();
printf("Todos los trabajadores terminaron\n");
return 0;
}
El patron de trabajador mantiene un número constante de procesos activos. Cuando uno termina, el padre crea otro para reemplazarlo. Esto es la base de los servidores de alto rendimiento.
10. Un programa que demuestra esperar sin bloquearse
El siguiente programa crea 5 hijos, recoge a los que terminan con SIGCHLD + WNOHANG, y hace trabajo mientras tanto:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
static volatile int terminados = 0;
static int total = 5;
void manejador_Sigchld(int sig)
{
(void) Sig;
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf(" [SIGCHLD] Hijo %d terminó", (int) pid);
if (WIFEXITED(status))
printf(" (Código %d)", WEXITSTATUS(status));
printf("\n");
terminados++;
}
}
int main(void)
{
struct sigaction sa;
sa.sa_handler = manejador_Sigchld;
Sigemptyset(&sa.sa_Mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
Sigaction(SIGCHLD, &sa, NULL);
/* Crear hijos */
for (int i = 0; i < total; i++) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
printf("Hijo %d (PID %d) durmiendo %d s\n", i, (int) getpid(), 2 + i);
sleep(2 + i);
_exit(i);
}
}
/* El padre hace trabajo mientras espera */
int contador = 0;
while (terminados < total) {
printf("Padre: trabajando... (contador=%d, terminados=%d/%d)\n",
contador++, terminados, total);
sleep(1);
}
printf("Padre: todos los hijos terminaron\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o demo_no_bloquear demo_no_bloquear.c
$ ./demo_no_bloquear
Hijo 0 (PID 1020) durmiendo 2 s
Hijo 1 (PID 1021) durmiendo 3 s
Hijo 2 (PID 1022) durmiendo 4 s
Hijo 3 (PID 1023) durmiendo 5 s
Hijo 4 (PID 1024) durmiendo 6 s
Padre: trabajando... (contador=0, terminados=0/5)
Padre: trabajando... (contador=1, terminados=0/5)
[SIGCHLD] Hijo 1020 Terminó (Código 0)
Padre: trabajando... (contador=2, terminados=1/5)
[SIGCHLD] Hijo 1021 Terminó (Código 1)
Padre: trabajando... (contador=3, terminados=2/5)
[SIGCHLD] Hijo 1022 Terminó (Código 2)
Padre: trabajando... (contador=4, terminados=3/5)
[SIGCHLD] Hijo 1023 Terminó (Código 3)
Padre: trabajando... (contador=5, terminados=4/5)
[SIGCHLD] Hijo 1024 Terminó (Código 4)
Padre: todos los hijos terminaron
El programa demuestra que el padre puede hacer trabajo (incrementar un contador) mientras los hijos terminan. Los SIGCHLD se reciben asincronicamente, y el manejador recoge a los hijos con WNOHANG.
11. Resumen
- WNOHANG hace que waitpid no bloquee: retorna 0 Sí el hijo no ha terminado.
- El patron de polling verifica periodicamente con WNOHANG, haciendo otra tárea entre verificaciones.
- SIGCHLD + WNOHANG es más eficiente: el núcleo notifica al padre cuándo un hijo termina.
- El manejador de SIGCHLD debe llamar a waitpid en un bucle (while) porque SIGCHLD puede coalescer.
- El event loop combina I/O multiplexing (select/poll/epoll) con SIGCHLD.
- pselect/ppoll permiten bloquear senales durante la espera y desbloquearlas atominimamente.
- Se puede esperar con timeout usando WNOHANG en un bucle con sleep.
- El patron de trabajador mantiene un pool de procesos activos.
- El manejador de SIGCHLD Sólo debe establecer un flag (async-Signal-safe).
12. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Qué hace WNOHANG?
Hace que waitpid no bloquee: Sí el hijo no ha terminado, retorna 0 inmediatamente.
2. Qué devuelve waitpid con WNOHANG Sí el hijo no ha terminado?
0. Si el hijo terminó, devuelve su PID. Si hay error, devuelve -1.
3. Por que el manejador de SIGCHLD debe llamar a waitpid en un bucle?
Porque SIGCHLD puede coalescer: Sí varios hijos terminan casí al mismo tiempo, el núcleo envia un sólo SIGCHLD. El bucle recoge a todos.
4. Qué es un event loop?
Un bucle que espera eventos (E/S, senales, terminación de hijos) y los maneja. Es el patron fundamental de los servidores.
5. Qué es pselect?
Una version de select que permite bloquear senales durante la espera y desbloquearlas atominimamente, evitando condiciones de carrera.
6. Qué es un patron de polling?
Verificar periodicamente sí los hijos terminaron (con WNOHANG), haciendo otra tárea entre verificaciones.
7. Cómo se implementa un timeout al esperar a un hijo?
Usando WNOHANG en un bucle con sleep, y matando al hijo sí no termina en el tiempo dado.
8. Qué es el patron de trabajador (worker)?
Un servidor que mantiene un pool de procesos activos. Cuando uno termina, el padre crea otro para reemplazarlo.
9. Por que el manejador de SIGCHLD Sólo debe establecer un flag?
Porque sólo las funciónes async-Signal-safe se pueden llamar en un manejador de senales. printf y malloc no son seguras. Establecer un flag es seguro.
10. Qué es async-Signal-safe?
Una función que se puede llamar de forma segura dentro de un manejador de senales, Sin riesgo de corromper el estado del programa. Incluye write, _exit, waitpid (con WNOHANG), etc.
Para prácticar
- Compila y ejecuta demo_no_bloquear.c. Observa como el padre hace trabajo mientras los hijos terminan.
- Compila y ejecuta timeout_wait.c. Verifica que el hijo se mata sí no termina a tiempo.
- Escribe un programa que use pselect para manejar SIGCHLD y E/S Simultaneamente.
- Modifica sigchld_wnohang.c para contar cuántos hijos terminaron.
- Escribe un programa que implemente un pool de 3 trabajadores que se reemplazan cuándo terminan.
- Investiga las funciónes async-Signal-safe en man 7 Signal-safety.