Memoria virtual
"La ilusión más útil de la informática: que cada programa tiene toda la memoria para sí solo."
Qué vas a aprender en este capítulo
Tu computadora tiene 8 o 16 GB de RAM. Pero un programa cualquiera "ve" un espacio de direcciones de 256 TB (en sistemas de 64 bits). ¿Cómo? Mediante la memoria virtual: una abstracción que el SO + hardware mantienen para que cada proceso crea que tiene toda la memoria del mundo, incluso aunque la RAM física sea pequeña y compartida con otros procesos. Vas a entender cómo funciona la paginación, qué hace la MMU, qué es la TLB, cómo el SO swappea al disco cuando se llena, y qué algoritmos usa para decidir qué desalojar.
5.1 La idea: la mentira útil
💡 Intuición
Imaginate que sos el SO. Tenés 8 GB de RAM y 50 procesos corriendo. Si cada proceso necesitara su propia "porción contigua" de memoria física, tendrías un rompecabezas imposible — fragmentación, complicaciones para crecer, ningún proceso puede confiar en su dirección.
Solución: mentirles a todos. Cada proceso ve un espacio "infinito" desde la dirección 0. El SO, junto con el hardware, traduce las direcciones que el proceso usa a direcciones reales en RAM. Esa traducción es invisible para el programador.
Esa es la memoria virtual. Cada proceso vive en una burbuja propia. Si dos procesos miran la dirección 0x1000, están mirando distintas posiciones de RAM (o pueden incluso compartir, controladamente).
Beneficios concretos:
- Aislamiento. Un proceso no puede leer la memoria de otro (a menos que el SO lo autorice).
- Memoria > RAM. El proceso puede usar más memoria de la que hay físicamente — el SO guarda lo que no cabe en disco (swap).
- Compartir selectivamente. Dos procesos pueden compartir bibliotecas (
libc, etc.) sin duplicarlas. - Reubicación libre. El SO puede mover páginas físicas de lugar sin que el proceso se dé cuenta.
- Protección. Páginas marcadas read-only no se pueden escribir. Páginas no asignadas, ni leer.
Sin memoria virtual, los SO modernos no podrían existir.
📜 Historia
La idea de paginación la propuso un grupo de la Universidad de Manchester en 1959 con el sistema Atlas. La pieza clave: introducir una tabla de páginas que el hardware consulta automáticamente al acceder a memoria.
En los 60 los mainframes ya tenían memoria virtual. Las PCs llegaron tarde: las primeras (8086, 1978) no la tenían. Recién con el Intel 80286 (1982) y, sobre todo, el 80386 (1985), x86 tuvo paginación seria. Linux y Windows NT asumen 386+ y por eso pueden usar memoria virtual a fondo.
Hoy, toda CPU mainstream — x86-64, ARM64, RISC-V, POWER — tiene una MMU (Memory Management Unit) que hace la traducción en hardware. Si la MMU se desactiva, el SO se cae instantáneamente. La memoria virtual no es opcional.
5.2 Paginación
📐 Fundamento
Idea base. Dividir la memoria virtual y la física en bloques de tamaño fijo (típicamente 4 KB), llamados páginas (virtual) y marcos (físico).
- Página virtual: unidad del espacio del proceso.
- Marco físico: unidad de la RAM.
Cada dirección virtual se compone de:
- Número de página (VPN): índice en la tabla.
- Desplazamiento (offset): dentro de la página.
Para una dirección de 32 bits con páginas de 4 KB ():
| 31 ............ 12 | 11 ........ 0 |
| VPN | offset |
20 bits 12 bits
Tabla de páginas. El kernel mantiene, por cada proceso, una tabla que mapea VPN → marco físico (PFN). Cuando el proceso accede a una dirección virtual:
- MMU extrae el VPN.
- Consulta la tabla → obtiene el PFN.
- Combina PFN + offset → dirección física real.
- Accede a RAM.
Todo esto en hardware, en cada acceso. Tu programa nunca lo "ve".
dirección virtual: | VPN | offset |
↓ traducción por MMU
dirección física: | PFN | offset |
Tabla de páginas — entrada típica:
| Campo | Para qué |
|---|---|
| PFN | El marco físico al que apunta |
| válida | Si la página está mapeada |
| presente | Si está en RAM o en disco |
| lectura/escritura | Permisos |
| usuario/kernel | Quién puede acceder |
| referenced | Si se usó recientemente |
| dirty | Si fue modificada |
Si un acceso intenta escribir una página marcada read-only, page fault → el kernel toma el control y decide qué hacer (bloquear el proceso, copy-on-write, etc.).
5.3 Tablas de páginas multinivel
📐 Fundamento
El problema. Una tabla plana sería enorme. En 64 bits hay páginas; una tabla con entradas no cabe en ninguna parte.
Solución: tablas multinivel. La VPN se subdivide en partes, cada una indexa un nivel.
| 47 ... 39 | 38 ... 30 | 29 ... 21 | 20 ... 12 | 11 ... 0 |
PML4 PDPT PD PT offset
x86-64 usa 4 niveles (PML4, PDPT, PD, PT). Cada nivel solo se materializa cuando se usa — un proceso pequeño tiene tablas chicas.
Costo: cada acceso requeriría 5 lecturas a memoria (4 niveles + el dato). Inaceptable. La solución: caché.
TLB (Translation Lookaside Buffer)
La TLB es una caché en hardware (dentro de la CPU) que guarda traducciones recientes VPN → PFN. Tamaños típicos: 64–4096 entradas.
Ciclo de un acceso:
- CPU extrae VPN.
- Consulta TLB.
- Hit: PFN listo en 1 ciclo. Acceso continúa.
- Miss: hay que recorrer la tabla (4 accesos a memoria), después se carga la TLB.
Hit rate típico: >99%. Eso es lo que permite que la paginación sea viable.
Cuándo la TLB se vacía:
- Cambio de proceso (otro espacio de direcciones — distinta tabla).
- Cambio explícito (al modificar la tabla).
TLB shootdown. En multinúcleo, si el SO modifica una tabla, tiene que invalidar la TLB de los otros núcleos donde el proceso pueda haber corrido. Es caro pero necesario.
Páginas grandes (huge pages, 2 MB o 1 GB en x86): cada entrada cubre más memoria, así una sola entrada de TLB sirve para más. Útil para servidores con datasets enormes (bases de datos, JVM con heaps de cientos de GB).
5.4 Page faults y demanda
📐 Fundamento
Page fault: la MMU encuentra que la página accedida no está en RAM (bit "presente" = 0). Levanta una excepción y el control salta al kernel.
Tipos de page fault:
- Minor (soft). La página está en memoria pero no mapeada en la tabla del proceso, o requiere copy-on-write. Se resuelve en microsegundos.
- Major (hard). La página está en disco (swap) o nunca se cargó. El kernel debe traerla. Costa milisegundos — mil veces más lento que un acceso normal.
- Inválido. Acceso a una dirección no mapeada. El kernel mata el proceso (
SIGSEGV). Ese es el famoso segfault que veías en C.
Demanda paginada (demand paging). Estrategia clave: no cargar páginas hasta que se necesiten. Cuando lanzás firefox, el kernel no copia los 200 MB del binario a RAM al inicio; solo configura las tablas y deja todas las páginas marcadas "no presente". El primer acceso a cada página dispara un page fault que la trae.
Resultado: los programas arrancan rápido, ocupan poca memoria al inicio y solo lo que realmente usan se carga.
Comando para ver page faults:
$ ps -o pid,min_flt,maj_flt,cmd -p $$
PID MINFL MAJFL CMD
12345 12421 8 bash
minflt = minor faults, majflt = major (los caros).
5.5 Swap (memoria en disco)
📐 Fundamento
Cuando se llena la RAM y el SO necesita más, swappea: mueve páginas inactivas al espacio de swap (un archivo o partición en disco). La RAM queda libre para lo que está en uso.
Ciclo:
- Hay presión de memoria — se necesita un marco libre.
- El SO elige una víctima (página a evacuar).
- Si la página está dirty (modificada), la escribe a swap. Si está limpia (sólo lectura del binario), se descarta — se puede recuperar leyendo del binario original.
- Marca la entrada de la tabla como "no presente".
- Asigna ese marco al solicitante.
Después, cuando el proceso original quiera acceder, page fault → el SO trae la página de vuelta de swap → reconfigura tabla → reanuda.
Costo. Si el disco es HDD, ~10 ms por page-in. SSD: ~0.1 ms. RAM directa: ~0.0001 ms. Aún con SSD, swap es 1000× más lento que RAM. Si el sistema swappea mucho, está trasheando y la pantalla se vuelve melaza.
Ver swap en Linux:
$ free -h
total used free shared buff/cache available
Mem: 7.7Gi 2.4Gi 3.1Gi 320Mi 2.2Gi 4.9Gi
Swap: 2.0Gi 0B 2.0Gi
Swap: 0B used = bien, no estás swappeando. Si used crece, mirar qué proceso lo causa.
5.6 Algoritmos de reemplazo de página
📐 Fundamento
Cuando hay que liberar un marco, ¿cuál página desalojás? La mejor decisión rebaja drásticamente el número de page faults.
Óptimo (Belady)
Desalojar la página que no se va a usar por más tiempo en el futuro. Imposible en la práctica (no sabemos el futuro), pero útil como referencia teórica.
FIFO
Desalojar la más antigua (la que llegó primero). Simple, pero ignora si se sigue usando.
Anomalía de Belady: con FIFO, más marcos NO siempre dan menos faults. Caso clásico que demuestra que FIFO está mal.
LRU (Least Recently Used)
Desalojar la menos recientemente usada. Aproxima el óptimo bajo el principio de localidad (lo que se usó recientemente, probablemente se vuelva a usar).
Problema: implementar LRU exacto requeriría actualizar timestamps en cada acceso. Imposible al ritmo de la CPU.
Aproximaciones de LRU:
- Bit de referenced. El hardware lo activa al acceder; el SO lo limpia periódicamente. Páginas con bit=0 son candidatas a desalojo.
- Algoritmo del reloj (clock). Páginas en un anillo. Un puntero las recorre. Si bit=1, lo limpia y avanza. Si bit=0, esa es la víctima.
Working set / WSClock
Toma en cuenta no solo cuándo se usó, sino qué conjunto de páginas está usando activamente cada proceso. Linux usa una variante de esto.
Comparación
Considerá la secuencia de páginas: con 3 marcos disponibles.
| Algoritmo | Faults |
|---|---|
| Óptimo | 7 |
| LRU | 10 |
| FIFO | 9 |
| Reloj | ~9-10 |
Lección: la diferencia entre algoritmos es real pero modesta. Lo que realmente baja faults es agregar más RAM.
Linux real
Linux usa una variante de two-list LRU: una lista "activa" (recientes) y una "inactiva" (víctimas candidatas). Las páginas se mueven entre listas según uso. Está combinado con kswapd, un proceso de kernel que va liberando páginas en background antes que se sature.
5.7 Segmentación (legado)
📐 Fundamento
Segmentación es una alternativa (o complemento) a la paginación. La memoria se divide en segmentos de tamaño variable, con significado lógico: segmento de código, de datos, de pila, de heap.
Ventaja: mapea la estructura del programa.
Desventaja: segmentos de tamaño variable causan fragmentación externa — espacios chicos entre segmentos que no sirven a nadie.
x86 (16 y 32 bits) usaba segmentación + paginación combinadas. x86-64 prácticamente eliminó segmentación: solo quedan los segmentos cs, ds, ss que casi siempre apuntan al mismo espacio plano. La paginación ganó.
Hoy "segmento" se usa más coloquialmente: el segmento de texto (código), el de datos, etc. Son regiones lógicas dentro del espacio paginado del proceso.
5.8 Mapeo de memoria a archivos (mmap)
🛠️ En la práctica
Una de las features más potentes que permite la memoria virtual: mapear un archivo directamente al espacio de direcciones del proceso.
Syscall:
void *addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
Esto te da un puntero a una región. Leer del puntero es leer del archivo, sin read() explícitos. El SO trae las páginas a demanda.
Usos:
- Cargar binarios. El kernel
mmap-ea el ejecutable. Las páginas se traen cuando el programa "salta" a ellas. - Bibliotecas compartidas. Varias instancias de
firefoxmapean la mismalibc, físicamente compartida. - Bases de datos. SQLite, MySQL, PostgreSQL usan mmap para acceder a archivos enormes sin reservarlos en RAM.
- Comunicación entre procesos.
mmap(MAP_SHARED)da memoria compartida — más rápido que pipes para gran volumen.
Ejemplo:
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("archivo.txt", O_RDONLY);
off_t size = lseek(fd, 0, SEEK_END);
char *p = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
/* Acceso como si fuera un array común */
for (int i = 0; i < 100 && i < size; i++)
putchar(p[i]);
munmap(p, size);
close(fd);
return 0;
}
Tu programa lee el archivo sin un solo read. La paginación + page faults hacen el trabajo.
5.9 Copy-on-write
Volvemos al fork() del cap 2: cuando un proceso se duplica, copiar toda la memoria sería caro. Copy-on-write (COW):
fork: padre e hijo apuntan al mismo conjunto de marcos físicos, todos marcados read-only.- Si alguno escribe, page fault → el kernel:
- Copia esa página a un nuevo marco.
- Reasigna la tabla del que escribió.
- Marca ambas como writable.
- El otro proceso sigue con la página original.
Resultado: si los procesos casi no escriben (caso típico: hijo hace exec enseguida y reemplaza todo), prácticamente no se copia memoria.
fork es rápido en Linux gracias a esto.
5.10 Métricas y observación
🛠️ En la práctica
/proc/meminfo muestra el estado de la memoria:
$ cat /proc/meminfo | head -10
MemTotal: 7811236 kB
MemFree: 2094812 kB
MemAvailable: 4720416 kB
Buffers: 112640 kB
Cached: 2384812 kB
SwapCached: 0 kB
Active: 3450176 kB
Inactive: 1452856 kB
Active(anon): 1985244 kB
Inactive(anon): 16 kB
/proc/<pid>/status muestra memoria por proceso:
VmSize: total virtual.VmRSS(Resident Set Size): RAM realmente usada.VmSwap: cuánto está en swap.
Comando pmap <pid> muestra el mapa completo del proceso — todas sus regiones, tamaños, permisos. Útil para entender por qué un proceso usa tanta memoria.
Comando vmstat 1 muestra estadísticas en vivo:
procs -----------memory---------- ---swap-- -----io----
r b swpd free buff cache si so bi bo
1 0 0 2094812 112640 2384812 0 0 12 20
si y so son swap-in y swap-out por segundo. Si crecen, estás en problemas.
5.11 Resumen visual
| Concepto | Una línea |
|---|---|
| Memoria virtual | Cada proceso ve su propio espacio "infinito". |
| MMU | Hardware que traduce direcciones virtuales a físicas. |
| Página/marco | Unidad fija (típicamente 4 KB). |
| Tabla de páginas | Mapeo VPN → PFN (por proceso). |
| TLB | Caché de hardware de traducciones. |
| Page fault | Acceso a página no presente en RAM. |
| Swap | Páginas guardadas en disco. |
| Demand paging | Cargar bajo demanda, no por adelantado. |
| LRU / Clock | Algoritmos para elegir página a desalojar. |
| Copy-on-write | Compartir hasta que alguien escriba. |
| mmap | Mapear archivo a memoria virtual. |
5.12 Ejercicios
✏️ Ejercicio 5.1 — Cálculo de direcciones
Un sistema usa direcciones virtuales de 16 bits, páginas de 256 bytes y memoria física de 1024 bytes (4 marcos).
a. ¿Cuántos bits son VPN y cuántos offset?
b. La tabla mapea: VPN 0 → marco 2, VPN 1 → marco 0, VPN 2 → marco 3, VPN 3 → marco 1. ¿A qué dirección física corresponde la virtual 0x012F?
Solución
a. Página = 256 = → offset 8 bits. VPN = 16 - 8 = 8 bits.
b. 0x012F en binario (16 bits): 0000 0001 0010 1111.
VPN = 00000001 = 1 → marco 0.
Offset = 00101111 = 47.
Dirección física = = 0x002F.
✏️ Ejercicio 5.2 — FIFO vs LRU
Secuencia de acceso a páginas: . 3 marcos.
Calculá el número de page faults con FIFO y con LRU.
Solución
FIFO:
| Acceso | Marcos | ¿Fault? |
|---|---|---|
| 1 | [1] | sí |
| 2 | [1,2] | sí |
| 3 | [1,2,3] | sí |
| 4 | [2,3,4] | sí (saca 1) |
| 1 | [3,4,1] | sí (saca 2) |
| 2 | [4,1,2] | sí (saca 3) |
| 5 | [1,2,5] | sí (saca 4) |
| 1 | [1,2,5] | no |
| 2 | [1,2,5] | no |
| 3 | [2,5,3] | sí (saca 1) |
| 4 | [5,3,4] | sí (saca 2) |
| 5 | [5,3,4] | no |
Total FIFO: 9 faults.
LRU:
| Acceso | Marcos (más viejo→nuevo) | ¿Fault? |
|---|---|---|
| 1 | [1] | sí |
| 2 | [1,2] | sí |
| 3 | [1,2,3] | sí |
| 4 | [2,3,4] | sí |
| 1 | [3,4,1] | sí |
| 2 | [4,1,2] | sí |
| 5 | [1,2,5] | sí |
| 1 | [2,5,1] | no |
| 2 | [5,1,2] | no |
| 3 | [1,2,3] | sí |
| 4 | [2,3,4] | sí |
| 5 | [3,4,5] | sí |
Total LRU: 10 faults.
(En este caso particular, FIFO gana — un caso del corolario de Belady. Promedio en cargas reales, LRU suele ser igual o mejor.)
✏️ Ejercicio 5.3 — Detección de problema con mmap
Mapeás un archivo de 10 GB con mmap en una máquina de 8 GB de RAM. ¿Funciona? ¿Qué pasa?
Solución
Sí funciona. mmap solo establece el mapeo virtual; no carga las páginas. El espacio virtual del proceso (en 64 bits) son terabytes — caben 10 GB sin problemas.
Cuando accedés, el SO trae las páginas de a una. Si recorrés todo el archivo secuencialmente, vas a tener un montón de page faults — el SO va trayendo de disco y desalojando. La velocidad depende del SSD/HDD.
Ventaja: podés trabajar con datasets más grandes que la RAM sin manejar la paginación a mano. Desventaja: acceso aleatorio puede generar mucho swap-in si el archivo no cabe.
(Esto es exactamente cómo bases de datos tipo SQLite manejan archivos enormes con poca RAM.)
5.13 Para profundizar
- Tanenbaum & Bos cap. 3 completo.
- OSTEP "Virtualization → Memory" (caps 13-22 aproximadamente).
- What every programmer should know about memory (Drepper, 2007). Largo, denso, técnico — pero la referencia.
- Próximo capítulo: Sistemas de archivos — cómo se guardan y organizan los datos en disco.
Definiciones nuevas: memoria virtual, página, marco, MMU, VPN, PFN, tabla de páginas, TLB, page fault, demand paging, swap, FIFO, LRU, clock, copy-on-write, mmap, segmentación.