1. Qué es fork
La llamada al sistema fork() crea un proceso nuevo duplicando el proceso que la llama. El nuevo proceso (el hijo) es una copia casí exacta del proceso original (el padre): tiene la misma memoria, los mismos archivos abiertos, las mismás variables de entorno, el mismo directorio de trabajo. La unica diferencia es que fork devuelve valores distintos en cada uno, y el hijo tiene un PID nuevo.
fork es la forma en que unix crea procesos. No hay una syscall "crear proceso y ejecutar programa": en su lugar, fork duplica el proceso actual, y luego (Sí se quiere) se usa exec para reemplazar el programa del hijo por otro. Esta separación entre "crear" y "ejecutar" es una de las decisiones de diseno más importantes de unix, y la veremos en el tema 17.
La firma de fork es simple:
#include
pid_t fork(void);
No recibe argumentos. Devuelve un pid_t: el PID del hijo en el padre, 0 en el hijo, o -1 Sí fallo. Es la unica syscall que retorna dos veces: una en el padre y una en el hijo.
Casí todos los procesos de un sistema unix se crean mediante fork (o clone, Que es la version generalizada que usa linux). La shell usa fork para ejecutar cada comando. Los servidores usan fork para atender conexiones. Los demonios usan fork para crear procesos trabajadores. Entender fork es entender como funcióna unix.
2. Una llamada, dos retornos
La carácteristica más sorprendente de fork es que retorna dos veces: una en el padre y una en el hijo. Ambas llamadas retornan de la misma instrucción fork(), pero con valores distintos:
- En el padre, fork devuelve el PID del hijo (un número positivo).
- En el hijo, fork devuelve 0.
- Si falla, fork devuelve -1 en el padre (no se crea hijo).
Esto significa que, Después de fork(), el código que sigue se ejecuta en dos procesos distintos. El programa debe usar el valor de retorno para decidir que hace cada uno:
pid_t pid = fork();
if (pid < 0) {
/* Error: no se pudo crear el hijo */
perror("fork");
return 1;
} else if (pid == 0) {
/* Este código se ejecuta en el HIJO */
printf("Soy el hijo, PID %d\n", getpid());
} else {
/* Este código se ejecuta en el PADRE */
printf("Soy el padre, hijo PID %d\n", pid);
}
Despues del if/else, ambos procesos continuan ejecutando el mismo código (el que sigue después del bloque). Si no se separan con if/else o con return/_exit, ambos ejecutaran el mismo código, lo que suele ser un error.
3. El valor de retorno
El valor de retorno de fork es la clave para distinguir padre de hijo:
| Valor devuelto | En que proceso | Significado |
|---|---|---|
| Mayor que 0 | Padre | El PID del hijo recien creado. El padre puede usarlo para esperarlo con waitpid(). |
| 0 | Hijo | Indica que este código se ejecuta en el hijo. El hijo puede obtener su propio PID con getpid(). |
| -1 | Padre | Error: no se pudo crear el hijo. errno indica la causa (EAGAIN: limite de procesos, ENOMEM: Sin memoria). |
El hijo no necesita el valor de retorno para saber su PID: puede llamar a getpid(). Y el padre no necesita llamar a getpid() para saber el PID del hijo: ya lo tiene en el valor de retorno. Esta asimetria es deliberada: el padre necesita el PID del hijo para esperarlo, y el hijo puede obtener el suyo cuándo lo necesite.
Si fork falla, no se crea ningun hijo. El padre recibe -1 y errno se establece. Las causas más comunes son: limite de procesos por usuario (ulimit -u), falta de memoria, o limite del sistema (pid_max).
4. Qué se copia
fork crea una copia del proceso padre. Lo que se copia y lo que se comparte es:
| Recurso | Que pasa | Detalle |
|---|---|---|
| Memoria (Código, datos, heap, stack) | Se copia (con copia en escritura) | El hijo recibe una copia del espacio de direcciónes del padre. Las páginas se comparten hasta que uno de los dos las modifica (tema 13). |
| Archivos abiertos | Se comparten | El hijo hereda los mismos descriptores, apuntando a las mismás estructuras del núcleo. Si uno modifica la posicion de lectura, el otro la ve. |
| Variables de entorno | Se copian | El hijo recibe una copia del entorno del padre. |
| Directorio de trabajo | Se copia | El hijo hereda el cwd del padre. |
| Directorio raiz | Se copia | El hijo hereda el root del padre. |
| umask | Se copia | El hijo hereda la umask del padre. |
| Senales y manejadores | Se copian | El hijo hereda los manejadores de senales del padre. |
| Limites de recursos | Se copian | El hijo hereda los rlimit del padre. |
| PID | Nuevo | El hijo recibe un PID nuevo. |
| PPID | Se establece | El PPID del hijo es el PID del padre. |
| Timers | No se heredan | Los temporizadores del padre no se copian al hijo. |
| Bloqueos de archivo | No se heredan | Los bloqueos (flock) del padre no se copian. |
La copia de la memoria es la operación más costosa, pero gracias a la copia en escritura (copy-on-write), fork no copia ficamente las páginas: Sólo crea una nueva tabla de páginas que apunta a las mismás páginas ficas, marcadas como sólo lectura. Las páginas se copian sólo cuándo uno de los dos procesos las modifica. Esto hace que fork sea rápido incluso para procesos con mucha memoria.
5. El proceso hijo
El proceso hijo es un proceso completo e independiente. Tiene su propio PID, su propio espacio de direcciónes, su propia cola de planificacion. No es un "hilo" del padre: es un proceso separado que comparte (por copia en escritura) la memoria del padre.
El hijo empieza ejecutando desde la instrucción siguiente a fork(), con los mismos valores de variables que tenia el padre en ese momento. A partir de ahí, padre e hijo son independientes: lo que uno modifica no afecta al otro (gracias a la copia en escritura).
El hijo hereda el padre como PPID. Si el padre termina antes que el hijo, el hijo queda huerfano y el núcleo le reasigna como padre al proceso 1 (o a un subreaper). Esto se ve en el tema 26.
El hijo no hereda el estado de los mutex o semaforos del padre (Sí los usaba). Y no hereda los bloqueos de archivo (flock). Estas son excepciones importantes que pueden causar problemás en programás con hilos (tema 36).
Si un proceso con varios hilos llama a fork(), Sólo el hilo que llamo a fork() sobrevive en el hijo. Los demás hilos desaparecen, pero sus bloqueos (mutex, etc.) pueden quedar bloqueados para siempre. Esto es un problema clasico que se trata en el tema 36. La solución recomendada es no llamar a fork() en programás con hilos, o usar pthread_atfork() para preparar el estado.
6. El ejemplo más simple
El ejemplo más simple de fork muestra como padre e hijo ejecutan el mismo código pero con valores distintos:
#define _POSIX_C_SOURCE 200809L
#include
#include
int main(void)
{
printf("Antes de fork: PID %d\n", (int) getpid());
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
/* Este código se ejecuta en AMBOS procesos */
printf("Despues de fork: PID %d, retorno de fork %d\n",
(int) getpid(), (int) pid);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o fork_Simple fork_Simple.c
$ ./fork_Simple
Antes de fork: PID 901
Despues de fork: PID 901, retorno de fork 902
Despues de fork: PID 902, retorno de fork 0
La linea "Antes de fork" aparece una sola vez, porque antes de fork sólo hay un proceso. Las dos lineas "Despues de fork" aparecen porque después de fork hay dos procesos, y ambos ejecutan el printf. El padre muestra su PID (901) y el valor de retorno de fork (902, el PID del hijo). El hijo muestra su PID (902) y el valor de retorno de fork (0).
El orden de las dos lineas "Despues de fork" puede variar: el planificador decide que proceso se ejecuta primero. En este caso, el padre se ejecuto primero, pero podria haber sido al reves.
7. Padre e hijo en la práctica
En la práctica, padre e hijo suelen hacer cosas distintas. El patron más comun es: el padre crea el hijo, el hijo hace una tárea y termina, y el padre espera al hijo y recoge su resultado.
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
int main(void)
{
printf("Padre: PID %d, creando hijo...\n", (int) getpid());
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
/* Hijo: hacer una tárea */
printf("Hijo: PID %d, trabajando...\n", (int) getpid());
sleep(2); /* Simular trabajo */
printf("Hijo: terminando con código 42\n");
_exit(42);
}
/* Padre: esperar al hijo */
printf("Padre: esperando al hijo %d...\n", (int) pid);
int status;
pid_t w = waitpid(pid, &status, 0);
if (w == -1) {
perror("waitpid");
return 1;
}
if (WIFEXITED(status)) {
printf("Padre: hijo %d terminó con código %d\n",
(int) w, WEXITSTATUS(status));
}
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o fork_wait fork_wait.c
$ ./fork_wait
Padre: PID 910, creando hijo...
Padre: esperando al hijo 911...
Hijo: PID 911, trabajando...
Hijo: terminando con código 42
Padre: hijo 911 Terminó con código 42
El hijo hace el trabajo (sleep de 2 Segúndos) y termina con _exit(42). El padre espera con waitpid() y recoge el código de salida (42). El tema 21 profundiza en wait y waitpid.
Nota el uso de _exit() en lugar de exit() en el hijo. _exit() termina el proceso inmediatamente sin ejecutar los manejadores de atexit ni vaciar los buferes de stdio. Es importante usar _exit() en el hijo después de fork, para evitar que los buferes de stdio se vacien dos veces (una en el hijo y otra en el padre). El tema 20 Explica esta diferencia.
8. Observar fork con strace
strace permite ver las syscalls que fork genera por debajo. En linux, fork es en realidad una envoltura de clone():
$ strace -f ./fork_Simple
execve("./fork_Simple", ...) = 0
...
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, ...) = 902
strace: Process 902 attached
[pid 901] wait4(-1, 0x7ffd..., 0, NULL) = ? ERESTARTSYS
[pid 902] write(1, "Despues de fork: PID 902...", 42) = 42
[pid 902] exit_group(0) = ?
[pid 902] +++ exited with 0 +++
[pid 901] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 902
[pid 901] +++ exited with 0 +++
La syscall clone() es la que realmente crea el proceso. fork() es una envoltura de la biblioteca de C Que llama a clone() con los flags apropiados. La opcion -f de strace sigue a los procesos hijos, mostrando las syscalls de ambos.
El flag SIGCHLD Indica que cuándo el hijo termine, se enviara la senal SIGCHLD al padre. El flag CLONE_CHILD_CLEARTID y CLONE_CHILD_SETTID se usan para la sincronizacion de hilos (no relevante para fork normal).
9. Observar fork en /proc
Despues de fork, padre e hijo tienen directorios separados en /proc, con PIDs distintos. Pero comparten el mismo archivo ejecutable (exe) y el mismo código:
$ ./fork_wait &
[1] 910
$ ls -l /proc/910/exe /proc/911/exe
lrwxrwxrwx 1 ana ana 0 ... /proc/910/exe -> /home/ana/procesos/fork_wait
lrwxrwxrwx 1 ana ana 0 ... /proc/911/exe -> /home/ana/procesos/fork_wait
$ grep PPid /proc/911/status
PPid: 910
$ grep threads /proc/910/status /proc/911/status
/proc/910/status:Threads: 1
/proc/911/status:Threads: 1
Los dos procesos tienen el mismo exe (apuntan al mismo archivo), pero PIDs y PPIDs distintos. Cada uno tiene su propio directorio en /proc con su propia información.
10. Errores comunes
fork es simple en concepto, pero tiene varios errores comunes que conviene evitar:
- No comprobar el valor de retorno: Sí fork falla y devuelve -1, el código del hijo se ejecuta en el padre (porque pid == 0 es falso cuándo pid == -1). Siempre hay que comprobar
pid < 0primero. - No separar padre e hijo: Sí después de fork ambos ejecutan el mismo código sin if/else, se duplica el trabajo. Siempre hay que separar con if (pid == 0) / else.
- Usar exit() en lugar de _exit() en el hijo: exit() vacia los buferes de stdio, lo que puede causar que la salida se duplique. Usar _exit() en el hijo.
- No esperar al hijo: Sí el padre no llama a wait/waitpid, el hijo queda como zombi cuándo termina.
- fork() en un programa con hilos: Sólo el hilo que llamo a fork() sobrevive en el hijo. Los mutex bloqueados pueden quedar bloqueados para siempre.
- Compartir descriptores de archivo: el hijo hereda los descriptores del padre. Si ambos escriben en el mismo archivo, las escrituras se intercalan. Usar O_APPEND o cerrar los descriptores no necesarios en el hijo.
Si fork falla y devuelve -1, el código del hijo (if pid == 0) no se ejecuta, pero el código del padre (else) Sí. Pero sí no hay if/else, ambos procesos ejecutan el mismo código. Siempre hay que comprobar pid < 0 antes de pid == 0.
11. Un programa que demuestra fork
El siguiente programa crea un hijo, ambos cuentan hasta 3 con pausas, y el padre espera al hijo. Demuestra que padre e hijo tienen su propia memoria:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
int main(void)
{
int contador = 0; /* variable compartida por copia */
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
/* Hijo */
for (int i = 0; i < 3; i++) {
contador++;
printf("Hijo (PID %d): contador = %d\n", (int) getpid(), contador);
sleep(1);
}
_exit(0);
}
/* Padre */
for (int i = 0; i < 3; i++) {
contador++;
printf("Padre (PID %d): contador = %d\n", (int) getpid(), contador);
sleep(1);
}
wait(NULL);
printf("Padre: hijo terminado. Mi contador final = %d\n", contador);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o fork_demo fork_demo.c
$ ./fork_demo
Padre (PID 920): contador = 1
Hijo (PID 921): contador = 1
Padre (PID 920): contador = 2
Hijo (PID 921): contador = 2
Padre (PID 920): contador = 3
Hijo (PID 921): contador = 3
Padre: hijo terminado. Mi contador final = 3
Ambos procesos tienen su propio contador, Que empieza en 0 y llega a 3. Aunque comparten el mismo código, cada uno tiene su propia copia de la variable. El padre no ve los cambios del hijo, y viceversa. Esto demuestra que fork crea una copia independiente de la memoria.
12. Resumen
- fork() crea un proceso nuevo duplicando el proceso que la llama.
- Una llamada, dos retornos: el padre recibe el PID del hijo, el hijo recibe 0, y -1 Sí fallo.
- El hijo recibe una copia del espacio de direcciónes del padre (con copia en escritura).
- El hijo hereda archivos abiertos, variables de entorno, cwd, umask, senales y limites.
- El hijo no hereda timers ni bloqueos de archivo.
- Siempre hay que comprobar el valor de retorno: < 0 primero, luego == 0, luego > 0.
- El hijo debe usar _exit() en lugar de exit() para evitar duplicar la salida de buferes.
- El padre debe esperar al hijo con wait/waitpid para evitar zombis.
- fork() en un programa con hilos es peligroso: Sólo el hilo que llamo sobrevive.
- En linux, fork es una envoltura de clone() con flags SIGCHLD.
13. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Qué hace fork?
Crea un proceso nuevo duplicando el proceso que la llama. El hijo es una copia del padre con su propia memoria, PID y recursos.
2. Qué devuelve fork en el padre y en el hijo?
En el padre, el PID del hijo (mayor que 0). En el hijo, 0. Si falla, -1 en el padre.
3. Por que se dice que fork "retorna dos veces"?
Porque la misma llamada a fork() retorna una vez en el padre (con el PID del hijo) y otra en el hijo (con 0). Ambos procesos continuan ejecutando desde la misma instrucción.
4. Qué se copia en fork y que se comparte?
Se copia la memoria (con copia en escritura), las variables de entorno, el cwd, la umask, las senales y los limites. Se comparten los descriptores de archivo (apuntan a las mismás estructuras del núcleo).
5. Por que se debe usar _exit() en lugar de exit() en el hijo?
Porque exit() vacia los buferes de stdio, lo que puede causar que la salida se duplique (el hijo y el padre vacian los mismos buferes). _exit() termina inmediatamente sin vaciar buferes.
6. Qué pasa sí el padre no espera al hijo?
El hijo queda como zombi cuándo termina, hasta que el padre lo recoge con wait(). Si el padre termina, el hijo queda huerfano y es adoptado por el proceso 1.
7. Por que fork en un programa con hilos es peligroso?
Porque sólo el hilo que llamo a fork() sobrevive en el hijo. Los demás hilos desaparecen, pero sus bloqueos (mutex, etc.) pueden quedar bloqueados para siempre.
8. Qué syscall usa fork internamente en linux?
clone(), con los flags CLONE_CHILD_CLEARTID, CLONE_CHILD_SETTID y SIGCHLD. fork() es una envoltura de la biblioteca de C.
9. Qué error causa fork?
EAGAIN (limite de procesos por usuario o del sistema) o ENOMEM (Sin memoria suficiente para crear el proceso).
10. Cómo se observa fork con strace?
Con strace -f, Que sigue a los procesos hijos. Se ve la syscall clone() y las syscalls de ambos procesos.
Para prácticar
- Compila y ejecuta fork_Simple.c. Observa que el orden de las lineas puede variar en cada ejecución.
- Compila y ejecuta fork_wait.c. Verifica que el padre recoge el código de salida del hijo.
- Compila y ejecuta fork_demo.c. Verifica que padre e hijo tienen contadores independientes.
- Usa strace -f ./fork_Simple para ver las syscalls que genera fork.
- Escribe un programa que haga fork en un bucle y cuente cuántos procesos se crean. Observa el limite con ulimit -u.
- Modifica fork_wait.c para que el hijo falle (return 1) y verifica que el padre detecta el error.