Procesos en Linux con C — teoría con fork(), exec y wait en la cocina de un restaurante (y el contador que se quedó en 0)

Preparando la práctica de procesos de IC2 me inventé una cocina: un jefe (el proceso padre) que contrata a tres cocineros (tres procesos hijos), y cada cocinero suma 1 a un contador global de platos listos. Tres cocineros, tres platos. Fácil, ¿no? Compilé, ejecuté y el jefe me soltó esto:

/* gancho.c: el contador de platos que se quedó en 0 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int platos_listos = 0;   /* contador global: yo creía que era compartido */

int main(void) {
    for (int i = 0; i < 3; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            /* el cocinero (hijo) prepara su plato y suma 1 al contador */
            platos_listos++;
            printf("Cocinero %d: plato listo (mi contador vale %d)\n", i + 1, platos_listos);
            exit(0);
        }
        /* el jefe (padre) espera a ese cocinero antes de lanzar el siguiente */
        wait(NULL);
    }
    printf("Jefe: platos listos = %d\n", platos_listos);
    return 0;
}

Y la salida real:

Cocinero 1: plato listo (mi contador vale 1)
Cocinero 2: plato listo (mi contador vale 1)
Cocinero 3: plato listo (mi contador vale 1)
Jefe: platos listos = 0

Cada cocinero dice que su contador vale 1… y el jefe dice que hay 0 platos listos. Me pasé un buen rato buscando el fallo en el bucle, en el ++, en el wait. El código estaba bien. Lo que estaba mal era mi idea de lo que hace fork(): no comparte la memoria, la copia. Cada cocinero sumó 1 a su copia del contador, y la copia del jefe nunca se tocó.

Hoy vamos a entender eso y todo lo que lo rodea: qué es un proceso, cómo se crea con fork(), cómo se convierte en otro programa con exec, cómo el padre espera y recoge el resultado con wait() y exit(), y qué son los famosos zombis. Venimos del preprocesador en C, y cuando termines tienes la práctica de procesos con la cocina completa funcionando.

Qué es un proceso (y en qué se diferencia de un programa)

Un programa es un fichero en el disco: fork_basico.c compilado se queda quieto en tu carpeta como fork_basico. Un proceso es ese programa en ejecución: tiene su memoria (variables, pila, montículo), su contador de programa (por qué línea va), sus ficheros abiertos y, sobre todo, un número que lo identifica, el PID (Process IDentifier).

Un mismo programa puede estar corriendo varias veces a la vez, y cada ejecución es un proceso distinto con su PID. Si abres dos terminales, tienes dos procesos bash del mismo programa /bin/bash.

Además, todo proceso lo crea otro proceso. Al creador se le llama padre y al nuevo, hijo. Por eso los procesos forman un árbol: tu programa es hijo de la shell, la shell es hija del emulador de terminal… y arriba del todo está el proceso de PID 1 (init o systemd), el antepasado de todos. En la práctica guiada lo ves con dos órdenes:

OrdenQué enseña
psLos procesos de tu terminal, con su PID
ps -o pid,ppid,stat,commAdemás, el PID del padre (PPID) y el estado
pstree -pEl árbol completo con los PID entre paréntesis
sleep 20 &Lanza un proceso en segundo plano para verlo en ps mientras vive

Tu PID y el de tu padre: getpid() y getppid()

El primer programa de la práctica pregunta al sistema quién es y quién es su padre. Las dos funciones están en <unistd.h> y devuelven un pid_t, que en Linux es un entero:

#include <stdio.h>
#include <unistd.h>

int main(void) {
    /* getpid(): el número del proceso que está ejecutando esta línea */
    printf("PID del proceso actual: %d\n", getpid());
    /* getppid(): el número de su padre (en tu terminal, la shell que lo lanzó) */
    printf("PID del padre: %d\n", getppid());
    return 0;
}

En mi única ejecución salió esto:

