⏱️ Lectura: 16 min

Cada vez que un proceso en Linux llama a fork(), el kernel crea un hijo completo en microsegundos sin copiar memoria. El secreto detrás es la técnica copy-on-write: padre e hijo comparten las mismas páginas físicas hasta que alguno escribe en ellas.

📑 En este artículo
  1. TL;DR
  2. ¿Qué es la técnica copy-on-write?
  3. Por qué importa la copia diferida de memoria
  4. Cómo funciona COW (copy-on-write) por dentro
    1. La página como unidad mínima
    2. El fallo de página que dispara la copia
    3. mmap() con MAP_PRIVATE también usa COW
  5. Ejemplos prácticos
    1. Fork básico: dos procesos, un mismo punto de partida
    2. Memoria compartida hasta que alguien escribe
    3. El patrón real: workers que heredan datos sin copiarlos
  6. Cómo empezar
    1. Clonar un archivo sin copiarlo (btrfs o xfs)
    2. Verificar que una página sigue compartida
  7. Casos de uso reales
  8. Errores comunes y buenas prácticas
  9. Comparativa con alternativas
  10. Profundizando: qué pasa a nivel de MMU
  11. Preguntas frecuentes
    1. ¿Qué diferencia hay entre la técnica copy-on-write y clonar un archivo completo?
    2. ¿Por qué Python multiprocessing no logra el ahorro completo que promete la copia diferida de memoria?
    3. ¿Funciona COW (copy-on-write) en Windows?
    4. ¿Redis usa COW (copy-on-write) para los snapshots RDB?
    5. ¿Qué pasa si el sistema se queda sin memoria mientras copia páginas por duplicación perezosa de datos?
    6. ¿Qué sistemas de archivos soportan la duplicación perezosa de datos a nivel de bloque?
  12. Referencias

Git, Docker y ZFS usan la misma idea para evitar copiar archivos completos. Redis la aprovecha para guardar una foto de la base de datos sin bloquear las escrituras.

TL;DR

  • Copy-on-write deja que dos procesos compartan la misma página de memoria hasta que uno la modifica.
  • fork() en Linux crea un proceso hijo completo sin duplicar memoria porque hereda las páginas del padre.
  • Docker copia el archivo completo a la capa de escritura al modificar un solo byte, no solo el bloque cambiado.
  • Redis usa fork() para tomar una foto consistente de la base de datos sin bloquear las escrituras.
  • cp –reflink=auto clona un archivo de cualquier tamaño en milisegundos en sistemas btrfs o xfs porque no copia bloques.

¿Qué es la técnica copy-on-write?

La técnica copy-on-write es un mecanismo que posterga la copia de un dato hasta que alguien intenta modificarlo. Mientras solo se lee, varios consumidores comparten la misma copia física; en el instante de la escritura, el sistema crea una copia privada solo de esa porción modificada.

El mecanismo aplica a dos niveles distintos. En memoria, la unidad que se comparte es la página, un bloque de 4 KB que administra el sistema operativo. En almacenamiento, la unidad son los bloques del disco o, en el caso de Docker, el archivo completo dentro de una capa.

En ambos casos el ahorro es el mismo. Se evita una copia costosa cuando lo más probable es que nadie necesite modificar ese dato. Si nadie escribe nunca, la copia jamás ocurre.

Por qué importa la copia diferida de memoria

Sin copy-on-write, cada fork() tendría que duplicar toda la memoria del proceso padre antes de arrancar el hijo. Un servidor con varios gigabytes de memoria residente tardaría un tiempo notable en clonarse, y gastaría esa misma cantidad de RAM por cada hijo, incluso si el hijo solo iba a ejecutar un comando corto y salir.

Con copy-on-write, el fork() es casi instantáneo porque solo copia las estructuras de control del proceso (tabla de páginas, descriptores de archivo) y marca las páginas de datos como compartidas. El costo real se paga después, de forma gradual, solo por las páginas que efectivamente cambian.

Lo mismo aplica a nivel de almacenamiento. Clonar una imagen de máquina virtual, tomar un snapshot de una base de datos o crear un contenedor a partir de una imagen serían operaciones lentas y costosas en espacio si cada clon implicara copiar bytes reales. Con COW (copy-on-write), esas operaciones son casi gratuitas hasta que algo cambia.

Cómo funciona COW (copy-on-write) por dentro

La página como unidad mínima

El kernel de Linux organiza la memoria de un proceso en páginas de 4 KB. Cada página tiene una entrada en la tabla de páginas del proceso que apunta a una dirección física en RAM y lleva permisos de lectura, escritura o ejecución.

Cuando un proceso hace fork(), el kernel no copia esas páginas. En su lugar, copia la tabla de páginas del hijo apuntando a las mismas direcciones físicas que el padre, pero marca todas esas entradas como solo lectura, incluso si originalmente permitían escritura.

