⏱️ 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
  1. TL;DR
  2. ¿Qué es la memoria virtual?
  3. Por qué importa la paginación
  4. Cómo funciona la paginación, paso a paso
  5. Ejemplos prácticos y cómo empezar
  6. Casos de uso reales
  7. Errores comunes y buenas prácticas
  8. Comparativa con alternativas de gestión de memoria
  9. Profundizando: dentro del kernel
  10. Preguntas frecuentes
    1. ¿La memoria virtual hace más lenta la computadora?
    2. ¿Qué diferencia hay entre paginación y memoria swap?
    3. ¿Cómo veo cuánta memoria virtual usa un proceso en Linux?
    4. ¿Qué es un TLB miss y por qué importa?
    5. ¿Los sistemas embebidos usan paginación por demanda?
    6. ¿Qué pasa si se agota la memoria disponible en Linux?
  11. 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.

Los procesadores x86-64 actuales direccionan hasta 48 bits de espacio virtual por proceso. Foto de Chris Ried en Unsplash

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.

Un TLB miss puede costar cientos de ciclos de reloj frente a los pocos que toma un TLB hit. Foto de Mohammad Rahmani en Unsplash

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

EstrategiaCuándo se usaVentajaLimitación
Paginación por demandaLinux, Windows, macOS de propósito generalSolo carga lo que se usa, permite overcommitThrashing si la RAM se queda corta
SegmentaciónSistemas x86 heredados en modo real/protegidoMapea unidades lógicas completas (código, datos, pila)Fragmentación externa, casi abandonada
Memoria plana sin paginarMicrocontroladores y RTOS embebidosSin overhead de traducción, latencia predecibleSin aislamiento: un bug corrompe todo
Huge pages / THPBases de datos y VMs con heaps grandesMenos entradas de TLB, menos TLB missesPuede 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.

📬 Recibí lo nuevo en tu email

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

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

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.