PID del proceso actual: 508
PID del padre: 387

Ojo con los números: los PID los reparte el sistema según van naciendo procesos, así que en tu ordenador saldrán otros, y cambiarán cada vez que lo ejecutes. El padre (387) en mi caso es el script con el que lanzo todos los programas del artículo; si lo lanzas tú desde la terminal, getppid() te dará el PID de tu bash, y ese sí se repetirá entre ejecuciones (es la misma shell), mientras que el de tu programa irá cambiando.

Con -std=c11 hay un detalle: si declaras una variable pid_t y solo incluyes <unistd.h>, gcc puede decir unknown type name 'pid_t'. El tipo está en <sys/types.h> (y también lo traen <sys/wait.h> y otras cabeceras). Por eso verás <sys/types.h> o <sys/wait.h> en los programas que guardan un PID.

fork(): una llamada, dos vueltas

fork() crea un proceso nuevo copiando al que la llama. La práctica lo describe muy bien: es como si el sistema hiciera una foto del proceso en ese instante (memoria, variables, pila, contador de programa…) y arrancara un proceso nuevo con esa foto. A partir de ahí hay dos procesos independientes que siguen ejecutando desde la misma línea: justo después del fork().

¿Y cómo sabe cada uno quién es, si son copias? Por lo que devuelve fork():

Valor de retornoQuién lo recibeQué significa
0El hijo«Soy el hijo» (su PID real lo saca con getpid())
Mayor que 0El padreEs el PID del hijo que acaba de nacer
-1El padreNo se ha podido crear el hijo (no hay segundo proceso)

Vamos con el jefe abriendo la cocina con un cocinero:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    printf("Jefe (PID %d): abro la cocina\n", getpid());

    /* desde aquí hay DOS procesos, y los dos siguen en la línea de abajo */
    pid_t pid = fork();

    if (pid < 0) {
        /* -1: el sistema no ha podido crear el proceso (no hay hijo) */
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        /* 0: esta rama solo la ejecuta el hijo */
        printf("Cocinero: fork() me devolvió %d. Mi PID es %d y mi jefe es %d\n",
               pid, getpid(), getppid());
        exit(0);
    } else {
        /* mayor que 0: esta rama solo la ejecuta el padre; pid es el PID del hijo */
        wait(NULL);
        printf("Jefe: fork() me devolvió %d, el PID de mi cocinero\n", pid);
    }
    /* a esta línea solo llega el padre: el hijo ya salió con exit(0) */
    printf("Jefe: cierro la cocina\n");
    return 0;
}

Salida real:

Jefe (PID 514): abro la cocina
Cocinero: fork() me devolvió 0. Mi PID es 515 y mi jefe es 514
Jefe: fork() me devolvió 515, el PID de mi cocinero
Jefe: cierro la cocina

Entre líneas:

  • «abro la cocina» sale una sola vez: se imprime antes del fork(), cuando todavía hay un único proceso.
  • El cocinero recibió 0 de fork(), pero su PID es 515. El jefe recibió 515: el mismo número. Por eso el padre puede guardar ese valor y usarlo luego para esperar a ese hijo en concreto.
  • El cocinero dice que su jefe es 514, que es justo el PID que imprimió el jefe al abrir: getppid() del hijo = getpid() del padre.
  • «cierro la cocina» sale una vez porque el hijo terminó con exit(0) dentro de su rama y nunca llegó a esa línea.

Fíjate en el dibujo: la flecha se parte en dos en el fork(), y lo único que diferencia los dos caminos es el valor de pid.

fork() devuelve dos valores El proceso 514 llama a fork(); después hay dos procesos: el padre recibe 515 y el hijo recibe 0. fork(): una llamada, dos vueltas Jefe · PID 514 printf(…); pid_t pid = fork(); antes: un solo proceso Padre · PID 514 pid = 515 → rama else: wait(NULL) Hijo · PID 515 · PPID 514 pid = 0 → rama if (pid == 0) Si fork() falla devuelve -1 y no se crea ningún hijo
Una llamada, dos procesos: el valor de pid decide qué rama ejecuta cada uno.