Un fork() típico en Linux tarda microsegundos porque copia estructuras de control, no datos. Foto de Markus Winkler en Unsplash
flowchart TD
    A["Proceso padre en memoria"] --> B["fork()"]
    B --> C["Proceso hijo: mismas paginas, solo lectura"]
    C --> D{"Alguien escribe en una pagina?"}
    D -- "No" --> E["Las paginas siguen compartidas"]
    D -- "Si" --> F["El kernel copia solo esa pagina"]
    F --> G["Padre e hijo ya tienen paginas privadas distintas"]

El fallo de página que dispara la copia

Cuando padre o hijo intentan escribir en una de esas páginas marcadas como solo lectura, la MMU (la unidad de gestión de memoria del procesador) detecta la violación y genera un fallo de página. El kernel atiende ese fallo, asigna una página física nueva, copia el contenido viejo ahí y actualiza la tabla de páginas del proceso que escribió para que apunte a la copia privada.

El otro proceso nunca se enteró de nada. Sigue apuntando a la página original, que ahora es exclusivamente suya. El kernel solo copia esa página de 4 KB, no los gigabytes completos del proceso.

sequenceDiagram
    participant P as Proceso
    participant M as MMU
    participant K as Kernel
    P->>M: escribe en una pagina compartida
    M-->>K: genera un fallo de pagina
    K->>K: copia la pagina a una direccion fisica nueva
    K-->>M: actualiza la tabla de paginas del proceso
    M-->>P: la escritura se completa en la copia privada
    Note over P,K: el resto de las paginas sigue compartido

mmap() con MAP_PRIVATE también usa COW

No solo fork() se apoya en este mecanismo. Cuando un programa mapea un archivo con mmap(..., MAP_PRIVATE, ...), el kernel comparte las páginas del archivo entre todos los procesos que lo mapearon así, pero si alguno escribe, esa página se copia y se vuelve privada sin tocar el archivo en disco. Así comparten los cargadores de bibliotecas dinámicas el segmento de código de una librería entre decenas de procesos, mientras mantienen privado el segmento de datos de cada uno.

Ejemplos prácticos

Estos tres ejemplos en Python van de lo más simple a un patrón real de producción. Todos requieren Linux o macOS, porque Windows no implementa fork().

Fork básico: dos procesos, un mismo punto de partida

import os

pid = os.fork()
if pid == 0:
    print(f'Hijo: PID {os.getpid()}')
else:
    print(f'Padre: PID {os.getpid()}, hijo PID {pid}')

Este script crea un proceso hijo que imprime su propio PID mientras el padre imprime el suyo y el de su hijo. La salida real varía porque los PID los asigna el sistema, pero siempre sigue este patrón:

Padre: PID 48213, hijo PID 48214
Hijo: PID 48214

Memoria compartida hasta que alguien escribe

import os

datos = [0] * 10_000_000  # una lista grande en memoria

pid = os.fork()
if pid == 0:
    print('Hijo ve el primer valor:', datos[0])
    os._exit(0)
else:
    os.waitpid(pid, 0)
    print('Padre sigue con la misma lista sin duplicarla')

El hijo solo lee la lista, nunca la modifica, así que nunca dispara una copia de página. La salida es siempre esta:

Hijo ve el primer valor: 0
Padre sigue con la misma lista sin duplicarla

El patrón real: workers que heredan datos sin copiarlos

import multiprocessing as mp

dataset_grande = {'usuarios': list(range(5_000_000))}

def procesar(worker_id):
    total = len(dataset_grande['usuarios'])
    return f'worker {worker_id} vio {total} usuarios'

if __name__ == '__main__':
    ctx = mp.get_context('fork')
    with ctx.Pool(4) as pool:
        resultados = pool.map(procesar, range(4))
    for r in resultados:
        print(r)

Con el contexto fork, los cuatro procesos del pool heredan dataset_grande por copy-on-write en lugar de recibirlo serializado por cada worker. La salida:

worker 0 vio 5000000 usuarios
worker 1 vio 5000000 usuarios
worker 2 vio 5000000 usuarios
worker 3 vio 5000000 usuarios

Cómo empezar

Dependencias: un sistema de archivos con soporte COW (btrfs o xfs con reflink) para el primer ejemplo, y Python 3 con acceso a os.fork() para el segundo. ext4 no soporta reflink.

Clonar un archivo sin copiarlo (btrfs o xfs)

# crea un archivo de prueba de 1 GB
fallocate -l 1G original.img

# copia COW: no lee el gigabyte completo
cp --reflink=auto original.img copia.img

# tamaño aparente vs espacio real en disco
du -h --apparent-size copia.img
du -h copia.img

