← Gestión de Memoria Bloque 7 · Memoria virtual · Tema 31 de 52

Tema 31

Copy-on-write y fork

Cuando un proceso llama a fork(), el sistema crea un proceso hijo que es una copia exacta del padre. Copiar toda la memoria sería carísimo. La solución es copy-on-write (COW): padre e hijo comparten las mismas páginas físicas marcadas como solo lectura; solo cuando uno escribe se copia la página. En este tema verás cómo funciona, sus ventajas y su costo.

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():

  1. El SO crea la tabla de páginas del hijo copiando la del padre.
  2. Las entradas se marcan como solo lectura en ambos procesos (R/W=0).
  3. Los marcos se comparten: padre e hijo apuntan a los mismos marcos físicos.
  4. 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.

Copy-on-write: padre e hijo comparten marcos hasta que uno escribe Tabla padre R/W=0 Tabla hijo R/W=0 Marco físico refcount = 2 datos compartidos entrada entrada Tras escritura padre: marco A (rw) hijo: marco B (rw) refcount A=1, B=1
Tras fork, padre e hijo comparten el marco (refcount=2). Al escribir, se copia la página y cada uno tiene su propio marco.

4. Qué ocurre al escribir

Cuando el padre (o el hijo) intenta escribir en una página compartida:

  1. La MMU detecta R/W=0 y lanza una excepción de protección (trap).
  2. El SO interviene: verifica que es una página COW (no un error real).
  3. El SO asigna un nuevo marco y copia el contenido de la página original.
  4. El SO actualiza la tabla del proceso escritor: apunta al nuevo marco, con R/W=1.
  5. El contador de referencias del marco original se decrementa. Si llega a 0, se libera.
  6. 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

6. Desventajas y costos

7. Ejemplo numérico

Un proceso padre usa 100 MiB de RAM. Llama a fork(). Con COW:

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:

cow.c
#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;
}
terminal · copy-on-write en acción
$ 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

MedidaValor
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:

terminal · COW en Linux
$ 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

11. Resumen

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

  1. Modifica cow.c para que el hijo escriba en 0, 100 y 10000 páginas. ¿Cómo cambia el tiempo?
  2. Usa /usr/bin/time -v para medir el uso de memoria con y sin COW.
  3. Explica por qué fork() es rápido con COW aunque el proceso use 1 GiB.
  4. Investiga cómo Linux implementa COW en el manejador de fallos de página.
  5. Dibuja el estado de las tablas de páginas de padre e hijo antes y después de una escritura.
  6. Compara el tiempo de fork() con y sin COW (usando un kernel sin COW, si es posible).
  7. Explica por qué exec() después de fork() es eficiente con COW.