Copy-on-write: cómo el kernel posterga copiar memoria
@programacionLo esencial
- Copy-on-write comparte páginas de memoria entre procesos y solo copia cuando alguno escribe en ellas.
- fork() en Linux usa copy-on-write para no duplicar toda la memoria del proceso padre al crear el hijo.
- El kernel marca las páginas compartidas como solo lectura y usa un page fault para disparar la copia real.
- Redis usa copy-on-write durante BGSAVE para generar un snapshot en disco sin bloquear las escrituras del proceso principal.
- Procesos que escriben casi toda su memoria justo después de fork() anulan la ventaja de copy-on-write con una ráfaga de copias.
Qué es copy-on-write
Cada vez que un proceso en Linux llama a fork(), el kernel no duplica ni un solo byte de datos. Crea un proceso hijo que apunta a las mismas páginas físicas que el padre.
Copy-on-write (COW) es la estrategia detrás de esa trampa: padre e hijo comparten las páginas marcadas como solo lectura hasta que alguno intenta escribir. Solo en ese momento el kernel copia la página modificada y actualiza las tablas de páginas.
El problema que resuelve: duplicar memoria es caro
Antes de que Unix generalizara copy-on-write, fork() copiaba el espacio de direcciones completo del padre. Un proceso con 2 GB residentes generaba otros 2 GB de copias físicas en el instante, aunque el hijo fuera a llamar a execve() medio milisegundo después y descartar esa memoria.
Para ese caso típico (fork seguido de exec) ya existía vfork(), documentado en la página de manual de fork(2). Pero vfork() bloquea al padre hasta que el hijo ejecuta exec o termina, una restricción incómoda que copy-on-write eliminó de raíz.
Cómo dispara la copia un page fault
Al hacer fork(), el kernel copia las tablas de páginas del padre, no el contenido de la memoria. Marca cada entrada como solo lectura, aunque la página original fuera escribible, y aumenta en uno el contador de referencias de esa página física.
Cuando padre o hijo intentan escribir, la CPU genera un page fault porque la página es de solo lectura. El manejador del kernel revisa el contador: si hay más de un proceso referenciándola, reserva una página nueva, copia el contenido y apunta la tabla del proceso que escribió a esa copia. Si el contador ya es 1, no hace falta copiar nada: simplemente marca la página como escribible.
Ejemplo práctico en C
Este programa ilustra el momento exacto en que diverge la memoria compartida:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int contador_global = 42;
int main(void) {
pid_t hijo = fork();
if (hijo == 0) {
contador_global += 1;
printf("Proceso hijo (pid=%d): contador_global = %d\n", getpid(), contador_global);
} else {
wait(NULL);
printf("Proceso padre (pid=%d): contador_global = %d\n", getpid(), contador_global);
}
return 0;
}Justo después de fork(), ambos procesos ven contador_global en la misma página física con valor 42. La línea contador_global += 1 en el hijo dispara el page fault: el kernel copia esa página, el hijo queda con 43 en su copia privada y el padre conserva 42 en la original.
Cómo verificar las páginas compartidas en tu sistema
Podés observar el efecto sin escribir código. Con un proceso corriendo de PID conocido, revisá su mapa de memoria:
grep -E "Shared_Clean|Private_Dirty" /proc/$PID/smaps | sort -u | head -n 20Antes de que un hijo recién forkeado escriba algo, va a mostrar mucho Shared_Clean y poco Private_Dirty. A medida que escribe más memoria, Private_Dirty crece porque cada escritura convirtió una página compartida en una copia privada.
Dónde se usa en la práctica
Redis aprovecha copy-on-write para generar snapshots sin detener el servidor. Al ejecutar BGSAVE, Redis hace fork() de un proceso hijo que recorre la memoria y la escribe a disco mientras el padre sigue atendiendo escrituras; cada clave que el padre modifica mientras tanto dispara una copia de esa página, así que el hijo conserva una foto consistente del momento del fork. El mecanismo está descrito en la documentación de persistencia de Redis.
Servidores web como Gunicorn o Unicorn forkean varios procesos worker desde un proceso maestro que ya cargó el interprete y el código de la aplicación. Gracias a copy-on-write, esos workers no duplican esa memoria al nacer: la comparten hasta que el recolector de basura o el propio runtime empieza a tocar objetos, momento en el que cada worker va acumulando sus propias páginas privadas.
Cuándo copy-on-write no conviene
Si un proceso escribe casi toda su memoria justo después de fork(), copy-on-write no ahorra nada: produce una ráfaga de page faults que termina copiando lo mismo que se hubiera copiado de entrada, pero con el costo extra de cada fallo de página.
Hay otro gotcha menos obvio: el overcommit de memoria. fork() puede devolver éxito sin reservar físicamente toda la memoria que podría necesitarse si padre e hijo escriben en simultáneo. Si en ese momento el sistema no tiene páginas libres suficientes para satisfacer todas las copias, el OOM killer puede matar a cualquiera de los dos procesos, aunque ninguna llamada de asignación haya fallado explícitamente.
Conclusión
Copy-on-write resume en el kernel una idea simple: no copies hasta que alguien insista en modificar. Multiplicado por cada fork() que ocurre en un servidor, ese retraso es la diferencia entre lanzar un proceso nuevo en microsegundos o pagar el costo de duplicar gigabytes enteros.
📖 Versión extendida con más detalle: https://elsolitario.org/2026/10/05/copy-on-write-la-copia-que-el-kernel-posterga/?utm_source=telegraph&utm_medium=instant_view&utm_campaign=programacion