1. Por que esperar a los hijos
Despues de fork, el padre y el hijo son procesos independientes. El padre puede continuar ejecutandose, pero en algun momento necesita saber que paso con el hijo: Sí terminó correctamente, con que código, o sí fue terminado por una senal.
La función wait() (y sus variantes) permite al padre bloquearse hasta que un hijo termine y obtener su estado de terminación. Sin wait, el hijo queda como zombi cuándo termina, y el padre no sabe cuándo terminó ni como.
Esperar a los hijos es fundamental para:
- Obtener el resultado: el código de salida del hijo indica sí la tárea se completo correctamente.
- Evitar zombis: recoger al hijo libera su PCB y su entrada en la tabla de procesos.
- Sincronizacion: el padre puede esperar a que el hijo termine antes de continuar.
- Control de flujo: el padre puede decidir que hacer según como terminó el hijo.
fork crea un hijo, wait lo recoge. Sin wait, el hijo queda como zombi. Todo programa que crea hijos debe esperarlos en algun momento, o instalar un manejador de SIGCHLD para recogerlos automaticamente.
2. wait: esperar a cualquier hijo
wait() bloquea al padre hasta que Cualquier hijo termine. Devuelve el PID del hijo que terminó y guarda su estado de terminación en el puntero proporcionado.
#include
pid_t wait(int *status);
Si hay varios hijos, wait devuelve el primero que termine (no necesariamente el primero que se creó). Si no hay hijos, wait devuelve -1 y errno se establece a ECHILD.
Si status es NULL, el estado de terminación se descarta (pero el hijo se recoge igualmente). Esto es útil cuándo no interesa como terminó el hijo, Sólo se quiere recogerlo.
#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) {
printf("Hijo: trabajando...\n");
sleep(2);
printf("Hijo: terminando con código 42\n");
_exit(42);
}
/* Padre: esperar */
printf("Padre: esperando...\n");
int status;
pid_t w = wait(&status);
printf("Padre: hijo %d terminó\n", (int) w);
if (WIFEXITED(status))
printf(" Código de salida: %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf(" terminado por senal %d\n", WTERMSIG(status));
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o wait_Simple wait_Simple.c
$ ./wait_Simple
Padre: esperando...
Hijo: trabajando...
Hijo: terminando con código 42
Padre: hijo 990 Terminó
Código de salida: 42
3. waitpid: esperar a un hijo especÃfico
waitpid() es más flexible que wait: permite esperar a un hijo especÃfico, a cualquier hijo, o a un grupo de procesos. Tambien permite opciones (Como no bloquearse).
#include
pid_t waitpid(pid_t pid, int *status, int options);
El parámetro pid determina a que hijo se espera:
| Valor de pid | Significado |
|---|---|
| Mayor que 0 | Esperar al hijo con ese PID EspecÃfico. |
| -1 | Esperar a cualquier hijo (equivalente a wait). |
| 0 | Esperar a cualquier hijo del mismo grupo de procesos que el padre. |
| Menor que -1 | Esperar a cualquier hijo del grupo de procesos cuyo ID es |pid|. |
waitpid es la función recomendada para esperar a hijos especÃficos, porque permite controlar exactamente que hijo se espera y con que opciones.
4. Las opciones de waitpid
El parámetro options es un OR de bits que modifica el comportamiento de waitpid:
| Opcion | Significado |
|---|---|
0 | Bloquear hasta que el hijo termine (comportamiento por omision). |
WNOHANG | No bloquear: Sí el hijo no ha terminado, retornar 0 inmediatamente. |
WUNTRACED | Tambien retornar sí el hijo esta detenido (por una senal). Requiere que el padre maneje SIGCHLD con SA_NOCLDSTOP. |
WCONTINUED | Tambien retornar sí el hijo se reanudo (Después de estar detenido). |
Las opciones se pueden combinar con OR: WNOHANG | WUNTRACED.
WNOHANG es la opcion más usada: permite al padre verificar sí un hijo terminó sin bloquearse. Es la base del patron de "polling" de hijos.
5. Interpretar el estado
El estado de terminación que devuelve wait/waitpid se interpreta con las macros de POSIX, Que ya viste en el tema 20:
| Macro | Significado |
|---|---|
WIFEXITED(status) | Verdadero sí Terminó normalmente (exit/return). |
WEXITSTATUS(status) | Codigo de salida (0-255). Valido sólo sí WIFEXITED. |
WIFSIGNALED(status) | Verdadero sí Terminó por senal. |
WTERMSIG(status) | Numero de la senal. Valido sólo sí WIFSIGNALED. |
WCOREDUMP(status) | Verdadero sí genero core dump (no POSIX, pero disponible en linux). |
El patron de interpretacion es:
int status;
pid_t w = waitpid(pid, &status, 0);
if (w == -1) {
perror("waitpid");
} else if (WIFEXITED(status)) {
printf("Hijo %d terminó con código %d\n", (int) w, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Hijo %d terminó por senal %d\n", (int) w, WTERMSIG(status));
}
6. waitid: la version moderna
waitid() es la version moderna de waitpid, con una interfaz más flexible. Permite esperar a un hijo, a todos los hijos, o a un grupo de procesos, y puede devolver información adicional.
#include
int waitid(idtype_t idtype, id_t id, Siginfo_t *infop, int options);
El parámetro idtype determina como se interpreta id:
| idtype | Significado |
|---|---|
| P_PID | Esperar al hijo con PID = id. |
| P_PGID | Esperar a cualquier hijo del grupo de procesos id. |
| P_ALL | Esperar a cualquier hijo (id se ignora). |
Las opciones son similares a waitpid: WNOHANG, WUNTRACED, WCONTINUED, WEXITED, WSTOPPED. Se pueden combinar con OR.
waitid devuelve la información en una estructura Siginfo_t, Que incluye el PID del hijo, el código de salida, la senal, etc. Es más informativa que waitpid.
waitid es la función recomendada para código nuevo, porque es más flexible y más informativa. Pero waitpid sigue siendo la más usada por compatibilidad.
7. wait3 y wait4: con información de recursos
wait3() y wait4() son variantes de wait que también devuelven información de uso de recursos del hijo (tiempo de CPU, Memoria, etc.).
#include
#include
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
El parámetro rusage se llena con las estadisticas de uso de recursos del hijo: tiempo de usuario, tiempo de sistema, Memoria maxima, Número de fallos de página, etc.
$ man wait4 # ver la estructura rusage
wait3 y wait4 son extensiones de BSD, no POSIX. Pero estan disponibles en linux. Son útiles para medir el rendimiento de procesos hijos.
8. El problema de los zombis
Cómo viste en el tema 6, un proceso que termina pero cuyo padre no lo espera queda como zombi. wait y waitpid son la solución: recogen al hijo y liberan su PCB.
Si el padre no puede esperar a los hijos (porque esta ocupado haciendo otra cosa), puede usar una de estas estrategias:
- Manejador de SIGCHLD con WNOHANG: instalar un manejador de SIGCHLD Que llama a waitpid(-1, &status, WNOHANG) en un bucle. Esto recoge a todos los hijos terminados sin bloquearse.
- Ignorar SIGCHLD: en linux, Sí el padre ignora SIGCHLD (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.
La estrategia más comun en servidores es el manejador de SIGCHLD con WNOHANG, porque permite recoger a los hijos asincronicamente sin bloquear al padre.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
void manejador_Sigchld(int sig)
{
(void) Sig;
/* Recoger a todos los hijos terminados */
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("Hijo %d terminado\n", (int) pid);
}
}
int main(void)
{
/* Instalar el manejador */
struct sigaction sa;
sa.sa_handler = manejador_Sigchld;
Sigemptyset(&sa.sa_Mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
Sigaction(SIGCHLD, &sa, NULL);
/* Crear varios hijos */
for (int i = 0; i < 5; i++) {
pid_t pid = fork();
if (pid == 0) {
sleep(1 + i);
_exit(i);
}
}
/* El padre puede hacer otras cosas */
printf("Padre: haciendo trabajo...\n");
sleep(10);
printf("Padre: terminando\n");
return 0;
}
El manejador de SIGCHLD recoge a los hijos terminados sin bloquear al padre. El flag SA_RESTART hace que las syscalls interrumpidas por la senal se reinicien automaticamente. SA_NOCLDSTOP evita que se envie SIGCHLD Cuándo un hijo se detiene (Sólo cuándo termina).
9. Esperar a todos los hijos
Cuando un proceso crea varios hijos, necesita esperar a todos. El patron más comun es usar wait() en un bucle:
#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);
_exit(i + 1);
}
}
/* Esperar a todos los hijos */
int restantes = n;
while (restantes > 0) {
int status;
pid_t w = wait(&status);
if (w == -1) {
if (errno == ECHILD) break; /* no hay más hijos */
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;
}
El bucle llama a wait() hasta que no hay más hijos (ECHILD). Los hijos se recogen en el orden en que terminan, no en el orden en que se crearon.
Si se quiere recoger en orden de creacion, 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));
}
10. Esperar sin bloquearse
Con WNOHANG, waitpid retorna inmediatamente sí el hijo no ha terminado. Esto permite al padre verificar el estado de los hijos sin bloquearse:
#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(3);
_exit(0);
}
/* Polling: verificar cada segúndo */
while (1) {
int status;
pid_t w = waitpid(pid, &status, WNOHANG);
if (w == -1) { perror("waitpid"); break; }
if (w == 0) {
printf("Hijo aun ejecutandose...\n");
sleep(1);
} else {
printf("Hijo %d terminó\n", (int) w);
break;
}
}
return 0;
}
WNOHANG es la base del patron de "polling" de hijos: verificar periodicamente sí terminaron, Sin bloquearse. Es útil cuándo el padre tiene otras táreas que hacer mientras espera.
11. Un programa que demuestra wait y waitpid
El siguiente programa crea 3 hijos con diferentes tiempos de vida y los recoge en orden de terminación:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
int main(void)
{
/* Crear 3 hijos con diferentes tiempos */
int tiempos[] = {3, 1, 2};
pid_t pids[3];
for (int i = 0; i < 3; i++) {
pids[i] = fork();
if (pids[i] < 0) { perror("fork"); return 1; }
if (pids[i] == 0) {
printf("Hijo %d (PID %d) durmiendo %d s\n", i, (int) getpid(), tiempos[i]);
sleep(tiempos[i]);
_exit(i + 10);
}
}
/* Recoger en orden de terminación */
printf("Padre: esperando hijos...\n");
for (int i = 0; i < 3; i++) {
int status;
pid_t w = wait(&status);
if (w == -1) { perror("wait"); break; }
printf("Padre: hijo %d terminó", (int) w);
if (WIFEXITED(status))
printf(" (Código %d)\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf(" (senal %d)\n", WTERMSIG(status));
}
printf("Padre: todos recogidos\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o demo_wait demo_wait.c
$ ./demo_wait
Hijo 0 (PID 1010) durmiendo 3 s
Hijo 1 (PID 1011) durmiendo 1 s
Hijo 2 (PID 1012) durmiendo 2 s
Padre: esperando hijos...
Padre: hijo 1011 Terminó (Código 11)
Padre: hijo 1012 Terminó (Código 12)
Padre: hijo 1010 Terminó (Código 10)
Padre: todos recogidos
Los hijos se recogen en orden de terminación: el hijo 1 (1 Segúndo) primero, luego el 2 (2 Segúndos), luego el 0 (3 Segúndos). Los códigos de salida son 10, 11, 12 (i + 10).
12. Resumen
- wait() bloquea hasta que cualquier hijo termine y devuelve su PID y estado.
- waitpid() permite esperar a un hijo especÃfico o a cualquier hijo, con opciones.
- WNOHANG hace que waitpid no bloquee: retorna 0 Sí el hijo no ha terminado.
- WUNTRACED y WCONTINUED También reportan detencion y reanudacion.
- waitid() es la version moderna con siginfo_t.
- wait3/wait4 devuelven información de uso de recursos.
- Sin wait, los hijos quedan como zombis.
- El manejador de SIGCHLD con WNOHANG recoge hijos asincronicamente.
- Ignorar SIGCHLD en linux recoge hijos automaticamente.
- wait() en un bucle recoge a todos los hijos en orden de terminación.
13. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Qué hace wait()?
Bloquea al padre hasta que cualquier hijo termine, y devuelve el PID del hijo y su estado de terminación.
2. Qué diferencia hay entre wait y waitpid?
wait espera a cualquier hijo. waitpid permite esperar a un hijo especÃfico (por PID) o a cualquier hijo, y acepta opciones (WNOHANG, etc.).
3. Qué hace WNOHANG?
Hace que waitpid no bloquee: Sí el hijo no ha terminado, retorna 0 inmediatamente.
4. Qué pasa sí no hay hijos cuándo se llama a wait?
wait devuelve -1 y errno se establece a ECHILD.
5. Qué es waitid?
La version moderna de waitpid, con una interfaz más flexible (idtype, id, Siginfo_t, options). Devuelve información más detallada.
6. Qué es wait4?
Una variante de waitpid que también devuelve información de uso de recursos (tiempo de CPU, Memoria, etc.) en una estructura rusage.
7. Cómo se evitan los zombis sin bloquear al padre?
Con un manejador de SIGCHLD Que llama a waitpid(-1, &status, WNOHANG) en un bucle, o ignorando SIGCHLD en linux.
8. Qué hace WUNTRACED?
Hace que waitpid también retorne sí el hijo esta detenido (por una senal), no sólo cuándo termina.
9. Cómo se espera a todos los hijos en orden de creacion?
Guardando los PIDs al crearlos y llamando a waitpid(pid, &status, 0) para cada uno en orden.
10. Qué es el patron de polling de hijos?
Usar waitpid con WNOHANG en un bucle, verificando periodicamente sí los hijos terminaron, Sin bloquearse.
Para prácticar
- Compila y ejecuta wait_Simple.c. Verifica que el padre recoge el código de salida del hijo.
- Compila y ejecuta esperar_todos.c. Observa que los hijos se recogen en orden de terminación.
- Compila y ejecuta poll_hijos.c. Observa el polling con WNOHANG.
- Compila y ejecuta sigchld_handler.c. Observa como el manejador recoge a los hijos asincronicamente.
- Escribe un programa que ignore SIGCHLD y verifique que los hijos no quedan como zombis.
- Usa wait4 para medir el tiempo de CPU de un hijo.