Pensar que fork() «ejecuta la función hijo» o que el hijo empieza desde el principio del main. No: el hijo nace en mitad del programa, en la línea siguiente al fork(), con todas las variables tal como estaban. Si el hijo no termina con exit() dentro de su rama, seguirá ejecutando lo que venga después, igual que el padre.

El if (pid < 0) no es de adorno. Si el sistema se queda sin recursos (demasiados procesos), fork() devuelve -1 y no hay hijo. perror("fork") te dice por qué.

Sin wait(): el orden no está garantizado (y los huérfanos)

En el programa anterior el jefe hacía wait(NULL) antes de su mensaje final, y por eso el orden de la salida estaba asegurado. ¿Qué pasa si el padre no espera? Este cocinero tarda un segundo en terminar y el jefe se va sin mirar atrás:

#include <stdio.h>
#include <sys/types.h>   /* pid_t: con -std=c11, unistd.h no lo trae */
#include <unistd.h>

int main(void) {
    pid_t pid = fork();
    if (pid == 0) {
        printf("Cocinero %d: empiezo, mi jefe es %d\n", getpid(), getppid());
        /* durante este segundo el jefe termina sin esperarme */
        sleep(1);
        printf("Cocinero %d: termino, ahora mi jefe es %d\n", getpid(), getppid());
        return 0;
    }
    /* el padre no llama a wait(): se va en cuanto imprime */
    printf("Jefe %d: me voy sin esperar a nadie\n", getpid());
    return 0;
}

Salida real de mi única ejecución:

Jefe 589: me voy sin esperar a nadie
Cocinero 590: empiezo, mi jefe es 589
Cocinero 590: termino, ahora mi jefe es 1

Aquí pasan dos cosas que conviene entender bien:

  1. El orden de las dos primeras líneas no es fijo. Después del fork(), padre e hijo compiten por la CPU, y es el planificador del sistema el que decide quién va primero. En mi ejecución habló antes el jefe, pero en la tuya puede salir antes el cocinero. Si el orden importa para lo que quieres explicar o calcular, **usa wait()**.
  2. El hijo se ha quedado huérfano. Al principio su jefe es 589. Durante el sleep(1) el padre termina, y cuando el cocinero vuelve a preguntar, su padre es 1: el sistema le ha buscado un padre adoptivo, el proceso de PID 1 (init/systemd). En algunos escritorios no sale 1, sino el PID de otro proceso que hace de «adoptador» (un subreaper), pero la idea es la misma.

Si al ejecutar esto en tu terminal ves aparecer el prompt ($) y luego, un segundo después, la línea del cocinero, no es un fallo: la shell solo esperaba al padre. Cuando el padre termina, la shell vuelve a pedirte órdenes aunque el hijo siga vivo.

La memoria se copia, no se comparte

Volvamos al misterio del gancho. Este programa es el ejercicio 6.2 de la práctica reducido a lo esencial: una variable global que vale 1, el padre le suma 1 y el hijo le suma 5. Para rematar, cada uno imprime la dirección de su variable:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int contador = 1;   /* variable global, como en el ejercicio 6.2 de la práctica */

int main(void) {
    pid_t pid = fork();
    if (pid == 0) {
        /* el hijo cambia SU copia de contador */
        contador += 5;
        printf("Hijo:  contador = %d, en la dirección %p\n", contador, (void *)&contador);
        exit(0);
    }
    /* el padre espera a que el hijo acabe y luego cambia SU copia */
    wait(NULL);
    contador += 1;
    printf("Padre: contador = %d, en la dirección %p\n", contador, (void *)&contador);
    return 0;
}

Salida real:

Hijo:  contador = 6, en la dirección 0x55def59cd010
Padre: contador = 2, en la dirección 0x55def59cd010

