1. El problema: fork y la copia de memoria
fork() crea un proceso hijo que es una copia exacta del padre: mismo código, mismos datos, misma pila, mismo heap. Si el padre usa 100 MiB de RAM, copiar todo al hijo tomaría tiempo y memoria. Además, muchos procesos llaman a exec() inmediatamente después de fork(), descartando la copia.
Copiar toda la memoria en cada fork sería un desperdicio enorme. La solución es no copiar hasta que sea necesario.
2. La idea: copy-on-write
Copy-on-write (COW) es la técnica en la que padre e hijo comparten las mismas páginas físicas inicialmente. Las páginas se marcan como solo lectura en ambos procesos. Cuando uno de los dos intenta escribir, se produce un fallo de página (protección), y el sistema copia la página, dándole al escritor su propia copia (marcada como lectura/escritura). El otro proceso sigue usando la original.
Copy-on-write (COW) es la técnica por la cual dos procesos comparten las mismas páginas físicas marcadas como solo lectura, y se copia una página solo cuando uno de los dos intenta escribirla.
3. Cómo funciona fork con COW
Al llamar a fork():
- El SO crea la tabla de páginas del hijo copiando la del padre.
- Las entradas se marcan como solo lectura en ambos procesos (R/W=0).
- Los marcos se comparten: padre e hijo apuntan a los mismos marcos físicos.
- El contador de referencias de cada marco se incrementa.
No se copia ningún dato. El costo de fork es proporcional al tamaño de la tabla de páginas, no al tamaño de la memoria.
4. Qué ocurre al escribir
Cuando el padre (o el hijo) intenta escribir en una página compartida:
- La MMU detecta R/W=0 y lanza una excepción de protección (trap).
- El SO interviene: verifica que es una página COW (no un error real).
- El SO asigna un nuevo marco y copia el contenido de la página original.
- El SO actualiza la tabla del proceso escritor: apunta al nuevo marco, con R/W=1.
- El contador de referencias del marco original se decrementa. Si llega a 0, se libera.
- La instrucción se reanuda: ahora la escritura tiene éxito en la copia.
El otro proceso sigue usando la página original, sin saber que se copió.
5. Ventajas
- fork() es rápido: no se copia la memoria, solo la tabla de páginas.
- Ahorro de memoria: si el hijo llama a exec() inmediatamente, no se copia nada.
- Compartición: el código y los datos de solo lectura se comparten entre padre e hijo.
- Transparencia: el programa no se entera de que se usó COW.
6. Desventajas y costos
- Costo de la primera escritura: la primera escritura a una página compartida es cara (copia de 4 KiB + actualización de la tabla).
- Overhead de la tabla: la tabla de páginas se copia en cada fork.
- Fragmentación: las páginas copiadas ocupan más memoria.
- No funciona para páginas compartidas explícitamente: las páginas con MAP_SHARED no usan COW.
7. Ejemplo numérico
Un proceso padre usa 100 MiB de RAM. Llama a fork(). Con COW:
- Costo de fork: copiar la tabla de páginas (100 MiB / 4 KiB = 25600 entradas × 8 bytes = 200 KiB). Tiempo: ~1 ms.
- Memoria adicional: 0 (las páginas se comparten).
- Si el hijo escribe en 10 páginas: se copian 10 × 4 KiB = 40 KiB. Costo: ~0,1 ms.
Sin COW: fork copiaría 100 MiB. Tiempo: ~100 ms. Memoria: +100 MiB.
8. Experimento en C
El programa siguiente mide el tiempo de fork y el efecto de escribir en el hijo:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>
static double ahora(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec/1e9; }
int main(void)
{
size_t tam = 100 << 20; /* 100 MiB */
char *p = malloc(tam);
for (size_t i = 0; i < tam; i += 4096) p[i] = 1;
double t0 = ahora();
pid_t pid = fork();
double t1 = ahora();
printf("fork() tardó %.3f ms\\n", (t1 - t0) * 1000);
if (pid == 0) {
/* Hijo: escribir en 1000 páginas */
double t2 = ahora();
for (int i = 0; i < 1000; i++) p[i * 4096] = 2;
double t3 = ahora();
printf("Hijo: escribir 1000 páginas tardó %.3f ms\\n", (t3 - t2) * 1000);
return 0;
}
waitpid(pid, NULL, 0);
return 0;
}
$ gcc -std=c17 -O2 -Wall -Wextra -o cow cow.c
$ ./cow
fork() tardó 0,450 ms
Hijo: escribir 1000 páginas tardó 2,100 msHijo: escribir 1000 páginas tardó 2,100 ms
El fork tardó solo 0,45 ms (copiar la tabla de páginas), aunque el proceso usaba 100 MiB. La escritura en 1000 páginas tardó 2,1 ms (copiar 1000 páginas de 4 KiB). Esto demuestra que COW hace fork rápido y solo paga el costo cuando se escribe.
9. Probar con un simulador
Este simulador muestra el estado de las páginas compartidas entre padre e hijo, y qué ocurre al escribir.
Simulador · copy-on-write
PADRE
HIJO
| Medida | Valor |
|---|---|
| Páginas compartidas | |
| Páginas copiadas (padre) | |
| Páginas copiadas (hijo) |
10. En Linux
Linux implementa COW con la paginación por demanda. Cuando se escribe en una página compartida, se genera un fallo de página (protección), y el manejador copia la página. Las herramientas para observar:
$ strace -e trace=clone,fork ./cow 2>&1 | head
$ /usr/bin/time -v ./cow 2>&1 | grep -i 'max.*resident'
$ grep VmRSS /proc/PID/status
strace: muestra las llamadas al sistema (fork, clone)./usr/bin/time -v: muestra el uso máximo de memoria (Max resident set size)./proc/PID/status: muestra VmRSS del proceso.
11. Resumen
- fork() crea un proceso hijo que comparte las páginas del padre.
- Las páginas se marcan como solo lectura (COW) en ambos procesos.
- Al escribir, se copia la página y se da al escritor su propia copia.
- COW hace fork rápido y ahorra memoria.
- El costo se paga solo cuando se escribe (primera escritura por página).
12. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué es copy-on-write (COW)?
La técnica por la cual dos procesos comparten páginas físicas marcadas como solo lectura, y se copia una página solo cuando uno intenta escribirla.
2. ¿Qué hace fork() con la memoria?
Copia la tabla de páginas del padre al hijo y marca las páginas como solo lectura (compartidas).
3. ¿Se copia la memoria en fork()?
No: las páginas se comparten. Solo se copia la tabla de páginas.
4. ¿Qué ocurre cuando el hijo escribe en una página compartida?
Se produce un fallo de página (protección), el SO copia la página y el hijo recibe su propia copia.
5. ¿Qué ventaja tiene COW?
Hace fork rápido y ahorra memoria, especialmente cuando el hijo llama a exec() inmediatamente.
6. ¿Cuál es el costo de COW?
La primera escritura a una página compartida es cara (copia de 4 KiB).
7. ¿Qué es el refcount de un marco?
El número de procesos que comparten ese marco. Cuando llega a 0, se libera.
8. ¿Funciona COW con mmap MAP_SHARED?
No: las páginas compartidas explícitamente no usan COW; las modificaciones son visibles para todos.
Para practicar
- Modifica
cow.cpara que el hijo escriba en 0, 100 y 10000 páginas. ¿Cómo cambia el tiempo? - Usa
/usr/bin/time -vpara medir el uso de memoria con y sin COW. - Explica por qué fork() es rápido con COW aunque el proceso use 1 GiB.
- Investiga cómo Linux implementa COW en el manejador de fallos de página.
- Dibuja el estado de las tablas de páginas de padre e hijo antes y después de una escritura.
- Compara el tiempo de fork() con y sin COW (usando un kernel sin COW, si es posible).
- Explica por qué exec() después de fork() es eficiente con COW.