El primer du muestra el tamaño lógico del archivo. El segundo muestra el espacio real que ocupa en disco, que apenas cambia hasta que modificás la copia:

1.0G    copia.img
4.0K    copia.img

Verificar que una página sigue compartida

grep -E 'Shared_Clean|Private_Dirty' /proc/$PID/smaps_rollup

Reemplazá $PID por el PID real del proceso. Mientras las páginas sigan compartidas, Shared_Clean va a ser alto y Private_Dirty cercano a cero. Un ejemplo de salida:

Shared_Clean:      81920 kB
Private_Dirty:         0 kB

Casos de uso reales

Redis y el guardado RDB. Para escribir una foto completa de la base de datos a disco sin bloquear las escrituras entrantes, Redis hace fork() de sí mismo. El proceso hijo ve una copia congelada de toda la memoria gracias a COW, mientras el padre sigue atendiendo comandos; si el padre modifica una clave, esa página se duplica, pero el resto sigue compartido.

Las capas de una imagen de Docker son de solo lectura; solo la capa superior se puede escribir. Foto de CHUTTERSNAP en Unsplash

Docker y overlay2. Las imágenes de Docker se componen de capas de solo lectura apiladas. Cuando un contenedor modifica un archivo, overlay2 copia ese archivo completo a la capa de escritura del contenedor antes de aplicar el cambio. Esa operación se llama copy-up. Docker también soporta un driver sobre Btrfs que sí copia a nivel de bloque, pero overlay2 es el predeterminado por ser más simple de administrar.

flowchart TD
    subgraph Imagen
    L1["Capa base: sistema operativo"]
    L2["Capa 2: dependencias"]
    L3["Capa 3: aplicacion"]
    end
    L1 --> L2 --> L3
    L3 --> W["Capa de escritura del contenedor"]
    W -. "copy-up al modificar un archivo" .-> L3

Git y los clones locales. Cuando clonás un repositorio local con git clone –local, Git enlaza los objetos del repositorio original mediante hard links en lugar de copiarlos byte a byte. Ambos repositorios comparten el mismo inodo hasta que uno de los dos corre git gc o reescribe un objeto.

ZFS y Btrfs. Un snapshot de ZFS o Btrfs no copia ningún bloque al crearse. El sistema de archivos solo congela los punteros existentes; los bloques nuevos que escribís después del snapshot se asignan en otro lugar, y el snapshot sigue viendo los bloques viejos intactos. ZFS lleva la idea más lejos: su estructura en disco es un árbol de bloques copy-on-write, donde cada escritura crea bloques nuevos y una nueva raíz del árbol, mientras la raíz anterior sigue intacta mientras algo la referencie.

Errores comunes y buenas prácticas

El refcounting de Python rompe el ahorro. CPython guarda un contador de referencias dentro de cada objeto. Cada vez que un worker hijo solo lee un objeto, Python igual incrementa ese contador, y esa escritura al contador basta para disparar la copia de la página completa. En la práctica, un pool de workers que solo lee datos termina copiando buena parte del heap de todas formas.

💡 Tip: para mitigar esto en Python, llamá a gc.freeze() antes del fork(). Congela los objetos existentes fuera del rastreo del recolector de basura y reduce las escrituras de contador que los tocan.

Docker copia el archivo entero, no el bloque. A diferencia de Btrfs o ZFS, overlay2 hace copy-up a nivel de archivo completo. Modificar un byte de un archivo grande dentro de un contenedor copia el archivo completo a la capa de escritura, no solo el fragmento cambiado.

⚠️ Ojo: si tu contenedor escribe con frecuencia en archivos grandes, montá un volumen en vez de confiar en la capa de escritura del contenedor.

Los hilos no necesitan COW. Los hilos de un mismo proceso ya comparten el mismo espacio de memoria sin ningún truco adicional. Copy-on-write resuelve un problema distinto: compartir memoria entre procesos separados que antes eran uno solo.

El sobrecompromiso de memoria puede fallar tarde. El kernel permite que fork() prometa más memoria de la que hay disponible, confiando en que no todas las páginas se van a escribir. Si muchos procesos hijos escriben a la vez y disparan copias simultáneas, el sistema puede quedarse sin RAM real. El kernel pondera cada proceso con un oom_score basado en cuánta memoria usa y un ajuste manual, y el proceso con el puntaje más alto es el primero en caer, sin importar si fue el que disparó las copias.

Comparativa con alternativas

Cada mecanismo resuelve el mismo problema (compartir datos sin duplicarlos) a un nivel distinto del sistema.