Lo primero es normal: el hijo ve 6 (1 + 5) y el padre ve 2 (1 + 1). Si la variable fuera compartida, el padre habría visto 7 (1 + 5 + 1). Pero lo segundo es lo que descoloca: la dirección es la misma, 0x55def59cd010, en los dos. ¿Cómo puede haber dos valores distintos en la misma dirección?

Porque esa dirección es virtual. Cada proceso tiene su propio espacio de direcciones, y el sistema operativo traduce cada dirección virtual a una posición de la memoria física distinta para cada uno. Es como si padre e hijo tuvieran dos casas idénticas con la misma numeración de habitaciones: «habitación 10» es la misma etiqueta, pero son dos habitaciones diferentes.

Memoria copiada tras fork() Padre e hijo tienen contador en la misma dirección 0x55def59cd010; el padre pasa de 1 a 2 y el hijo de 1 a 6. Cada proceso tiene su copia de contador Padre &contador = 0x55def59cd010 tras fork() 1 después 2 contador += 1 Hijo &contador = 0x55def59cd010 tras fork() 1 después 6 contador += 5 Misma dirección virtual, memoria distinta: lo que cambia uno, el otro no lo ve
Fíjate en que la dirección es la misma y aun así los valores acaban siendo 2 y 6.

La traza de la variable, paso a paso:

Momentocontador del padrecontador del hijo
Antes de fork()1(no existe)
Justo después de fork()11 (copia)
El hijo hace contador += 516
El hijo imprime y hace exit(0)1(termina)
El padre, tras wait(), hace contador += 12—

Y esto explica el gancho: cada cocinero sumaba 1 a su copia (por eso todos decían «mi contador vale 1») y el contador del jefe seguía en 0.

Copiar toda la memoria en cada fork() sería muy lento, así que Linux hace trampa con elegancia: al principio padre e hijo comparten las páginas de memoria marcadas como «solo lectura», y solo cuando uno de los dos escribe se hace la copia de esa página. Se llama copy-on-write. Para tu programa el efecto es el mismo: cada uno ve solo sus cambios.

Usar una variable global para que los hijos «devuelvan» resultados al padre. No funciona. Para pasar información de vuelta hay mecanismos de comunicación: el más sencillo, el código de salida de exit() que verás enseguida (y más adelante, ficheros y tuberías).

exec(): convertirse en otro programa

fork() crea un proceso, pero es una copia del mismo programa. ¿Y si quiero ejecutar otro programa, como ls o date? Para eso está la familia exec. Lo importante, y lo que más cuesta al principio: **exec no crea un proceso nuevo**. Sustituye el código del proceso que la llama por el del programa nuevo. El PID se queda igual; lo que cambia es todo lo demás.

Para demostrarlo, este programa imprime su PID y luego se convierte en ps, que lista el proceso con ese mismo PID:

#include <stdio.h>
#include <unistd.h>

int main(void) {
    char pid_txt[16];
    /* guardamos el PID como texto para pasárselo a ps como argumento */
    snprintf(pid_txt, sizeof pid_txt, "%d", getpid());
    printf("Antes de exec: soy el proceso %s\n", pid_txt);

    /* el proceso se convierte en ps, que se lista a sí mismo */
    execlp("ps", "ps", "-o", "pid,comm", "-p", pid_txt, NULL);

    /* solo se llega aquí si exec ha fallado */
    perror("execlp");
    return 1;
}

Salida real:

Antes de exec: soy el proceso 530
  PID COMMAND
  530 ps

Antes de exec el proceso 530 era nuestro programa exec; después, el proceso 530 se llama ps. Mismo número, otro programa. Y fíjate en lo que no sale: ni rastro del perror("execlp"). Cuando exec funciona, no vuelve nunca: el código que había después ya no existe, ha sido sustituido. Por eso la línea siguiente a un exec solo se ejecuta si ha fallado.

