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.
Tabla de Contenidos
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:
| Orden | Qué enseña |
|---|---|
ps | Los procesos de tu terminal, con su PID |
ps -o pid,ppid,stat,comm | Además, el PID del padre (PPID) y el estado |
pstree -p | El á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 retorno | Quién lo recibe | Qué significa |
|---|---|---|
0 | El hijo | «Soy el hijo» (su PID real lo saca con getpid()) |
| Mayor que 0 | El padre | Es el PID del hijo que acaba de nacer |
-1 | El padre | No 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ó
0defork(), 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.
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:
- 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, **usawait()**. - 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.
La traza de la variable, paso a paso:
| Momento | contador del padre | contador del hijo |
|---|---|---|
Antes de fork() | 1 | (no existe) |
Justo después de fork() | 1 | 1 (copia) |
El hijo hace contador += 5 | 1 | 6 |
El hijo imprime y hace exit(0) | 1 | (termina) |
El padre, tras wait(), hace contador += 1 | 2 | — |
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:
| Letra | Significa | Ejemplo |
|---|---|---|
l (list) | Los argumentos van uno a uno, terminando en NULL | execlp("date", "date", NULL) |
v (vector) | Los argumentos van en un array de char * terminado en NULL | execvp("ls", args) con char *args[] = {"ls", "-l", NULL} |
p (PATH) | Busca el programa en las carpetas de la variable PATH | Con 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:
Linuxlo escribeuname -s, que es en lo que se convirtió el primer hijo. Termina bien y su código de salida es 0.cafeterano existe en ninguna carpeta delPATH, así queexecvpfalla y vuelve. Por eso se ejecutaperror(argv[0]), que imprimecafetera: No such file or directory, y el hijo termina conexit(127), el mismo código que usa bash cuando no encuentra una orden.- El jefe sigue vivo al final, con su PID: ningún
execlo ha tocado, porque losexecse 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í:
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:
| Macro | Qué te dice | En 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ñal | 6 (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:
| Llamada | Espera 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 PID | Ese PID, o -1 si hay error |
waitpid(-1, &estado, 0) | Cualquier hijo (igual que wait) | Su PID |
wait(NULL) | Cualquier hijo, sin guardar el estado | Su PID |
Y tres formas de terminar, que la práctica distingue:
| Forma | Qué hace |
|---|---|
return n; en main | Equivale 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… | Uso | Recuerda |
|---|---|---|
| Saber quién soy y quién es mi padre | getpid(), getppid() | Los números cambian en cada ejecución |
| Hacer dos cosas a la vez con el mismo programa | fork() | Distingue padre e hijo con if (pid == 0) |
| Ejecutar otro programa sin perder el mío | fork() y exec en el hijo | Tras el exec, perror + exit por si falla |
| Convertirme en otro programa | exec (sin fork) | No vuelve si funciona |
| Esperar a que acabe un hijo cualquiera | wait(&estado) | Devuelve el PID del que acabó |
| Esperar a un hijo concreto | waitpid(pid, &estado, 0) | Útil para recoger en un orden fijo |
| Devolver un número pequeño al padre | exit(n) + WEXITSTATUS | Solo 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:
- Cuando se crea
padre, fíjate en sus tres campos:pid100,ppid1 ycontador1. - En la línea
struct proceso hijo = padre;aparece un segundo struct con exactamente los mismos valores. No es una flecha haciapadre: es una copia, igual que la memoria del hijo trasfork(). - Al cambiar
hijo.pidyhijo.ppid, solo cambia el struct de abajo. Es lo único que el sistema cambia en el hijo real: su PID y su padre. - Con
hijo.contador += 5ypadre.contador += 1verá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
| Concepto | En una frase |
|---|---|
| Proceso | Un 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 |
exec | Sustituye el programa del proceso; mismo PID; no vuelve si funciona |
fork + exec | Lo que hace la shell para lanzar cada orden |
exit(n) | Termina y deja un código de 0 a 255 |
wait / waitpid | Esperan a un hijo y rellenan estado (léelo con WEXITSTATUS) |
| Zombi | Hijo terminado que el padre aún no ha recogido con wait |
| Huérfano | Hijo cuyo padre terminó antes; lo adopta init |
Tres ideas para llevarte
- **
fork()devuelve dos veces**, una en cada proceso, y elif (pid == 0)decide qué hace cada uno. El hijo termina conexit()dentro de su rama. - Copiar no es compartir. Lo que cambia el hijo no lo ve el padre; para devolver un dato pequeño está
exit(n)+WEXITSTATUS. - **
execsustituye, no crea.** Para ejecutar otro programa sin desaparecer:fork(),execen el hijo ywaiten 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).
¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

2 comentarios