MecanismoQué comparteCuándo copiaEjemplo real
fork() con COWPáginas de memoria del procesoAl escribir en una página (fallo de página)Redis, servidores preforked como gunicorn
Hilos (threads)Todo el espacio de memoria del procesoNunca copia; requiere locks para evitar colisionesServidores multihilo en Java o Go
Memoria compartida explícita (mmap/shm)Una región de memoria elegida a manoNunca copia; la sincronización la maneja la appBases de datos con buffer pool compartido
COW a nivel de bloque (Btrfs/ZFS)Bloques del discoAl escribir en un bloque modificadoSnapshots de volúmenes
Copy-up a nivel de archivo (overlay2)Archivos completos de una capaAl abrir el archivo en modo escrituraContenedores Docker

Profundizando: qué pasa a nivel de MMU

La tabla de páginas de cada proceso vive en memoria y la consulta la MMU en cada acceso. Cada entrada incluye un bit de permiso de escritura y, en muchas arquitecturas, un bit dirty que marca si la página ya fue modificada desde que se cargó. Copy-on-write apaga el bit de escritura aunque la página original lo tuviera activado, y esa discrepancia es la que provoca el fallo de página cuando alguien intenta escribir.

Linux además distingue entre fork() y vfork(). Esta segunda variante, mucho menos usada, ni siquiera crea una tabla de páginas separada para el hijo. Suspende al padre y deja que el hijo use literalmente la misma memoria hasta que llama a exec() o termina. Es más rápida que fork() con COW, pero también más riesgosa, porque un error en el hijo puede corromper la memoria del padre.

El conteo de cuántos procesos comparten una página física vive en una estructura del kernel llamada struct page. Cuando ese contador llega a uno, significa que ya nadie más apunta ahí, y el kernel puede tratar esa página como privada sin mantener el permiso de solo lectura.

📖 Resumen en Telegram: Ver resumen

Tu próximo paso: corré el ejemplo de cp --reflink=auto en una partición btrfs o xfs y compará el tiempo contra cp --reflink=never sobre el mismo archivo para ver la diferencia con tus propios ojos.

📬 Recibí lo nuevo en tu email

Te avisamos de artículos grandes (1-2 por mes).

Preguntas frecuentes

¿Qué diferencia hay entre la técnica copy-on-write y clonar un archivo completo?

Clonar un archivo completo copia todos los bytes de inmediato, sin importar si alguna vez se van a modificar. La técnica copy-on-write posterga esa copia hasta el momento exacto en que algo cambia, y en muchos casos esa copia nunca llega a ocurrir.

¿Por qué Python multiprocessing no logra el ahorro completo que promete la copia diferida de memoria?

Porque el recolector de basura de CPython modifica el contador de referencias de cada objeto incluso cuando un worker solo lo lee, y esa escritura dispara la copia de la página que contiene ese contador.

¿Funciona COW (copy-on-write) en Windows?

Windows no implementa fork() como Linux, pero sí usa copy-on-write internamente en ciertas rutas de creación de procesos, y ReFS soporta clones de bloque a nivel de sistema de archivos similares a los de Btrfs.

¿Redis usa COW (copy-on-write) para los snapshots RDB?

Sí. Redis llama a fork() antes de escribir el archivo RDB, y el proceso hijo recorre una copia congelada de la base de datos gracias a COW mientras el padre sigue respondiendo comandos normalmente.

¿Qué pasa si el sistema se queda sin memoria mientras copia páginas por duplicación perezosa de datos?

Si el sistema permitió sobrecomprometer memoria y de golpe muchos procesos escriben a la vez, puede no haber RAM física suficiente para todas las copias. El kernel mata procesos para liberar memoria según su oom_score, y no necesariamente elige al proceso que causó el problema.

¿Qué sistemas de archivos soportan la duplicación perezosa de datos a nivel de bloque?

Btrfs y ZFS son los más extendidos en Linux, ambos con snapshots y clones instantáneos. XFS sumó soporte de reflink más tarde, y ReFS lo ofrece en Windows Server. ext4 no lo soporta.

Referencias

  • Wikipedia: artículo general sobre copy-on-write en sistemas operativos y almacenamiento.
  • man7.org: página de manual de fork() en Linux.
  • docs.python.org: documentación oficial de os.fork() en Python.
  • docs.docker.com: cómo funciona el driver de almacenamiento overlay2.
  • redis.io: documentación de persistencia RDB y el uso de fork().
  • git-scm.com: documentación de git clone y la opción –local.
  • gnu.org: manual de GNU coreutils sobre cp y la bandera –reflink.

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Imagen destacada: Foto de Liam Briese en Unsplash

¿Te sirvió? ¿Te dio otro error? Contalo abajo: las preguntas se responden y le sirven al siguiente que llegue.

Dejar un comentario

Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Podés incluir código entre <code>…</code> o, para varias líneas, <pre><code>…</code></pre>.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.