¿Por qué tantos nombres (execl, execlp, execv, execvp…)? Solo hay una llamada al sistema de verdad (execve); la biblioteca de C ofrece varias funciones que se diferencian en cómo le pasas los argumentos. Las letras lo dicen:

LetraSignificaEjemplo
l (list)Los argumentos van uno a uno, terminando en NULLexeclp("date", "date", NULL)
v (vector)Los argumentos van en un array de char * terminado en NULLexecvp("ls", args) con char *args[] = {"ls", "-l", NULL}
p (PATH)Busca el programa en las carpetas de la variable PATHCon p basta "ls"; sin p hay que poner "/bin/ls"

Olvidar que el primer argumento de la lista es el nombre del programa. En execlp("date", "date", NULL) el primer "date" es el programa que se busca y el segundo es argv[0] para ese programa. Si escribes execlp("ls", "-l", NULL), ls recibe -l como su nombre y ninguna opción.

No terminar la lista o el array con NULL. exec no sabe cuántos argumentos hay: sigue leyendo hasta encontrar un NULL. Sin él, lee basura de la memoria.

fork + exec: así lanza programas tu shell

Si exec sustituye al proceso, ¿cómo ejecuta la shell ls sin desaparecer ella misma? Con la pareja que lo explica todo: primero **fork() (crea un hijo) y luego exec en el hijo** (el hijo se convierte en ls). El padre, la shell, sigue vivo y espera. Lo empaquetamos en una función lanzar:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/* lanza una orden en un hijo, la espera y devuelve su código de salida */
int lanzar(char *argv[]) {
    pid_t pid = fork();
    if (pid == 0) {
        /* el hijo se convierte en la orden; si exec vuelve, es que ha fallado */
        execvp(argv[0], argv);
        perror(argv[0]);
        exit(127);   /* 127: el código que usa la shell para "orden no encontrada" */
    }
    int estado;
    waitpid(pid, &estado, 0);
    return WEXITSTATUS(estado);
}

int main(void) {
    char *orden1[] = {"uname", "-s", NULL};
    char *orden2[] = {"cafetera", "--encender", NULL};

    int c1 = lanzar(orden1);
    printf("uname terminó con código %d\n", c1);
    int c2 = lanzar(orden2);
    printf("cafetera terminó con código %d\n", c2);
    printf("El jefe sigue vivo: PID %d\n", getpid());
    return 0;
}

Salida real:

Linux
uname terminó con código 0
cafetera: No such file or directory
cafetera terminó con código 127
El jefe sigue vivo: PID 536

Vamos por partes:

  • Linux lo escribe uname -s, que es en lo que se convirtió el primer hijo. Termina bien y su código de salida es 0.
  • cafetera no existe en ninguna carpeta del PATH, así que execvp falla y vuelve. Por eso se ejecuta perror(argv[0]), que imprime cafetera: No such file or directory, y el hijo termina con exit(127), el mismo código que usa bash cuando no encuentra una orden.
  • El jefe sigue vivo al final, con su PID: ningún exec lo ha tocado, porque los exec se hicieron en los hijos.

Volvamos a ver la función clave con lupa:

int lanzar(char *argv[]) {
    pid_t pid = fork();
    if (pid == 0) {
        /* el hijo se convierte en la orden; si exec vuelve, es que ha fallado */
        execvp(argv[0], argv);
        perror(argv[0]);
        exit(127);   /* 127: el código que usa la shell para "orden no encontrada" */
    }
    int estado;
    waitpid(pid, &estado, 0);
    return WEXITSTATUS(estado);
}

Si el exit(127) no estuviera, un hijo cuyo exec falla seguiría ejecutando el resto de lanzar como si fuera el padre: haría su propio waitpid (sin hijos que esperar) y volvería al main a lanzar más órdenes. Tendrías dos «jefes» a la vez. Ese exit tras el exec es obligatorio.

El ciclo completo de un hijo queda así:

