wait y waitpid - Procesos e Hilos
Volver a Procesos e Hilos Bloque 5 - Esperar y terminar - Tema 21 de 43

Tema 21

wait y waitpid

Cuando un proceso crea un hijo con fork, necesita saber cuándo terminó y como. Las funciónes wait y waitpid permiten al padre esperar a un hijo y obtener su estado de terminación. En este tema veras como usarlas, las opciones disponibles, y como interpretar el estado del hijo.

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:

wait es la contrapartida de fork

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.

C - firma de wait

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

wait_Simple.c

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

$ 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).

C - firma de waitpid

#include 

pid_t waitpid(pid_t pid, int *status, int options);

El parámetro pid determina a que hijo se espera:

Valor de pidSignificado
Mayor que 0Esperar al hijo con ese PID Específico.
-1Esperar a cualquier hijo (equivalente a wait).
0Esperar a cualquier hijo del mismo grupo de procesos que el padre.
Menor que -1Esperar 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:

OpcionSignificado
0Bloquear hasta que el hijo termine (comportamiento por omision).
WNOHANGNo bloquear: Sí el hijo no ha terminado, retornar 0 inmediatamente.
WUNTRACEDTambien retornar sí el hijo esta detenido (por una senal). Requiere que el padre maneje SIGCHLD con SA_NOCLDSTOP.
WCONTINUEDTambien 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:

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

interpretar_status.c

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.

C - firma de waitid

#include 

int waitid(idtype_t idtype, id_t id, Siginfo_t *infop, int options);

El parámetro idtype determina como se interpreta id:

idtypeSignificado
P_PIDEsperar al hijo con PID = id.
P_PGIDEsperar a cualquier hijo del grupo de procesos id.
P_ALLEsperar 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.).

C - firmás de wait3 y wait4

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

terminal - usar wait4 para obtener recursos

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

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.

Sigchld_handler.c

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

esperar_todos.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);
            _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_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));
    }

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:

poll_hijos.c

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

demo_wait.c

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

$ 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

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

  1. Compila y ejecuta wait_Simple.c. Verifica que el padre recoge el código de salida del hijo.
  2. Compila y ejecuta esperar_todos.c. Observa que los hijos se recogen en orden de terminación.
  3. Compila y ejecuta poll_hijos.c. Observa el polling con WNOHANG.
  4. Compila y ejecuta sigchld_handler.c. Observa como el manejador recoge a los hijos asincronicamente.
  5. Escribe un programa que ignore SIGCHLD y verifique que los hijos no quedan como zombis.
  6. Usa wait4 para medir el tiempo de CPU de un hijo.