⏱️ Lectura: 13 min
Ahora mismo, el navegador, el editor de código y la terminal que tenés abiertos creen tener acceso a toda la memoria RAM del equipo, aunque sumados pidan mucho más de la que existe físicamente instalada. Eso no es un error de cálculo: es memoria virtual, el mecanismo que sostiene esa ilusión sin que un programa choque con otro.
📑 En este artículo
- TL;DR
- ¿Qué es la memoria virtual?
- Por qué importa la paginación
- Cómo funciona la paginación, paso a paso
- Ejemplos prácticos y cómo empezar
- Casos de uso reales
- Errores comunes y buenas prácticas
- Comparativa con alternativas de gestión de memoria
- Profundizando: dentro del kernel
- Preguntas frecuentes
- ¿La memoria virtual hace más lenta la computadora?
- ¿Qué diferencia hay entre paginación y memoria swap?
- ¿Cómo veo cuánta memoria virtual usa un proceso en Linux?
- ¿Qué es un TLB miss y por qué importa?
- ¿Los sistemas embebidos usan paginación por demanda?
- ¿Qué pasa si se agota la memoria disponible en Linux?
- Referencias
La traducción ocurre en hardware, miles de veces por segundo, con una caché especial que la acelera. Esa pieza, el TLB, es la que determina si tu programa corre rápido o se arrastra.
TL;DR
- El espacio de direcciones virtual traduce direcciones lógicas a físicas mediante tablas de páginas.
- TLB cachea esas traducciones para evitar consultar la tabla de páginas en cada acceso a memoria.
- Un fallo de página ocurre cuando el dato no está en RAM y el kernel debe traerlo del disco.
- `vmstat` y `/proc/
/smaps` muestran en vivo cómo Linux reparte páginas de memoria. - Cada proceso tiene su propio espacio de direcciones virtual, aislado del resto por diseño.
¿Qué es la memoria virtual?
La memoria virtual es una técnica de gestión de memoria que asigna a cada proceso un espacio de direcciones propio, separado de la RAM física real. El sistema operativo y la MMU traducen esas direcciones a direcciones físicas mediante tablas de páginas, lo que aísla procesos entre sí.
La unidad básica de esta traducción es la página: un bloque de tamaño fijo, típicamente 4 KB en x86-64, que el sistema mueve, protege y reemplaza como un todo. Cada entrada de la tabla de páginas mapea una página virtual a un marco físico concreto, o marca que esa página no está presente en RAM todavía.
Por qué importa la paginación
Sin este esquema, cada programa tendría que conocer en qué dirección física exacta de la RAM va a correr, y dos procesos jamás podrían usar el mismo rango de direcciones sin pisarse. La paginación resuelve eso dándole a cada proceso la ilusión de un espacio de direcciones propio que empieza en cero.
También habilita el aislamiento de seguridad: un proceso no puede leer ni escribir la memoria de otro porque, simplemente, no tiene entradas en su tabla de páginas que apunten ahí. Y permite algo contraintuitivo, el overcommit, donde el sistema promete más memoria de la que existe físicamente, apostando a que no todos los procesos la van a usar al mismo tiempo.
Cómo funciona la paginación, paso a paso
Cuando un proceso accede a una dirección, la CPU primero consulta el TLB (Translation Lookaside Buffer), una caché pequeña y muy rápida de traducciones recientes. Si la traducción está ahí (TLB hit), la dirección física sale casi gratis. Si no está (TLB miss), el hardware recorre la tabla de páginas completa, un proceso llamado page table walk que puede costar cientos de ciclos de reloj.
Si la tabla de páginas indica que la página no está en RAM, ocurre un fallo de página (page fault). Hay dos tipos: el fallo menor, donde el kernel solo necesita asignar un marco físico libre y actualizar la tabla (por ejemplo en la primera escritura tras un `malloc`), y el fallo mayor, donde además hay que traer el contenido desde disco o desde el área de swap, mucho más lento.
flowchart TD
A["Direccion virtual del proceso"] --> B["MMU consulta el TLB"]
B -->|"Hit"| C["Direccion fisica en RAM"]
B -->|"Miss"| D["Camina la tabla de paginas"]
D --> E{"Pagina presente?"}
E -->|"Si"| C
E -->|"No"| F["Fallo de pagina: interviene el kernel"]
F --> G["Carga la pagina desde disco o swap"]
G --> C
Cuando el fallo es mayor, el kernel suspende al proceso, pide la página al dispositivo de almacenamiento y lo reanuda cuando el dato está listo. Esa secuencia es la que convierte un simple acceso a memoria en una operación de entrada/salida completa.
sequenceDiagram
participant P as Proceso
participant M as MMU
participant K as Kernel
participant D as DiscoSwap
P->>M: accede a direccion virtual
M->>M: busca en TLB y tabla de paginas
M-->>K: pagina ausente, genera fallo de pagina
K->>D: solicita la pagina faltante
D-->>K: devuelve el contenido de la pagina
K-->>P: actualiza la tabla y reanuda
Ejemplos prácticos y cómo empezar
Para probar esto en vivo necesitás una máquina Linux con gcc instalado (sudo apt install build-essential) y el paquete time de GNU para medir fallos de página (sudo apt install time), distinto del builtin de bash.
Primero, un vistazo general con free:
$ free -h
total used free shared buff/cache available
Mem: 15Gi 4.2Gi 1.1Gi 412Mi 10Gi 10Gi
Swap: 2.0Gi 0B 2.0Gi
La columna available es la que importa: es cuánta memoria el kernel puede entregar a un proceso nuevo sin empezar a usar swap. Con vmstat se ve el movimiento en tiempo real:
$ vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 1123456 98234 9876543 0 0 3 12 410 620 8 2 89 1 0
Las columnas si y so (swap in / swap out) en cero son buena señal: el sistema no está intercambiando páginas con el disco. Ahora, un programa que fuerza fallos de página a propósito:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t size = 100 * 1024 * 1024; // 100 MB
char *buf = malloc(size);
for (size_t i = 0; i < size; i += 4096) {
buf[i] = 1; // toca una pagina nueva cada vez
}
printf("Listo: %zu paginas tocadas\n", size / 4096);
free(buf);
return 0;
}
Este programa reserva 100 MB con malloc, pero el sistema todavía no les asigna RAM física. Solo cuando el bucle escribe en el primer byte de cada página de 4 KB, el kernel resuelve un fallo menor y le asigna un marco real. Compilalo y midelo así:
$ gcc -O0 -o paginas paginas.c
$ /usr/bin/time -v ./paginas
Listo: 25600 paginas tocadas
...
Minor (reclaiming a frame) page faults: 25607
Major (requiring I/O) page faults: 0
El número de fallos menores rondará las 25.600 páginas (100 MB dividido entre 4 KB), más un puñado extra por el propio binario y la pila del proceso. Los fallos mayores en cero confirman que todo se resolvió en RAM, sin tocar el disco.
Casos de uso reales
Los contenedores (Docker, containerd) no crean una copia física de la RAM del host: usan cgroups para poner un tope a cuántas páginas puede retener un grupo de procesos, mientras el espacio de direcciones virtual de cada proceso sigue siendo gestionado por el kernel compartido del host.
Las bases de datos como PostgreSQL o SQLite usan mmap para mapear archivos completos al espacio de direcciones virtual del proceso, dejando que el propio mecanismo de paginación del sistema operativo decida qué bloques del archivo mantener en RAM en cada momento, en lugar de reinventar ese cacheo en la aplicación.
En la nube, instancias pequeñas con poca RAM suelen usar zram (swap comprimido en memoria) en lugar de swap a disco, porque comprimir y descomprimir en CPU resulta más rápido que esperar una lectura de un disco de red.
Errores comunes y buenas prácticas
Un error frecuente es confundir memoria virtual con swap: el swap es solo uno de los destinos posibles para una página que no cabe en RAM, pero el mecanismo de paginación existe aunque el swap esté desactivado por completo.
Otro error es asumir que reservar memoria virtual (un malloc grande) consume RAM de inmediato. En Linux no es así: el consumo real aparece recién cuando se escribe en esas páginas, por eso VmSize y VmRSS en /proc/<pid>/status pueden diferir en órdenes de magnitud.
💡 Tip: si tu programa tiene patrones de acceso muy dispersos en memoria y te preocupa la latencia, medí primero con perf stat -e dTLB-load-misses antes de asumir que el cuello de botella está en el algoritmo.
⚠️ Ojo: el overcommit de Linux permite reservar más memoria de la que existe físicamente; si varios procesos la reclaman toda al mismo tiempo, el OOM killer empieza a matar procesos sin previo aviso.
Comparativa con alternativas de gestión de memoria
| Estrategia | Cuándo se usa | Ventaja | Limitación |
|---|---|---|---|
| Paginación por demanda | Linux, Windows, macOS de propósito general | Solo carga lo que se usa, permite overcommit | Thrashing si la RAM se queda corta |
| Segmentación | Sistemas x86 heredados en modo real/protegido | Mapea unidades lógicas completas (código, datos, pila) | Fragmentación externa, casi abandonada |
| Memoria plana sin paginar | Microcontroladores y RTOS embebidos | Sin overhead de traducción, latencia predecible | Sin aislamiento: un bug corrompe todo |
| Huge pages / THP | Bases de datos y VMs con heaps grandes | Menos entradas de TLB, menos TLB misses | Puede reservar más RAM de la necesaria |
Profundizando: dentro del kernel
En x86-64, la tabla de páginas no es una sola estructura plana: es una jerarquía de hasta cuatro niveles (PGD, PUD, PMD, PTE), donde cada nivel reduce el espacio de búsqueda antes de llegar al marco físico final. El registro CR3 apunta siempre al primer nivel.
flowchart TD
A["CR3: tabla de nivel superior PGD"] --> B["PUD"]
B --> C["PMD"]
C --> D["PTE"]
D --> E["Marco de pagina fisico"]
subgraph "Traduccion x86-64 de 4 niveles"
A
B
C
D
end
En sistemas multinúcleo, cuando el kernel cambia una entrada de la tabla de páginas tiene que invalidar la entrada correspondiente en el TLB de cada núcleo, una operación llamada TLB shootdown. Es cara porque interrumpe a los otros núcleos, así que el kernel la agrupa en lotes cuando puede.
Las huge pages (de 2 MB o 1 GB en lugar de 4 KB) reducen cuántas entradas necesita el TLB para cubrir la misma cantidad de RAM, a costa de desperdiciar memoria si una página grande queda medio vacía. Para confirmar si las Transparent Huge Pages están activas en tu sistema:
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
El valor entre corchetes indica el modo activo; always entre corchetes significa que el kernel intenta usar huge pages de forma automática para cualquier asignación grande.
El otro mecanismo que vale la pena entender es copy-on-write: cuando un proceso hace fork(), el hijo no copia la RAM del padre, solo copia las tablas de páginas y las marca como solo lectura. La copia real de una página ocurre, página por página, solo si alguno de los dos la modifica después.
📖 Resumen en Telegram: Ver resumen
Tu próximo paso: compilá el programa de ejemplo de este artículo, corré /usr/bin/time -v sobre él y compará los fallos de página menores que reporta contra el tamaño del buffer dividido entre 4 KB.
Preguntas frecuentes
¿La memoria virtual hace más lenta la computadora?
No por sí misma. La memoria virtual agrega una traducción de direcciones, pero el TLB la vuelve casi gratuita en el caso común. La lentitud aparece cuando no hay páginas libres y el kernel empieza a intercambiar con el disco, un escenario llamado thrashing.
¿Qué diferencia hay entre paginación y memoria swap?
La paginación es el mecanismo de traducción de direcciones; el swap es solo un destino posible para páginas que no caben en RAM. Un sistema puede paginar sin tener nada de swap configurado: simplemente no podrá liberar páginas hacia el disco cuando falte memoria física.
¿Cómo veo cuánta memoria virtual usa un proceso en Linux?
Con cat /proc/<pid>/status y mirando el campo VmSize para la memoria reservada o VmRSS para la que realmente ocupa páginas físicas. La diferencia entre ambos suele ser enorme, porque reservar espacio de direcciones virtual no consume RAM hasta que se escribe en él.
¿Qué es un TLB miss y por qué importa?
Es cuando la MMU no encuentra la traducción en su caché y debe recorrer la tabla de páginas completa, algo que puede tardar cientos de ciclos de reloj. Programas con patrones de acceso muy dispersos en memoria sufren más TLB misses que los que acceden de forma secuencial.
¿Los sistemas embebidos usan paginación por demanda?
La mayoría no. Un microcontrolador con RTOS suele trabajar con memoria física plana porque su RAM es pequeña, fija y conocida de antemano, así que el overhead de traducir direcciones no se justifica. Linux embebido, por ejemplo en routers, sí mantiene paginación completa.
¿Qué pasa si se agota la memoria disponible en Linux?
Si el overcommit permitió reservar más de lo que existe y la demanda real supera la RAM y el swap disponibles, el kernel activa el OOM killer, que elige y mata procesos según una puntuación (oom_score) hasta liberar memoria.
Referencias
- docs.kernel.org: documentación oficial del kernel Linux sobre los conceptos de gestión de memoria.
- man7.org: referencia completa de los campos de /proc/[pid]/status y /proc/[pid]/smaps.
- en.wikipedia.org: historia y fundamentos de la memoria virtual desde los sistemas de los años 60.
- pages.cs.wisc.edu: Operating Systems Three Easy Pieces, capítulos de paginación y tablas multinivel.
📱 ¿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 Luan Gjokaj en Unsplash
¿Te sirvió? ¿Te dio otro error? Contalo abajo: las preguntas se responden y le sirven al siguiente que llegue.
Dejar un comentario
0 Comentarios