Ciclo de vida de un proceso hijo El padre crea un hijo con fork, el hijo se convierte en otro programa con exec, termina con exit, queda zombi y el padre lo recoge con wait. fork, exec, exit y wait: el ciclo completo Padre el programa que ya corría Hijo copia exacta del padre Programa nuevo mismo PID, otro código Zombi ya terminó; guarda su código Padre recoge WEXITSTATUS(estado) fork() exec() exit(código) wait(&estado) el zombi desaparece al recogerlo exec no crea procesos: sustituye el programa. Por eso casi siempre va después de fork()
Cuatro llamadas, cuatro papeles: crear, sustituir, terminar y recoger.

wait(), exit() y el código de salida

exit(n) termina el proceso y deja un número, el código de salida, para que lo lea el padre. Por convenio, 0 significa «todo bien» y cualquier otro valor, «algo ha ido mal» (o un dato pequeño, como veremos en la práctica). wait(&estado) hace dos cosas: bloquea al padre hasta que termine uno de sus hijos y rellena el entero estado con información sobre cómo terminó.

Y aquí está la trampa: estado no es el código de salida. Es un entero con varios datos empaquetados. Mira:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/* interpreta el entero que rellena wait() */
void informar(int estado) {
    if (WIFEXITED(estado)) {
        printf("  terminó con exit, código de salida %d\n", WEXITSTATUS(estado));
    } else if (WIFSIGNALED(estado)) {
        printf("  lo terminó la señal %d\n", WTERMSIG(estado));
    }
}

int main(void) {
    int estado;

    /* hijo 1: termina con exit(7) */
    if (fork() == 0) {
        exit(7);
    }
    wait(&estado);
    printf("Hijo 1: estado en bruto = %d\n", estado);
    informar(estado);

    /* hijo 2: abort() le envía la señal SIGABRT (la 6) */
    if (fork() == 0) {
        abort();
    }
    wait(&estado);
    printf("Hijo 2: estado en bruto = %d\n", estado);
    informar(estado);
    return 0;
}

Salida real:

Hijo 1: estado en bruto = 1792
  terminó con exit, código de salida 7
Hijo 2: estado en bruto = 6
  lo terminó la señal 6

El hijo 1 hizo exit(7) y el estado en bruto vale 1792. ¿De dónde sale? 7 × 256 = 1792. En Linux, el código de salida se guarda en el segundo byte del entero (desplazado 8 bits), y el byte bajo se reserva para otros datos, como la señal que mató al proceso. Por eso se usan macros para leerlo:

MacroQué te diceEn el ejemplo
WIFEXITED(estado)¿Terminó con exit o return del main?Hijo 1: sí
WEXITSTATUS(estado)El código que pasó a exit (solo si WIFEXITED)7
WIFSIGNALED(estado)¿Lo terminó una señal?Hijo 2: sí
WTERMSIG(estado)El número de esa señal6 (SIGABRT)

El hijo 2 llamó a abort(), que le envía la señal SIGABRT. No terminó con exit, así que WIFEXITED es falso y miramos WTERMSIG: 6. El estado en bruto vale 6 porque la señal va en el byte bajo. (Si tu sistema guarda volcados de memoria, core dumps, puede salirte 134 = 128 + 6: se activa además un bit que indica «hubo core dump». Las macros te dan 6 igualmente: para eso están.)

Imprimir estado directamente y creer que es el código de salida. Si ves números como 256, 512 o 1792 donde esperabas 1, 2 o 7, te falta WEXITSTATUS.

El código de salida solo tiene un byte: de 0 a 255. Si haces exit(300), al padre le llega 300 − 256 = 44. Para mandar datos grandes hacen falta otros mecanismos.

Cuando hay varios hijos, wait() devuelve el PID del primero que termine, sea cual sea. Si quieres esperar a uno concreto, usa waitpid:

