1. Qué es la copia en escritura
La copia en escritura (copy-on-write, o COW) es una tetécnica de optimización que permite que fork cree un proceso hijo sin copiar ficamente la memoria del padre. En lugar de copiar todas las páginas, el núcleo hace que padre e hijo compartan 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 intenta modificarlas.
La idea es simple: la mayoria de los procesos que se crean con fork inmediatamente llaman a exec para reemplazar su programa (por ejemplo, la shell crea un hijo para ejecutar "ls"). En ese caso, copiar toda la memoria del padre seria un desperdicio, porque el hijo va a descartarla inmediatamente. Con COW, fork es casí instantaneo, y exec libera las páginas compartidas sin haberlas copiado nunca.
Incluso cuándo el hijo no llama a exec (por ejemplo, un servidor que crea un hijo para atender una conexion), la mayoria de las páginas no se modifican: el código (texto) nunca se modifica, los datos globales pueden no modificarse, y sólo las páginas que realmente se escriben se copian. Esto hace que fork sea eficiente en todos los casos.
La copia en escritura se basa en una observacion: la mayoria de las páginas de un proceso no se modifican después de fork. Por eso, en lugar de copiar todas las páginas, se comparten y se copian sólo las que se modifican. Es como hacer una fotocopia de un documento pero sólo copiar las páginas que alguien realmente edita.
2. Por que fork es barato
Sin COW, fork tendria que copiar toda la memoria del padre al hijo: Código, datos, heap, stack. Para un proceso con 1 GB de memoria, esto tardaria decenas de milisegúndos y consumiria 1 GB de RAM adicional. Con COW, fork sólo crea una nueva tabla de páginas (unos pocos kilobytes) y marca las páginas como sólo lectura. La operación tarda microsegúndos, independientemente del tamano de la memoria.
| Sin COW | Con COW |
|---|---|
| Copia todas las páginas ficas | Solo crea una tabla de páginas nueva |
| Tiempo proporcional al tamano de la memoria | Tiempo constante (microsegúndos) |
| Duplica el consumo de RAM | Comparte las páginas hasta que se modifican |
| costoso para procesos grandes | Eficiente para cualquier tamano |
Por eso, en un sistema unix moderno, crear un proceso con fork es una operación rápida y barata, incluso para procesos con gigabytes de memoria. La mayoria de las páginas nunca se copian, porque nunca se modifican.
3. Cómo funcióna COW
El mecanismo de COW se basa en la protección de páginas del hardware. El núcleo y el MMU (Memory management unit) colaboran para detectar cuándo un proceso intenta escribir en una página compartida. El flujo es:
- fork crea una nueva tabla de páginas para el hijo: cada entrada apunta a la misma página fica que el padre, pero con permisos de sólo lectura.
- Las páginas del padre también se marcan como sólo lectura: el núcleo modifica la tabla de páginas del padre para que las páginas compartidas sean sólo lectura.
- Padre e hijo comparten las páginas ficas: ambas tablas de páginas apuntan a las mismás páginas en la RAM.
- Cuando un proceso intenta escribir en una página compartida: el hardware detecta que la página es sólo lectura y genera un fallo de página (page fault).
- El núcleo intercepta el fallo: el manejador de fallos de página del núcleo se ejecuta.
- El núcleo copia la página: reserva una nueva página fica, copia el contenido de la página original, y actualiza la tabla de páginas del proceso que escribio para que apunte a la nueva copia (con permisos de lectura y escritura).
- El proceso continua: el núcleo retorna del fallo de página, y la instrucción que causo el fallo se re-ejecuta, esta vez con exito (porque la página ya es escribible).
Todo esto ocurre de forma transparente para el proceso: el programa no sabe que se copio la página. Solo nota que la escritura funcióno. El costo se paga una sola vez por página (la primera vez que se modifica), y las páginas que nunca se modifican nunca se copian.
4. La protección de páginas
El mecanismo de COW se basa en los bits de protección de las tablas de páginas. Cada entrada en la tabla de páginas tiene bits que indican sí la página es legible, Escribible y ejecutable. El núcleo manipula estos bits para implementar COW.
Cuando fork crea el hijo:
- Las entradas de la tabla de páginas del padre se marcan como sólo lectura: el bit de escritura (W) se desactiva.
- Las entradas de la tabla de páginas del hijo también se marcan como sólo lectura: apuntan a las mismás páginas ficas, pero con permisos de sólo lectura.
- El contador de referencias de cada página fica se incrementa: el núcleo mantiene un contador que indica cuántos procesos comparten cada página. Solo cuándo el contador llega a 0 se libera la página.
ASí, las páginas compartidas son de sólo lectura para ambos procesos. Cuando uno intenta escribir, el hardware genera un fallo de página, y el núcleo decide que hacer.
En linux, las tablas de páginas se gestionan con la estructura mm_struct y las entradas con pte_t (page table entry). El bit de protección es parte de la entrada. El núcleo usa funciónes como pte_wrprotect() para marcar una página como sólo lectura y pte_mkwrite() para hacerla escribible.
5. El manejador de fallos de página
Cuando un proceso intenta escribir en una página marcada como sólo lectura, el hardware genera un fallo de página (page fault): una excepción que hace que el procesador salte al manejador de fallos del núcleo. El manejador examina la situacion y decide que hacer:
- El hardware genera el fallo: el procesador detecta que la página es sólo lectura y genera una excepción. Guarda la dirección que causo el fallo y el tipo de acceso (lectura/escritura).
- El núcleo identifica la página: busca en la tabla de páginas del proceso la entrada correspondiente a la dirección que causo el fallo.
- El núcleo comprueba sí es COW: Sí la página esta marcada como COW (compartida y sólo lectura), el núcleo procede a copiarla.
- El núcleo reserva una nueva página fica: busca una página libre en la RAM.
- El núcleo copia el contenido: copia los 4096 bytes (o el tamano de página) de la página original a la nueva.
- El núcleo actualiza la tabla de páginas: la entrada del proceso que escribio ahora apunta a la nueva página, con permisos de lectura y escritura.
- El núcleo decrementa el contador de referencias: de la página original. Si llega a 0, se libera.
- El núcleo retorna del fallo: la instrucción que causo el fallo se re-ejecuta, esta vez con exito.
Todo esto ocurre en modo núcleo, en unos pocos microsegúndos. El proceso no se entera: Sólo nota que la escritura funcióno. Si la nueva página no se puede reservar (Sin memoria), el núcleo envia SIGSEGV al proceso y lo termina.
Si el núcleo no puede reservar una nueva página para la copia (por falta de memoria), el manejador de fallos de página no puede completar la operación. En ese caso, el núcleo envia SIGSEGV al proceso, Que termina. Es una de las razones por las que un programa puede recibir SIGSEGV Sin haber accedido a memoria invalida.
6. Cuando se copia una página
Una página se copia sólo cuándo un proceso intenta escribir en ella. Las lecturas no causan copia: ambas tablas de páginas apuntan a la misma página fica, y la lectura es valida porque la página es legible.
Esto significa que:
- El código (texto) nunca se copia: es de sólo lectura, por lo que nunca se escribe. Se comparte entre todos los procesos que ejecutan el mismo programa.
- Los datos que no se modifican no se copian: Sí un proceso sólo lee una variable global, la página que la contiene no se copia.
- Solo las páginas que se modifican se copian: Sí un proceso escribe en una variable, la página que la contiene se copia (y sólo esa página, no toda la memoria).
- Las páginas se copian de a 4096 bytes: Sí un proceso modifica un sólo byte, se copia toda la página (4096 bytes). Por eso, es más eficiente modificar datos que estan en la misma página.
En un programa tipico, Después de fork, el hijo modifica sólo unas pocas páginas (las del stack, algunas del heap, algunas variables globales). El resto de las páginas (Código, datos de sólo lectura, datos no modificados) se comparten indefinidamente. Esto hace que COW sea muy eficiente.
7. Qué páginas se copian y cuáles no
No todas las páginas se tratan igual en COW. Algunas se copian inmediatamente (eager copying), otras se comparten (COW), y otras se descartan:
| Tipo de página | Tratamiento en fork | Razon |
|---|---|---|
| Codigo (texto) | Se comparte (COW, nunca se copia) | Es de sólo lectura, nunca se modifica. |
| Datos inicializados (no modificados) | Se comparte (COW) | No se modifica, no se copia. |
| Datos modificados | Se copia (en el primer acceso de escritura) | El proceso que escribe recibe su propia copia. |
| Stack | Se comparte (COW, se copia al modificar) | Cada proceso tiene su propia pila, pero las páginas se comparten hasta que se modifican. |
| Heap (malloc) | Se comparte (COW, se copia al modificar) | La memoria dinaminima se comparte hasta que se modifica. |
| Paginas mapeadas (mmap) | Depende de los flags (MAP_PRIVATE vs MAP_SHARED) | MAP_PRIVATE usa COW; MAP_SHARED comparte la página entre procesos. |
| Paginas del núcleo | No se copian (no son parte del espacio de usuario) | El espacio del núcleo se comparte entre todos los procesos. |
La distinción entre MAP_PRIVATE y MAP_SHARED es importante: MAP_PRIVATE usa COW (cada proceso tiene su propia copia al escribir), mientras que MAP_SHARED comparte la página entre procesos (las escrituras de uno son visibles para el otro). Esto es la base de la memoria compartida entre procesos, Que se ve en el curso de comunicacion entre procesos.
8. COW y la memoria compartida
COW no es lo mismo que la memoria compartida. COW comparte las páginas hasta que se modifican; la memoria compartida (MAP_SHARED) comparte las páginas siempre, incluso después de modificarlas.
| Caracteristica | COW (fork) | Memoria compartida (mmap MAP_SHARED) |
|---|---|---|
| Comparticion inicial | Si (Páginas compartidas) | Si (Páginas compartidas) |
| Despues de escribir | Cada proceso tiene su propia copia | Todos ven las mismás modificaciones |
| Uso tipico | fork (crear procesos) | Comunicacion entre procesos |
| costo | Se copia sólo al escribir | Sin costo adicional (ya esta compartida) |
COW es una optimización de fork; la memoria compartida es un mecanismo de comunicacion. Ambos usan el mismo hardware (tablas de páginas, bits de protección), pero con propositos distintos.
9. COW y exec
La combinacion de fork + COW + exec es la razon por la que fork es tan eficiente en unix. El flujo tipico es:
- fork crea el hijo con COW: las páginas se comparten, no se copian. costo: microsegúndos.
- El hijo llama a exec: exec reemplaza el programa del hijo por otro. El núcleo libera las páginas compartidas (decrementa los contadores de referencia) y carga el nuevo programa.
- Las páginas del padre nunca se copiaron: porque el hijo no las modifico antes de exec. Se liberaron directamente.
Esto significa que cuándo la shell ejecuta "ls", el fork es casí instantaneo (Sólo crea una tabla de páginas), y exec libera las páginas compartidas sin haberlas copiado. La unica memoria que se carga es la del nuevo programa ("ls"), no la del padre (la shell).
Si no hubiera COW, fork copiaria toda la memoria de la shell (Que puede ser grande), y exec la descartaria inmediatamente. Seria un desperdicio enorme. COW hace que fork + exec sea eficiente.
La combinacion de fork (crear proceso), COW (no copiar memoria hasta que sea necesario) y exec (reemplazar programa) es una de las decisiones de diseno más elegantes de unix. Permite crear procesos de forma eficiente, independientemente del tamano del padre.
10. Observar COW en la práctica
COW se puede observar en /proc comparando el tamano virtual (Vmsize) y el tamano residente (VmRSS) de un proceso antes y después de fork:
- Vmsize: el tamano total del espacio de direcciónes del proceso (incluye páginas compartidas y no residentes).
- VmRSS: el tamano de la memoria residente (Páginas que estan en la RAM). Despues de fork, VmRSS del hijo es pequeno (Sólo las páginas que se copiaron), mientras que vmsize es igual al del padre (porque comparte las mismás páginas).
- Rssfile y rssshmem: en /proc/PID/smaps, se puede ver cuántas páginas son compartidas (file-backed) y cuántas son anonimás.
$ grep -E "Vmsize|VmRSS" /proc/PID/status
Vmsize: 100000 kB
VmRSS: 50000 kB
$ grep -E "Vmsize|VmRSS" /proc/PID_HIJO/status
Vmsize: 100000 kB (igual: comparte el espacio de direcciónes)
VmRSS: 1000 kB (mucho menor: Sólo las páginas copiadas)
La diferencia entre vmsize y vmRSS del hijo es la evidencia de COW: el hijo "tiene" 100000 kB de espacio de direcciónes (porque comparte las páginas del padre), pero sólo 1000 kB estan realmente en la RAM (las páginas que se copiaron al escribir).
Tambien se puede usar /proc/PID/smaps para ver el detalle de cada region de memoria, incluyendo cuántas páginas son compartidas (Shared_Clean, Shared_Dirty) y cuántas son privadas (Private_Clean, Private_Dirty).
$ grep -E "Shared|Private" /proc/PID/smaps | head -8
Shared_Clean: 4000 kB
Shared_Dirty: 0 kB
Private_Clean: 200 kB
Private_Dirty: 800 kB
Shared_Clean son páginas compartidas y no modificadas (compartidas con COW). Private_Dirty son páginas que se copiaron (porque se modificaron). Despues de fork, el hijo tendrá mucho shared_Clean y poco private_Dirty.
11. Un programa que demuestra COW
El siguiente programa crea un hijo, Modifica una variable en el hijo, y verifica que el padre no ve el cambio. Tambien muestra la memoria antes y después de fork:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
/* Un arreglo grande para ver el efecto de COW */
static char buffer[10 * 1024 * 1024]; /* 10 MB */
static void mostrar_Memoria(const char *Quién)
{
FILE *f = fopen("/proc/self/status", "r");
if (!f) return;
char linea[256];
printf("%s:\n", Quién);
while (fgets(linea, Sizeof linea, f)) {
if (strncmp(linea, "Vmsize:", 7) == 0 ||
strncmp(linea, "VmRSS:", 6) == 0) {
printf(" %s", linea);
}
}
fclose(f);
}
int main(void)
{
/* Inicializar el buffer */
for (int i = 0; i < Sizeof buffer; i++)
buffer[i] = i % 256;
mostrar_Memoria("Antes de fork");
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
/* Hijo: modificar una página del buffer */
buffer[0] = 0xFF; /* esto causa un COW */
mostrar_Memoria("Hijo después de modificar");
_exit(0);
}
wait(NULL);
mostrar_Memoria("Padre después del hijo");
printf("Padre: buffer[0] = %d (no cambio)\n", buffer[0]);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o cow_demo cow_demo.c
$ ./cow_demo
Antes de fork:
Vmsize: 20480 kB
VmRSS: 10240 kB
Hijo después de modificar:
Vmsize: 20480 kB
VmRSS: 10244 kB
Padre después del hijo:
Vmsize: 20480 kB
VmRSS: 10240 kB
Padre: buffer[0] = 0 (no cambio)
El programa demuestra que: Vmsize es igual en padre e hijo (comparten el espacio de direcciónes), VmRSS del hijo es sólo 4 kB Más que antes (Sólo se copio una página al modificar buffer[0]), y el padre no ve el cambio del hijo (buffer[0] Sigue siendo 0).
12. Resumen
- COW permite que fork cree un proceso hijo sin copiar la memoria ficamente.
- Las páginas se comparten (marcadas como sólo lectura) hasta que uno de los dos las modifica.
- Cuando un proceso escribe en una página compartida, el núcleo la copia y actualiza la tabla de páginas.
- Solo las páginas que se modifican se copian; las demás se comparten indefinidamente.
- El código (texto) nunca se copia porque es de sólo lectura.
- COW hace que fork sea rápido (microsegúndos) independientemente del tamano de la memoria.
- La combinacion fork + COW + exec es la razon por la que unix es eficiente.
- Se puede observar en /proc comparando vmsize y vmRSS del padre e hijo.
- COW es diferente de la memoria compartida (MAP_SHARED): COW copia al escribir, MAP_SHARED comparte siempre.
13. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Qué es la copia en escritura (COW)?
Es una tetécnica que permite que fork cree un hijo sin copiar la memoria ficamente. Las páginas se comparten (marcadas como sólo lectura) hasta que uno de los dos las modifica, momento en que se copian.
2. Por que fork es barato con COW?
Porque fork sólo crea una nueva tabla de páginas (unos pocos kilobytes) en lugar de copiar toda la memoria. Las páginas se comparten hasta que se modifican.
3. Cuando se copia una página en COW?
Solo cuándo un proceso intenta escribir en ella. Las lecturas no causan copia. El código (texto) nunca se copia porque es de sólo lectura.
4. Qué pasa cuándo un proceso escribe en una página compartida?
El hardware genera un fallo de página (porque la página es sólo lectura). El núcleo intercepta el fallo, copia la página, actualiza la tabla de páginas del proceso que escribio, y retorna. La escritura se re-ejecuta con exito.
5. Qué es el contador de referencias de una página?
Es un contador que indica cuántos procesos comparten una página fica. Cuando llega a 0 (todos los procesos la liberaron), la página se libera.
6. Qué diferencia hay entre COW y MAP_SHARED?
COW comparte las páginas hasta que se modifican (entonces se copian). MAP_SHARED comparte las páginas siempre, incluso después de modificarlas (las escrituras de uno son visibles para el otro).
7. Por que la combinacion fork + COW + exec es eficiente?
Porque fork es rápido (COW no copia), y exec libera las páginas compartidas sin haberlas copiado. La unica memoria que se carga es la del nuevo programa.
8. Cómo se observa COW en /proc?
Comparando vmsize y vmRSS del padre e hijo. Vmsize es igual (comparten espacio de direcciónes), pero vmRSS del hijo es mucho menor (Sólo las páginas copiadas).
9. Qué es shared_Clean y private_Dirty en /proc/PID/smaps?
Shared_Clean son páginas compartidas y no modificadas (compartidas con COW). Private_Dirty son páginas que se copiaron (porque se modificaron).
10. Qué pasa sí no hay memoria para copiar una página en COW?
El núcleo envia SIGSEGV al proceso y lo termina, porque no puede completar la operación de copia.
Para prácticar
- Compila y ejecuta cow_demo.c. Observa la diferencia entre vmsize y vmRSS antes y después de fork.
- Modifica cow_demo.c para que el hijo modifique más páginas (por ejemplo, 100 Páginas). Observa como aumenta vmRSS del hijo.
- Usa /proc/PID/smaps para ver las páginas compartidas y privadas de un proceso antes y después de fork.
- Ejecuta un programa que haga fork y luego exec. Verifica que las páginas del padre no se copian.
- Compara el tiempo de fork con y sin COW (en un sistema antiguo sin COW, Como linux 2.0).
- Usa pmap -x PID para ver la memoria de un proceso y sus páginas compartidas.