Procesos 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)
La primera versión de mi cocina tenía un jefe muy maleducado. Lanzaba a tres cocineros con fork(), imprimía «Servicio cerrado» y terminaba. En la terminal volvía a aparecer el prompt… y, un par de segundos después, empezaban a salir mensajes de cocineros terminando sus platos por encima de lo que yo estaba escribiendo. El jefe se había ido a casa con la cocina llena. Me faltaba lo más importante: que el jefe esperase a cada cocinero con wait() antes de cerrar. Esa lección (el padre espera, recoge y solo entonces sigue) es el hilo de los tres programas de hoy.
Esta práctica pone en marcha todo lo de la teoría de procesos en C. Son tres programas de nivel medio, todos en la misma cocina: el jefe lanza a varios cocineros a la vez y los recoge según terminan; después, los cocineros le devuelven sus tiempos con exit(); y por último el jefe monta un «pase» que ejecuta órdenes reales de Linux (wc, grep, head…) con fork + exec, incluidas una que falla y otra que ni existe, porque los programas de verdad tienen que saber qué hacer cuando algo sale mal.
Tabla de Contenidos
Antes de empezar
Conviene tener claro:
- Qué devuelve
fork()a cada proceso (0 al hijo, el PID del hijo al padre, -1 si falla) y por qué el hijo termina conexit(). - Que la memoria se copia: lo que cambia un hijo no lo ve el padre.
- Que
wait()devuelve el PID del primer hijo que termina ywaitpid()espera a uno concreto. - Que el código de salida se lee con
WEXITSTATUS(estado)y solo va de 0 a 255.
Si alguno te suena dudoso, repasa la teoría antes de seguir.
Compila cada programa con gcc -Wall -std=c11 cocina.c -o cocina y ejecútalo con ./cocina desde una terminal de Linux (en Windows, con WSL). Los PID que veas serán distintos de los míos y cambiarán en cada ejecución: es normal, los reparte el sistema.
Programa 1: el servicio de cocina
El jefe abre el servicio con tres cocineros: paella, ensalada y croquetas. Los tres trabajan a la vez y cada plato tarda un tiempo distinto (3, 1 y 2 segundos, simulados con sleep). El jefe tiene que esperar a los tres y, cada vez que uno termina, decir qué cocinero era. Lo que lo complica: wait() devuelve un PID, no «el cocinero 2», y además los cocineros no terminan en el orden en que se crearon.
La idea
Primero, un bucle que crea a los tres cocineros. Dentro, el if (pids[i] == 0) separa el código del cocinero, que acaba siempre en exit(0). El jefe guarda cada PID en un array pids para poder reconocer después a quién recoge. Segundo, otro bucle que hace wait() tres veces (una por hijo) y busca el PID devuelto en el array para traducirlo a «cocinero número tal».
Antes de programar, piensa en quién ejecuta cada línea. El primer bucle lo empieza solo el jefe, pero cada fork() crea un proceso que también está «dentro» del bucle. Si ese proceso no sale con exit(), seguirá dando vueltas y creando cocineros.
El código
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define N 3
int main(void) {
const char *platos[N] = {"paella", "ensalada", "croquetas"};
int segundos[N] = {3, 1, 2}; /* lo que tarda cada plato (simulado con sleep) */
pid_t pids[N]; /* el jefe apunta el PID de cada cocinero */
printf("Jefe %d: abro el servicio con %d cocineros\n", getpid(), N);
for (int i = 0; i < N; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork");
exit(1);
}
if (pids[i] == 0) {
/* CÓDIGO DEL COCINERO: prepara su plato y se va */
printf(" Cocinero %d (PID %d): empiezo %s\n", i + 1, getpid(), platos[i]);
sleep(segundos[i]);
printf(" Cocinero %d (PID %d): termino %s\n", i + 1, getpid(), platos[i]);
/* imprescindible: sin exit, el hijo seguiría el bucle y crearía más cocineros */
exit(0);
}
}
/* CÓDIGO DEL JEFE: espera a los N cocineros en el orden en que acaben */
for (int k = 0; k < N; k++) {
pid_t fin = wait(NULL);
/* busca en la lista qué cocinero tenía ese PID */
int num = -1;
for (int i = 0; i < N; i++) {
if (pids[i] == fin) {
num = i + 1;
}
}
printf("Jefe: recojo al cocinero %d (PID %d)\n", num, fin);
}
printf("Jefe: servicio cerrado\n");
return 0;
}
El programa tiene dos partes muy distintas. La primera es el bucle que crea a los cocineros:
for (int i = 0; i < N; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork");
exit(1);
}
if (pids[i] == 0) {
/* CÓDIGO DEL COCINERO: prepara su plato y se va */
printf(" Cocinero %d (PID %d): empiezo %s\n", i + 1, getpid(), platos[i]);
sleep(segundos[i]);
printf(" Cocinero %d (PID %d): termino %s\n", i + 1, getpid(), platos[i]);
/* imprescindible: sin exit, el hijo seguiría el bucle y crearía más cocineros */
exit(0);
}
}
El jefe da tres vueltas y en cada una hace un fork(). El valor devuelto se guarda directamente en pids[i]: en el jefe será el PID del cocinero nuevo; en el cocinero será 0. Por eso el if (pids[i] == 0) solo lo cumple el cocinero recién nacido, que imprime, duerme lo que tarda su plato y sale con exit(0). Nunca vuelve al bucle. El jefe, en cambio, no entra en el if y pasa directamente a la siguiente vuelta, así que crea a los tres casi a la vez.
Y una pregunta que surge siempre: si el hijo es una copia del jefe, ¿no tiene también su array pids? Sí, pero su copia está a medio rellenar y no la usa para nada: el array útil es el del jefe.
La segunda parte es la recogida:
/* CÓDIGO DEL JEFE: espera a los N cocineros en el orden en que acaben */
for (int k = 0; k < N; k++) {
pid_t fin = wait(NULL);
/* busca en la lista qué cocinero tenía ese PID */
int num = -1;
for (int i = 0; i < N; i++) {
if (pids[i] == fin) {
num = i + 1;
}
}
printf("Jefe: recojo al cocinero %d (PID %d)\n", num, fin);
}
printf("Jefe: servicio cerrado\n");
Tres wait(NULL), uno por cocinero. Cada wait bloquea al jefe hasta que termina algún hijo y devuelve su PID en fin. El bucle de dentro recorre pids buscando ese número; cuando lo encuentra, num pasa a valer la posición + 1 (los cocineros se numeran desde 1). El -1 inicial es un valor «imposible» que delataría un PID desconocido.
Salida
Jefe 469: abro el servicio con 3 cocineros Cocinero 1 (PID 470): empiezo paella Cocinero 3 (PID 472): empiezo croquetas Cocinero 2 (PID 471): empiezo ensalada Cocinero 2 (PID 471): termino ensalada Jefe: recojo al cocinero 2 (PID 471) Cocinero 3 (PID 472): termino croquetas Jefe: recojo al cocinero 3 (PID 472) Cocinero 1 (PID 470): termino paella Jefe: recojo al cocinero 1 (PID 470) Jefe: servicio cerrado
Comprobémosla con el reloj. Los tres empiezan en el segundo 0 (el orden de las tres líneas «empiezo» lo decide el planificador: en mi ejecución salió el 3 antes que el 2). La ensalada tarda 1 s, las croquetas 2 s y la paella 3 s, así que terminan en el orden 2, 3, 1, y el jefe los recoge exactamente en ese orden: wait() devuelve al que acaba primero, no al primero que se creó. «Servicio cerrado» sale lo último, cuando ya no queda nadie en la cocina. Todo el programa tarda unos 3 segundos, no 3 + 1 + 2 = 6, porque los cocineros trabajan en paralelo.
La traza de la recogida, con los PID de mi ejecución (470, 471 y 472 para los cocineros 1, 2 y 3):
Vuelta k | wait devuelve fin | Búsqueda en pids | num | Mensaje |
|---|---|---|---|---|
| 0 (segundo 1) | 471 | 470 ≠, 471 =, 472 ≠ | 2 | recojo al cocinero 2 |
| 1 (segundo 2) | 472 | 470 ≠, 471 ≠, 472 = | 3 | recojo al cocinero 3 |
| 2 (segundo 3) | 470 | 470 =, 471 ≠, 472 ≠ | 1 | recojo al cocinero 1 |
Preguntas que te puedes hacer
**¿Por qué el jefe hace exactamente N wait?** Porque cada wait recoge a un hijo. Con menos, algún cocinero se quedaría zombi hasta que el jefe terminara; con más, el wait sobrante devolvería -1 enseguida (no quedan hijos), así que no se bloquea, pero tendrías un PID -1 que no encuentra a nadie.
¿Por qué guardar los PID en un array? Porque wait() solo te da un número. Sin el array, el jefe sabría que «ha terminado el proceso 471», pero no qué plato era. Es la misma idea que una comanda numerada.
¿Por qué los mensajes de los cocineros llevan dos espacios delante? Solo para leer mejor la salida: así se distinguen de un vistazo los mensajes del jefe y los de los cocineros, que salen mezclados porque trabajan a la vez.
¿Y si dos cocineros tardaran lo mismo? Terminarían casi a la vez y el orden entre ellos dependería del planificador: unas veces recogerías antes a uno y otras al otro. El programa seguiría siendo correcto porque busca cada PID, no supone un orden.
**¿Qué pasa si quito el exit(0) del cocinero?** Que el cocinero 1, al terminar su plato, volvería al bucle con i = 1 y crearía su propio cocinero, y así sucesivamente. Lo verás con todo detalle en los ejercicios.
Poner el wait() dentro del primer bucle, justo después del fork(). El programa funciona, pero el jefe espera a cada cocinero antes de crear el siguiente: los platos se hacen uno detrás de otro y tardas 6 segundos en vez de 3. Has perdido la gracia de tener varios procesos.
Pruébalo tú
- Añade un cuarto cocinero (
"flan", 4 segundos) cambiando soloNy los dos arrays. Predice el orden de recogida antes de ejecutar. - Cambia
wait(NULL)porwaitpid(pids[k], NULL, 0)y observa que ahora el jefe recoge en el orden 1, 2, 3, aunque el cocinero 1 sea el último en terminar.
Programa 2: cuánto ha tardado cada plato
Ahora el jefe quiere números. Cuatro cocineros (paella, ensalada, croquetas y flan) tardan 25, 8, 12 y 15 minutos, y cada uno le devuelve su tiempo al jefe. El jefe los suma y calcula la media por plato. El problema es el del gancho de la teoría: una variable compartida no sirve, porque la memoria se copia. Hay que usar el único canal de vuelta que tenemos por ahora: el código de salida de exit().
La idea
Cada cocinero hace exit(minutos[i]): su código de salida es el dato. El jefe recoge a cada uno con waitpid(pids[i], &estado, 0), en el orden de la lista para que la suma acumulada salga ordenada, comprueba con WIFEXITED que terminó bien y saca los minutos con WEXITSTATUS. Al final, total / N con decimales.
Antes de programar, piensa en el límite del canal. El código de salida es un byte: 25, 8, 12 y 15 caben de sobra, pero un tiempo de 300 minutos llegaría como 44.
El código
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define N 4
int main(void) {
const char *platos[N] = {"paella", "ensalada", "croquetas", "flan"};
int minutos[N] = {25, 8, 12, 15}; /* cada cocinero lo devuelve con exit() */
pid_t pids[N];
for (int i = 0; i < N; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork");
exit(1);
}
if (pids[i] == 0) {
printf(" Cocinero %d: %s en %d minutos\n", i + 1, platos[i], minutos[i]);
/* el código de salida es un número de 0 a 255: sirve para mandar un dato pequeño */
exit(minutos[i]);
}
}
int total = 0;
for (int i = 0; i < N; i++) {
int estado;
/* waitpid espera a ESE cocinero: así recogemos en el orden de la lista */
waitpid(pids[i], &estado, 0);
if (WIFEXITED(estado)) {
int m = WEXITSTATUS(estado); /* el número que el hijo pasó a exit() */
total += m;
printf("Jefe: %s -> %d min (acumulado %d)\n", platos[i], m, total);
}
}
/* (double) para que la división no se haga con enteros */
printf("Jefe: %d minutos en total, %.1f de media por plato\n", total, (double)total / N);
return 0;
}
La parte nueva está en la recogida:
int total = 0;
for (int i = 0; i < N; i++) {
int estado;
/* waitpid espera a ESE cocinero: así recogemos en el orden de la lista */
waitpid(pids[i], &estado, 0);
if (WIFEXITED(estado)) {
int m = WEXITSTATUS(estado); /* el número que el hijo pasó a exit() */
total += m;
printf("Jefe: %s -> %d min (acumulado %d)\n", platos[i], m, total);
}
}
waitpid(pids[i], &estado, 0) espera a ese cocinero en concreto. Si el de la ensalada termina antes que el de la paella (seguro, porque no hacen nada más que imprimir), da igual: su estado queda guardado hasta que el jefe llegue a pedirlo, y el jefe pide en el orden de la lista. WIFEXITED(estado) comprueba que el hijo terminó con exit (no por una señal) y solo entonces tiene sentido WEXITSTATUS(estado), que saca el byte con el código. Cada valor se suma a total.
Así se ve el entero estado del cocinero de la paella por dentro:
El código de salida 25 ocupa el byte alto (bits 8 a 15), y por eso el entero completo vale 25 × 256 = 6400. WEXITSTATUS hace justo la operación del dibujo: desplazar 8 bits a la derecha y quedarse con un byte.
Salida
Cocinero 1: paella en 25 minutos Cocinero 2: ensalada en 8 minutos Cocinero 3: croquetas en 12 minutos Jefe: paella -> 25 min (acumulado 25) Jefe: ensalada -> 8 min (acumulado 33) Jefe: croquetas -> 12 min (acumulado 45) Cocinero 4: flan en 15 minutos Jefe: flan -> 15 min (acumulado 60) Jefe: 60 minutos en total, 15.0 de media por plato
Comprobación a mano: 25 + 8 = 33, 33 + 12 = 45 y 45 + 15 = 60 minutos, los mismos acumulados que imprime el jefe. La media: 60 / 4 = 15,0 minutos por plato. Ese (double)total es imprescindible: sin él, 60 / 4 se haría con enteros (aquí da exacto, pero con 61 minutos saldría 15 en lugar de 15,25).
Fíjate también en el orden de las líneas. Los mensajes del jefe salen siempre en el orden de la lista porque usa waitpid con cada PID. Los de los cocineros, en cambio, salen cuando cada uno consigue la CPU: en mi ejecución, el cocinero 4 habló después de que el jefe ya hubiera recogido a tres. Lo único garantizado es que «Cocinero 4: flan…» sale antes que «Jefe: flan…», porque el jefe no puede recoger al cocinero 4 hasta que ha terminado (y para terminar, primero imprime).
Vuelta i | Plato | waitpid espera a | WEXITSTATUS | total |
|---|---|---|---|---|
| 0 | paella | cocinero 1 | 25 | 25 |
| 1 | ensalada | cocinero 2 | 8 | 33 |
| 2 | croquetas | cocinero 3 | 12 | 45 |
| 3 | flan | cocinero 4 | 15 | 60 |
Preguntas que te puedes hacer
**¿Por qué waitpid y no wait?** Con wait también recogerías a los cuatro, pero en el orden en que terminan, y tendrías que buscar cada PID en el array para saber qué plato era (como en el programa 1). Con waitpid el orden lo pones tú y el acumulado sale en el orden de la carta.
**¿Para qué sirve WIFEXITED si todos hacen exit?** Para no fiarse. Si un cocinero muriera por una señal (un fallo de segmentación, por ejemplo), WEXITSTATUS daría un valor sin sentido y lo sumarías como si fueran minutos. Con el if, ese cocinero simplemente no cuenta.
**¿Por qué no usar un array global minutos_reales que rellenen los hijos?** Porque cada hijo rellenaría su copia. Es exactamente el error del contador del gancho de la teoría.
¿Qué pasaría con un plato de 300 minutos? Que al jefe le llegaría 44 (300 − 256) y el total saldría mal sin ningún aviso. Para datos así hacen falta ficheros o tuberías, que llegarán en los próximos temas.
Sumar estado en lugar de WEXITSTATUS(estado). Con 25 minutos sumarías 6400: un servicio de varios días.
Pruébalo tú
- Haz que el jefe imprima también el plato más lento y el más rápido, guardando el máximo y el mínimo mientras recoge.
- Cambia el tiempo del flan a 300 y comprueba que el total sale 89 en lugar de 345: al jefe le llegan 44 minutos (300 − 256), y 25 + 8 + 12 + 44 = 89.
Programa 3: el pase de la cocina
El pase es el sitio donde el jefe revisa las comandas antes de que salgan. Nuestro jefe primero apunta las comandas del día en un fichero comandas.txt y luego lanza cinco órdenes de Linux, una detrás de otra, como lo haría una shell: cuenta las líneas (wc -l), cuenta las pizzas (grep -c pizza), mira las dos primeras comandas (head -n 2), intenta leer un fichero de reservas que no existe (cat reservas.txt) y prueba a ejecutar lavavajillas, un programa que no está instalado. De cada orden apunta el código de salida y al final cuenta cuántas han dado problemas.
La idea
Una función ejecutar que hace lo que hace la shell con cada orden: fork(), execvp en el hijo (con perror + exit(127) por si el exec falla) y waitpid en el padre, devolviendo el código de salida. Otra función mostrar imprime la orden con un $ delante para que la salida parezca una terminal. Las órdenes van en un array de arrays de char *, cada una terminada en NULL, que es justo lo que pide execvp.
Antes de programar, piensa en las dos formas de «fallar». Si cat se ejecuta pero no encuentra el fichero, el exec ha funcionado y es cat quien devuelve un código distinto de 0. Si lavavajillas no existe, es el exec el que falla, y el código lo pone tu programa.
El código
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
/* escribe por pantalla la orden como la verías en la terminal: $ wc -l comandas.txt */
void mostrar(char *argv[]) {
printf("$");
for (int i = 0; argv[i] != NULL; i++) {
printf(" %s", argv[i]);
}
printf("\n");
fflush(stdout); /* que salga antes de que el hijo escriba */
}
/* fork + exec + waitpid: lo mismo que hace la shell con cada orden */
int ejecutar(char *argv[]) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1;
}
if (pid == 0) {
execvp(argv[0], argv);
/* si seguimos aquí, la orden no existe o no se pudo ejecutar */
perror(argv[0]);
exit(127);
}
int estado;
waitpid(pid, &estado, 0);
return WIFEXITED(estado) ? WEXITSTATUS(estado) : -1;
}
int main(void) {
/* 1. el jefe apunta las comandas del día en un fichero */
FILE *f = fopen("comandas.txt", "w");
if (f == NULL) {
perror("comandas.txt");
return 1;
}
fprintf(f, "mesa 1: paella\nmesa 2: pizza margarita\nmesa 3: croquetas\nmesa 4: pizza cuatro quesos\n");
fclose(f);
/* 2. las órdenes que va a lanzar, cada una terminada en NULL */
char *ordenes[][5] = {
{"wc", "-l", "comandas.txt", NULL},
{"grep", "-c", "pizza", "comandas.txt", NULL},
{"head", "-n", "2", "comandas.txt", NULL},
{"cat", "reservas.txt", NULL},
{"lavavajillas", NULL},
};
int n = sizeof ordenes / sizeof ordenes[0];
/* 3. una a una: lanzar, esperar y apuntar cómo ha ido */
int fallos = 0;
for (int i = 0; i < n; i++) {
mostrar(ordenes[i]);
int codigo = ejecutar(ordenes[i]);
printf(" -> código de salida %d\n", codigo);
if (codigo != 0) {
fallos++;
}
}
printf("Pase terminado: %d órdenes, %d con problemas\n", n, fallos);
return 0;
}
El corazón del programa es ejecutar:
/* fork + exec + waitpid: lo mismo que hace la shell con cada orden */
int ejecutar(char *argv[]) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1;
}
if (pid == 0) {
execvp(argv[0], argv);
/* si seguimos aquí, la orden no existe o no se pudo ejecutar */
perror(argv[0]);
exit(127);
}
int estado;
waitpid(pid, &estado, 0);
return WIFEXITED(estado) ? WEXITSTATUS(estado) : -1;
}
Es la función lanzar de la teoría con dos mejoras. La primera, el caso pid < 0: si no se puede crear el hijo, devuelve -1 en lugar de seguir con un PID inválido. La segunda, el return final: WIFEXITED(estado) ? WEXITSTATUS(estado) : -1 devuelve el código solo si el hijo terminó con exit; si lo mató una señal, devuelve -1. El operador ? : es un if/else en una expresión: «si se cumple lo de la izquierda, vale lo del medio; si no, lo de la derecha».
mostrar tiene un detalle que no se ve a simple vista:
/* escribe por pantalla la orden como la verías en la terminal: $ wc -l comandas.txt */
void mostrar(char *argv[]) {
printf("$");
for (int i = 0; argv[i] != NULL; i++) {
printf(" %s", argv[i]);
}
printf("\n");
fflush(stdout); /* que salga antes de que el hijo escriba */
}
El bucle recorre argv hasta encontrar el NULL final (el mismo que necesita execvp) y va imprimiendo cada palabra con un espacio delante. El fflush(stdout) asegura que la línea $ … sale antes que lo que escriba el hijo. En una terminal, printf ya vacía el búfer al llegar a \n, pero si rediriges la salida a un fichero (./pase > informe.txt) el búfer se vacía solo cuando se llena, y la línea del $ podría acabar detrás de la salida del hijo o incluso copiada en él con el fork().
Por último, la tabla de órdenes:
/* 2. las órdenes que va a lanzar, cada una terminada en NULL */
char *ordenes[][5] = {
{"wc", "-l", "comandas.txt", NULL},
{"grep", "-c", "pizza", "comandas.txt", NULL},
{"head", "-n", "2", "comandas.txt", NULL},
{"cat", "reservas.txt", NULL},
{"lavavajillas", NULL},
};
int n = sizeof ordenes / sizeof ordenes[0];
char *ordenes[][5] es un array de filas, y cada fila tiene 5 punteros a texto. Las filas cortas se rellenan solas con NULL (en C, lo que no inicializas en un array con llaves vale 0), así que todas acaban en NULL aunque solo lo escribamos una vez. sizeof ordenes / sizeof ordenes[0] calcula cuántas filas hay: el tamaño total entre el tamaño de una fila. Si añades una orden, n se actualiza solo.
Salida
$ wc -l comandas.txt 4 comandas.txt -> código de salida 0 $ grep -c pizza comandas.txt 2 -> código de salida 0 $ head -n 2 comandas.txt mesa 1: paella mesa 2: pizza margarita -> código de salida 0 $ cat reservas.txt cat: reservas.txt: No such file or directory -> código de salida 1 $ lavavajillas lavavajillas: No such file or directory -> código de salida 127 Pase terminado: 5 órdenes, 2 con problemas
Orden por orden:
wc -lcuenta 4 líneas: las cuatro mesas que escribió elfprintf. Código 0.grep -c pizzacuenta 2: la mesa 2 (margarita) y la mesa 4 (cuatro quesos). Código 0.head -n 2enseña las dos primeras líneas tal cual. Código 0.cat reservas.txt: elexecfuncionó (catexiste), perocatno encuentra el fichero, escribe su propio mensaje de error y termina con 1. Ese 1 lo decidecat.lavavajillas: no hay ningún programa con ese nombre en elPATH,execvpfalla y vuelve, nuestroperrorescribelavavajillas: No such file or directoryy el hijo sale con 127. Ese 127 lo decide nuestro código.
Total: 5 órdenes, 2 con código distinto de 0. Los mensajes de error salen en inglés porque los escribe el sistema (perror y cat usan el idioma configurado; en mi ejecución, el idioma por defecto, que es inglés).
| Vuelta | Orden | ¿exec funciona? | Código | fallos |
|---|---|---|---|---|
| 0 | wc -l comandas.txt | sí | 0 | 0 |
| 1 | grep -c pizza comandas.txt | sí | 0 | 0 |
| 2 | head -n 2 comandas.txt | sí | 0 | 0 |
| 3 | cat reservas.txt | sí (falla cat) | 1 | 1 |
| 4 | lavavajillas | no | 127 | 2 |
Preguntas que te puedes hacer
**¿Por qué el exec se hace en un hijo y no en el propio jefe?** Porque exec sustituye al proceso. Si el jefe hiciera execvp("wc", …), se convertiría en wc, contaría las líneas y terminaría: las otras cuatro órdenes no se lanzarían nunca.
**¿Por qué waitpid justo después de cada fork?** Porque queremos que las órdenes se ejecuten una detrás de otra, como en la terminal. Si lanzáramos las cinco y esperásemos al final, sus salidas se mezclarían y no sabríamos qué código es de cada una sin buscar los PID.
¿Por qué 127 y no 1? Por convenio: bash usa 127 para «orden no encontrada». Así, quien lea el código distingue «la orden falló» (1, 2…) de «la orden ni siquiera existe» (127).
**¿grep -c cuenta pizzas o líneas?** Líneas que contienen el patrón, no apariciones. Aquí da igual porque cada mesa pide como mucho una pizza, pero una línea mesa 5: pizza y pizza contaría 1.
**¿Qué pasa si el hijo hace exec y luego quiero saber su PID?** Sigue siendo el mismo: exec no cambia el PID. El waitpid(pid, …) del padre funciona aunque el hijo ya sea wc.
Olvidar el exit(127) después del perror. Cuando lavavajillas falla, el hijo volvería de ejecutar al bucle del main y seguiría lanzando órdenes como si fuera el jefe: el pase se ejecutaría dos veces a la vez.
echo $? en la terminal te enseña el código de salida de la última orden. Prueba cat reservas.txt; echo $? y verás el mismo 1 que recogió el jefe.
Pruébalo tú
- Añade la orden
{"sort", "comandas.txt", NULL}y una con tres argumentos más, como{"grep", "-n", "-i", "PIZZA", "comandas.txt", NULL}(ojo: necesitarás filas de 6 o más punteros). - Haz que el jefe pare el pase en cuanto una orden devuelva 127 («falta una máquina en la cocina») con un
break.
Errores típicos en esta práctica
- **El hijo sin
exit():** sigue el bucle del padre y crea procesos de más, o ejecuta código que solo debía hacer el padre. - **
wait()dentro del bucle de creación:** todo funciona, pero en serie; pierdes el paralelismo. - **Sumar
estadoen lugar deWEXITSTATUS(estado):** los números salen multiplicados por 256. - **Devolver con
exitun número mayor que 255:** llega recortado (300 → 44) sin ningún aviso. - **Olvidar el
NULLfinal de los argumentos deexecvp:**execsigue leyendo memoria basura. - Dar por hecho el orden de los mensajes de los hijos: solo está garantizado lo que ordenas con
wait/waitpid.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta el código línea a línea y dibuja cada variable (elige C en el desplegable). No puede crear procesos de verdad, así que en lugar de fork() simulamos lo que hacen exit() y WEXITSTATUS con los minutos de tres cocineros: cómo se mete el código en el entero estado y cómo se vuelve a sacar.
#include <stdio.h>
/* simulamos lo que hacen exit() y WEXITSTATUS en Linux con tres cocineros */
int main(void) {
int minutos[3] = {25, 8, 12};
int total = 0;
for (int i = 0; i < 3; i++) {
/* así guarda el sistema el código de salida dentro de estado */
int estado = (minutos[i] & 0xFF) << 8;
/* y así lo saca WEXITSTATUS */
int m = (estado >> 8) & 0xFF;
total += m;
printf("cocinero %d: estado=%d -> %d min (total %d)\n", i + 1, estado, m, total);
}
return 0;
}
Salida:
cocinero 1: estado=6400 -> 25 min (total 25) cocinero 2: estado=2048 -> 8 min (total 33) cocinero 3: estado=3072 -> 12 min (total 45)
Qué observar paso a paso:
- En la primera vuelta, cuando se calcula
estado, verás 6400: el 25 desplazado 8 bits, igual que en el diagrama de los bits. - En la línea de
m, el valor vuelve a ser 25. Esa línea es literalmente lo que haceWEXITSTATUS. - Mira cómo crece
total: 25, 33, 45. Es la misma suma acumulada del programa 2 (sin el flan). - Compara
estadoymen cada vuelta: 2048 frente a 8 y 3072 frente a 12. Si hubieras sumadoestado, el total sería 11520 en lugar de 45.
Resumen
- Para que varios hijos trabajen a la vez, se crean todos en un bucle y se recogen después, en otro bucle con un
waitpor hijo. wait()devuelve al primero que termina; guardar los PID en un array te permite saber quién era.waitpid(pid, &estado, 0)recoge a un hijo concreto, y con él decides el orden de la recogida.exit(n)+WIFEXITED+WEXITSTATUSes el canal más sencillo para que un hijo devuelva un número de 0 a 255.fork+execen el hijo +waitpiden el padre es lo que hace una shell con cada orden; tras elexecvan siempreperroryexit(127).- Un código distinto de 0 puede venir del programa ejecutado (
cat→ 1) o de que elexecfalló (127, puesto por ti).
Checklist: ¿lo has practicado?
- ¿Has ejecutado los tres programas y comprobado las salidas?
- ¿Sabrías explicar por qué el servicio del programa 1 tarda 3 segundos y no 6?
- ¿Puedes decir de memoria qué valor tiene
estadocuando un hijo haceexit(12)? - ¿Distingues un 1 de
catde un 127 de tu propio código? - ¿Has intentado los «Pruébalo tú» sin mirar la teoría?
Si has respondido que sí a casi todo, estás listo para los ejercicios de procesos en C, donde ya no te doy el código hecho. ¡Nos vemos allí!
Siguiente paso: ejerciciosProcesos en Linux con C — ejercicios con cocineros que se multiplican, búferes que se duplican y códigos de salida que mienten (y la cocina que contrató a siete cocineros)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