LlamadaEspera a…Devuelve
wait(&estado)Cualquier hijo (el primero que acabe)Su PID, o -1 si no quedan hijos
waitpid(pid, &estado, 0)El hijo con ese PIDEse PID, o -1 si hay error
waitpid(-1, &estado, 0)Cualquier hijo (igual que wait)Su PID
wait(NULL)Cualquier hijo, sin guardar el estadoSu PID

Y tres formas de terminar, que la práctica distingue:

FormaQué hace
return n; en mainEquivale a exit(n)
exit(n) (<stdlib.h>, C estándar)Hace limpieza: vacía los búferes de printf y cierra ficheros, y luego termina
_exit(n) (<unistd.h>, POSIX)Vuelve al núcleo inmediatamente, sin vaciar búferes

Zombis: hijos que nadie ha recogido

Un hijo que termina no puede desaparecer sin más: tiene que entregar su código de salida a alguien. Hasta que el padre hace wait, el sistema lo guarda en un estado especial: zombi. Ya no ejecuta nada ni usa CPU, pero sigue ocupando una entrada en la tabla de procesos. Podemos verlo con ps desde el propio programa:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    pid_t hijo = fork();
    if (hijo == 0) {
        /* el hijo termina enseguida */
        exit(0);
    }
    /* el padre tarda un segundo en hacer wait(): mientras tanto, el hijo es un zombi */
    sleep(1);

    char orden[64];
    snprintf(orden, sizeof orden, "ps -o pid,ppid,stat,comm -p %d,%d", getpid(), hijo);

    printf("Antes de wait:\n");
    fflush(stdout);   /* vaciamos el búfer antes de que ps escriba */
    system(orden);

    wait(NULL);       /* recogemos al hijo: deja de ser zombi */

    printf("Después de wait:\n");
    fflush(stdout);
    system(orden);
    return 0;
}

Salida real:

Antes de wait:
  PID  PPID STAT COMMAND
  552   387 S    zombi
  553   552 Z    zombi
Después de wait:
  PID  PPID STAT COMMAND
  552   387 S    zombi

Antes de wait, el hijo 553 aparece con estado Z (zombi) y su padre es 552. El padre está en S (sleeping: dormido, esperando, que es lo que hace mientras corre system). Después de wait, el 553 ya no existe: el padre lo ha recogido. Los fflush(stdout) están para que «Antes de wait:» salga antes que la salida de ps aunque redirijas el programa a un fichero.

¿Y si el padre nunca hace wait? Mientras el padre viva, el zombi se queda. Cuando el padre termina, sus hijos pasan a ser hijos de init, que los recoge automáticamente. El problema real son los programas que no terminan nunca (un servidor que crea un hijo por cliente): si no recogen a sus hijos, acumulan zombis.

¿Cuándo uso cada llamada?

Quiero…UsoRecuerda
Saber quién soy y quién es mi padregetpid(), getppid()Los números cambian en cada ejecución
Hacer dos cosas a la vez con el mismo programafork()Distingue padre e hijo con if (pid == 0)
Ejecutar otro programa sin perder el míofork() y exec en el hijoTras el exec, perror + exit por si falla
Convertirme en otro programaexec (sin fork)No vuelve si funciona
Esperar a que acabe un hijo cualquierawait(&estado)Devuelve el PID del que acabó
Esperar a un hijo concretowaitpid(pid, &estado, 0)Útil para recoger en un orden fijo
Devolver un número pequeño al padreexit(n) + WEXITSTATUSSolo de 0 a 255

Errores típicos

El hijo no termina con exit() y sigue ejecutando el código del padre. Dentro de un bucle, eso multiplica los procesos (lo verás en los ejercicios).

Esperar que padre e hijo compartan variables. Cada uno tiene su copia desde el fork().

Poner código importante después de un exec que funciona. Nunca se ejecuta.

Leer estado sin WEXITSTATUS y obtener múltiplos de 256.

Dar por hecho el orden de la salida entre padre e hijo sin ningún wait().

Míralo paso a paso en Python Tutor

Python Tutor ejecuta el código línea a línea y dibuja las variables, los structs y los punteros (elige C en el desplegable de lenguaje). Ejecuta tu programa como un único proceso, así que fork() no se puede ver allí. Lo que sí podemos hacer es simular lo que hace fork() con la memoria: un struct que representa al proceso y una copia de ese struct que hace de hijo.

#include <stdio.h>

/* un modelo de proceso: Python Tutor ejecuta un solo proceso, así que simulamos fork() */
struct proceso {
    int pid;
    int ppid;
    int contador;
};

int main(void) {
    struct proceso padre = {100, 1, 1};

    /* fork() hace una copia del proceso: el hijo nace con los mismos datos */
    struct proceso hijo = padre;
    hijo.pid = 101;          /* ...salvo su PID */
    hijo.ppid = padre.pid;   /* ...y su padre */

    /* a partir de aquí, cada uno cambia su copia */
    hijo.contador += 5;
    padre.contador += 1;

    printf("padre: pid=%d contador=%d\n", padre.pid, padre.contador);
    printf("hijo: pid=%d ppid=%d contador=%d\n", hijo.pid, hijo.ppid, hijo.contador);
    return 0;
}

Al ejecutarlo:

padre: pid=100 contador=2
hijo: pid=101 ppid=100 contador=6

Qué observar paso a paso:

  1. Cuando se crea padre, fíjate en sus tres campos: pid 100, ppid 1 y contador 1.
  2. En la línea struct proceso hijo = padre; aparece un segundo struct con exactamente los mismos valores. No es una flecha hacia padre: es una copia, igual que la memoria del hijo tras fork().
  3. Al cambiar hijo.pid y hijo.ppid, solo cambia el struct de abajo. Es lo único que el sistema cambia en el hijo real: su PID y su padre.
  4. Con hijo.contador += 5 y padre.contador += 1 verás 6 y 2, cada uno en su caja. Es el mismo 6 y 2 del programa de la memoria, y el mismo motivo por el que el jefe del gancho vio 0.

Resumen

ConceptoEn una frase
ProcesoUn programa en ejecución, con su PID, su memoria y su padre
getpid() / getppid()Mi PID / el PID de mi padre
fork()Copia el proceso; devuelve 0 al hijo, el PID del hijo al padre y -1 si falla
Memoria tras fork()Copiada, no compartida: cada uno ve solo sus cambios
execSustituye el programa del proceso; mismo PID; no vuelve si funciona
fork + execLo que hace la shell para lanzar cada orden
exit(n)Termina y deja un código de 0 a 255
wait / waitpidEsperan a un hijo y rellenan estado (léelo con WEXITSTATUS)
ZombiHijo terminado que el padre aún no ha recogido con wait
HuérfanoHijo cuyo padre terminó antes; lo adopta init

Tres ideas para llevarte

  1. **fork() devuelve dos veces**, una en cada proceso, y el if (pid == 0) decide qué hace cada uno. El hijo termina con exit() dentro de su rama.
  2. Copiar no es compartir. Lo que cambia el hijo no lo ve el padre; para devolver un dato pequeño está exit(n) + WEXITSTATUS.
  3. **exec sustituye, no crea.** Para ejecutar otro programa sin desaparecer: fork(), exec en el hijo y wait en el padre.

Ahora toca ponerlo en marcha en la práctica de procesos en C, donde el jefe lanza a varios cocineros a la vez, recoge sus tiempos con exit y monta un pase que ejecuta órdenes de verdad. Y en el siguiente tema daremos el paso que falta: las llamadas al sistema para trabajar con ficheros (open, read, write y los descriptores).

Siguiente paso: prácticaProcesos en Linux con C — práctica con un jefe de cocina, cocineros que trabajan a la vez y un pase que lanza órdenes de verdad (y el servicio que cerró antes de tiempo)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

2 comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *