Terminamos la lección anterior con una pregunta muy concreta: si ingestor y agregador tienen ambos código en la dirección 0x400000, ¿cómo es posible que no se pisen? La respuesta a esa pregunta es el segundo gran trabajo del sistema operativo, y es más profunda de lo que parece: implica al compilador, al enlazador, al cargador y a un circuito específico dentro de la CPU.

En esta lección vas a entender cómo varios procesos comparten una memoria física limitada sin invadirse, qué diferencia hay entre una dirección lógica y una física, y por qué la solución evolucionó desde un simple par de registros hasta los esquemas modernos. Verás también los dos tipos de fragmentación con cálculos concretos, y terminarás leyendo el mapa de memoria real de un proceso de Meteora región por región. Es la lección que prepara el terreno para la siguiente, que es la que de verdad explica cómo funciona un sistema moderno.

Contenido

  1. El problema: memoria compartida sin invasiones
  2. Direcciones lógicas y direcciones físicas
  3. Las tres fases de vinculación de direcciones
  4. Reubicación con registro base y límite
  5. La MMU: el traductor en el camino crítico
  6. Asignación contigua: particiones fijas y variables
  7. Estrategias de asignación: primer, mejor y peor ajuste
  8. Fragmentación interna y externa, con números
  9. Compactación y por qué casi nunca se usa
  10. Segmentación
  11. Intercambio (swapping) clásico
  12. La idea de la paginación
  13. El mapa de memoria real de un proceso de Meteora

El problema: memoria compartida sin invasiones

meteo-01 tiene 8 GB de RAM y ejecuta unos 180 procesos. El sistema operativo tiene que resolver simultáneamente cinco problemas que tiran en direcciones distintas:

Problema Pregunta Consecuencia si falla
Reubicación ¿Dónde cargo el programa si no sé de antemano qué zona estará libre? El programa no puede ejecutarse
Protección ¿Cómo impido que meteo-api lea la memoria de ingestor? Fuga de datos, corrupción
Compartición ¿Cómo dejo que dos procesos compartan el código de libc? Se malgastan cientos de MB
Organización lógica ¿Cómo doy permisos distintos a código y datos? Vulnerabilidades de ejecución de código
Capacidad ¿Qué hago si los procesos piden más RAM de la que hay? El sistema se queda sin memoria

Fíjate en un detalle que condiciona todo lo demás: la protección debe comprobarse en cada acceso a memoria. No una vez al arrancar el proceso, sino en cada mov que la CPU ejecuta, miles de millones de veces por segundo. Eso descarta de plano cualquier solución basada en software: si el sistema operativo tuviera que validar cada acceso, un programa iría mil veces más lento.

Como ya vimos en 01-06 con las instrucciones privilegiadas, la única solución posible es que lo haga el hardware. Y ese hardware es la MMU.

Direcciones lógicas y direcciones físicas

Cuando compilas el ingestor y miras dónde está su función principal:

$ nm -C /opt/meteora/bin/ingestor | grep ' T main'
0000000000401b40 T main

La CPU ejecutará instrucciones que hacen referencia a la dirección 0x401b40. Pero esa no es la posición real en los chips de RAM. Es una dirección lógica (o virtual): un número que solo tiene sentido dentro del espacio de direcciones de ese proceso.

Dirección lógica (virtual) Dirección física
Quién la genera La CPU al ejecutar el programa La MMU tras traducir
Qué ve El programa, el compilador, el depurador El bus de memoria, los chips de RAM
Rango 0 a 2^48 en x86-64 (256 TB) 0 a la RAM instalada (8 GB)
Única en el sistema No: cada proceso tiene la suya
Es la que ves en /proc/pid/maps No

Esto explica la paradoja del principio: ingestor y agregador pueden tener ambos código en 0x401b40 porque cada uno vive en su propio espacio de direcciones. La MMU traduce esa misma dirección lógica a marcos físicos distintos según qué proceso esté ejecutándose.

Puedes comprobarlo:

$ sudo grep -m1 'r-xp' /proc/1842/maps
00401000-00489000 r-xp 00001000 fd:01 1573241 /opt/meteora/bin/ingestor

$ sudo grep -m1 'r-xp' /proc/1877/maps
00401000-004a3000 r-xp 00001000 fd:01 1573242 /opt/meteora/bin/agregador

Los dos procesos tienen su código empezando exactamente en 0x401000. Ninguno de los dos sabe —ni necesita saber— dónde está realmente en la RAM.

Las tres fases de vinculación de direcciones

La traducción de "la variable total" a "el byte físico número 3.221.225.472" puede fijarse en tres momentos distintos, y la elección tiene consecuencias muy diferentes:

Fase Cuándo se decide Flexibilidad Necesita hardware Ejemplo
Compilación Al generar el código Nula: dirección absoluta fija No .COM de MS-DOS, firmware embebido
Carga Al cargar el programa en memoria Media: se elige el sitio una vez No Sistemas antiguos con reubicación estática
Ejecución En cada acceso a memoria Total: el proceso puede moverse (MMU) Todos los SO modernos

Vinculación en compilación. El compilador genera direcciones absolutas: "carga lo que hay en la posición 2000". Si el programa no se carga exactamente en el sitio previsto, no funciona. Es lo que hacen aún hoy los microcontroladores de las estaciones meteorológicas de Meteora: el firmware sabe que la memoria empieza en 0x20000000 porque es una placa concreta y nunca cambiará.

Vinculación en carga. El compilador genera código reubicable con direcciones relativas al inicio del programa. El cargador suma la dirección base real. Es más flexible, pero una vez cargado el proceso no puede moverse, porque sus direcciones ya fueron reescritas.

Vinculación en ejecución. El código conserva direcciones lógicas y la traducción ocurre en cada acceso, en hardware. Esto permite mover un proceso en RAM mientras se ejecuta, y es la base de todo lo moderno: memoria virtual, copy-on-write, bibliotecas compartidas y ASLR.

De hecho, ASLR (Address Space Layout Randomization) solo es posible con vinculación en ejecución. Compruébalo:

$ cat /proc/self/maps | tail -3
7ffd2a1c3000-7ffd2a1e4000 rw-p 00000000 00:00 0    [stack]
7ffd2a1f7000-7ffd2a1fb000 r--p 00000000 00:00 0    [vvar]
7ffd2a1fb000-7ffd2a1fd000 r-xp 00000000 00:00 0    [vdso]

$ cat /proc/self/maps | tail -3
7ffc8b442000-7ffc8b463000 rw-p 00000000 00:00 0    [stack]
7ffc8b47a000-7ffc8b47e000 r--p 00000000 00:00 0    [vvar]
7ffc8b47e000-7ffc8b480000 r-xp 00000000 00:00 0    [vdso]

Dos ejecuciones del mismo cat con la pila en direcciones completamente distintas. El sistema aleatoriza la disposición en cada arranque para que un atacante no pueda predecir dónde estará nada. Volveremos a esto en el módulo 5.

Reubicación con registro base y límite

El esquema hardware más simple que resuelve reubicación y protección a la vez usa dos registros:

  • Registro base (o de reubicación): la dirección física donde empieza el proceso.
  • Registro límite: el tamaño del espacio de direcciones del proceso.

En cada acceso a memoria, el hardware hace dos operaciones:

si (dirección_lógica < límite)
    dirección_física = base + dirección_lógica
si no
    generar excepción de fallo de direccionamiento  →  SIGSEGV

Ejemplo numérico con el ingestor:

Registro base:   0x0C000000  (201.326.592)
Registro límite: 0x00800000  (8.388.608 = 8 MB)

Acceso a la dirección lógica 0x401b40 (4.201.280):
  4.201.280 < 8.388.608  →  válido
  física = 201.326.592 + 4.201.280 = 205.527.872 = 0x0C401B40

Acceso a la dirección lógica 0x900000 (9.437.184):
  9.437.184 > 8.388.608  →  fuera de límites
  → excepción → el núcleo envía SIGSEGV → "Violación de segmento"

Aquí está el mecanismo completo de la protección de memoria, en dos líneas de lógica. Y observa el detalle crucial que conecta con 01-06: los registros base y límite solo pueden modificarse con instrucciones privilegiadas. Si un proceso pudiera cambiar su propio registro límite, la protección no valdría nada. Por eso el núcleo los carga en cada cambio de contexto y el proceso no puede tocarlos.

Este esquema es elegante y rapidísimo (una comparación y una suma, unos pocos ciclos), pero tiene tres limitaciones fatales:

  1. Todo el proceso debe estar en RAM y en un bloque contiguo. Si necesita 8 MB, hace falta un hueco de 8 MB seguidos.
  2. No permite permisos diferenciados. Todo el espacio tiene el mismo tratamiento: no puedes marcar el código como no escribible.
  3. No permite compartir. Dos procesos que ejecutan el mismo binario necesitan dos copias completas.

La MMU: el traductor en el camino crítico

La MMU (Memory Management Unit) es el circuito que realiza esa traducción. En los procesadores modernos está integrada en el propio núcleo de la CPU, junto a las cachés, y no es un detalle de implementación: es de las piezas más críticas del rendimiento del sistema.

flowchart LR
    CPU["CPU<br/>dirección lógica<br/>0x401b40"] --> MMU
    MMU{"MMU<br/>¿válida?<br/>¿permisos?"}
    MMU -->|sí| RAM["RAM<br/>dirección física<br/>0x0C401B40"]
    MMU -->|no| TRAP["Excepción<br/>→ núcleo → SIGSEGV"]
    SO["Sistema operativo"] -.->|carga base y límite<br/>en cada cambio de contexto| MMU

Tres características de la MMU que conviene fijar desde ya:

  • Actúa en cada acceso. Cada mov, cada instrucción leída, cada push a la pila. Si la traducción costara 10 ns, el sistema sería inutilizable. Por eso las MMU incorporan una caché de traducciones (la TLB) que veremos en la próxima lección.
  • Es hardware y no se puede eludir. Un programa en modo usuario no tiene ninguna forma de acceder a una dirección física directamente. Ninguna.
  • La configura el sistema operativo. El núcleo decide qué traducciones son válidas; la MMU las aplica. Es el mismo reparto de papeles que vimos en 01-06: la política la pone el software, el mecanismo lo impone el hardware.

Asignación contigua: particiones fijas y variables

Con base y límite, cada proceso ocupa un bloque contiguo. ¿Cómo se reparte la memoria entre ellos?

Particiones fijas

Se divide la memoria en un número fijo de particiones de tamaño predeterminado al arrancar el sistema.

┌──────────────────┐ 0 MB
│  Núcleo          │
├──────────────────┤ 512 MB
│  Partición 1     │ 1 GB
├──────────────────┤ 1,5 GB
│  Partición 2     │ 1 GB
├──────────────────┤ 2,5 GB
│  Partición 3     │ 2 GB
├──────────────────┤ 4,5 GB
│  Partición 4     │ 3,5 GB
└──────────────────┘ 8 GB

Simple de implementar (basta una tabla de 4 entradas), pero rígido:

  • Si meteo-api necesita 300 MB y le das la partición 1 de 1 GB, se desperdician 700 MB dentro de la partición. Nadie más puede usarlos.
  • Si el agregador necesita 4 GB, no cabe en ninguna, aunque sumando los huecos libres sobre espacio.
  • El grado de multiprogramación está limitado al número de particiones: cuatro procesos como máximo.

Es el esquema del IBM OS/MFT de los años 60, que vimos al hablar de multiprogramación en 01-02.

Particiones variables

El sistema mantiene una lista de huecos libres y asigna a cada proceso exactamente lo que pide. Al terminar, su espacio vuelve a la lista y se fusiona con los huecos adyacentes.

Simulemos una secuencia real en una memoria de 2.560 MB con el núcleo ocupando 400 MB:

Estado inicial:
[Núcleo 400][═══════════ libre 2160 ═══════════]

Llega ingestor (600 MB):
[Núcleo 400][ingestor 600][═════ libre 1560 ═════]

Llega agregador (1000 MB):
[Núcleo 400][ingestor 600][agregador 1000][ libre 560 ]

Llega meteo-api (300 MB):
[Núcleo 400][ingestor 600][agregador 1000][api 300][libre 260]

Termina el agregador:
[Núcleo 400][ingestor 600][═ libre 1000 ═][api 300][libre 260]

Llega backup (500 MB):
[Núcleo 400][ingestor 600][backup 500][libre 500][api 300][libre 260]

Termina el ingestor:
[Núcleo 400][═ libre 600 ═][backup 500][libre 500][api 300][libre 260]

Situación final: hay 1.360 MB libres en total, pero repartidos en tres huecos de 600, 500 y 260 MB. Si ahora llega un proceso que necesita 900 MB, no cabe, aunque haya de sobra. Ese es el problema al que llegaremos en el apartado de fragmentación.

Estrategias de asignación: primer, mejor y peor ajuste

Cuando hay varios huecos donde cabe un proceso, hay que elegir. Tres estrategias clásicas:

Estrategia Regla Ventaja Inconveniente
Primer ajuste El primer hueco donde quepa La más rápida Fragmenta el principio de la memoria
Mejor ajuste El hueco más pequeño donde quepa Aprovecha bien el espacio Recorre toda la lista; deja migajas inútiles
Peor ajuste El hueco más grande Deja restos utilizables Destruye los huecos grandes

Comparémoslas con el mismo caso. Huecos libres, en este orden:

H1: 200 MB    H2: 500 MB    H3: 300 MB    H4: 600 MB    H5: 250 MB

Peticiones sucesivas: P1 = 212 MB, P2 = 417 MB, P3 = 112 MB, P4 = 426 MB.

Primer ajuste:

Petición Hueco elegido Por qué Resto
P1 = 212 H2 (500) H1 (200) es demasiado pequeño; H2 es el primero que sirve H2 → 288
P2 = 417 H4 (600) H2 (288) y H3 (300) no llegan H4 → 183
P3 = 112 H1 (200) Primer hueco donde cabe H1 → 88
P4 = 426 ninguno Quedan 88, 288, 300, 183, 250 falla

Mejor ajuste:

Petición Hueco elegido Por qué Resto
P1 = 212 H3 (300) Es el más pequeño donde cabe H3 → 88
P2 = 417 H2 (500) El más pequeño de los que sirven (500 < 600) H2 → 83
P3 = 112 H5 (250) El más pequeño donde cabe H5 → 138
P4 = 426 H4 (600) Cabe H4 → 174

Las cuatro peticiones se satisfacen.

Peor ajuste:

Petición Hueco elegido Por qué Resto
P1 = 212 H4 (600) El mayor H4 → 388
P2 = 417 H2 (500) El mayor disponible H2 → 83
P3 = 112 H4 (388) El mayor disponible H4 → 276
P4 = 426 ninguno Quedan 200, 83, 300, 276, 250 falla

Resumen:

Estrategia Peticiones satisfechas Memoria libre final Mayor hueco final
Primer ajuste 3 de 4 909 MB 300 MB
Mejor ajuste 4 de 4 483 MB 174 MB
Peor ajuste 3 de 4 909 MB 300 MB

Cuidado con generalizar de un solo caso. Aquí gana el mejor ajuste, pero los estudios de simulación clásicos concluyen que:

  • Primer ajuste y mejor ajuste son equivalentes en aprovechamiento, y el primer ajuste es notablemente más rápido porque no recorre toda la lista.
  • El peor ajuste es peor que ambos en casi todos los escenarios: destruye sistemáticamente los huecos grandes, que son los más valiosos.
  • El mejor ajuste tiene un defecto acumulativo: deja restos diminutos (83 MB, 88 MB) que nunca servirán para nada, y la lista de huecos crece indefinidamente. Es fragmentación disfrazada de eficiencia.

Por eso el consenso práctico es primer ajuste, o su variante siguiente ajuste (empezar a buscar donde terminó la búsqueda anterior, en lugar de siempre desde el principio), que reparte mejor el desgaste.

Fragmentación interna y externa, con números

La fragmentación es memoria que existe físicamente pero no se puede usar. Hay dos tipos, y confundirlos es uno de los errores más frecuentes:

Fragmentación interna Fragmentación externa
Dónde está el desperdicio Dentro del bloque asignado Entre bloques asignados
Causa El bloque asignado es mayor que lo pedido Los huecos libres están dispersos
A quién pertenece Al proceso (pero no la usa) A nadie
Aparece en Particiones fijas, paginación Particiones variables, segmentación
Se soluciona con Bloques más pequeños Compactación o paginación

Cálculo de fragmentación interna. Supón particiones fijas de 512 MB:

Proceso Necesita Partición Desperdicio interno
ingestor 180 MB 512 MB 332 MB
agregador 490 MB 512 MB 22 MB
meteo-api 300 MB 512 MB 212 MB
backup 60 MB 512 MB 452 MB
Total asignado:     2.048 MB
Total usado:          1.030 MB
Fragmentación interna: 1.018 MB = 49,7 % desperdiciado

Casi la mitad de la memoria asignada no sirve para nada. Y es un desperdicio invisible: el sistema informa de 2.048 MB en uso y es técnicamente cierto.

Cálculo de fragmentación externa. Volvamos al estado final de la simulación de particiones variables:

[Núcleo 400][ libre 600 ][backup 500][ libre 500 ][api 300][ libre 260 ]
Memoria libre total: 600 + 500 + 260 = 1.360 MB
Mayor bloque contiguo: 600 MB
Petición de 900 MB: FALLA pese a haber 1.360 MB libres
Fragmentación externa: 1.360 − 600 = 760 MB inutilizables

La regla del 50 % cuantifica lo malo que llega a ser esto: en un sistema con particiones variables y primer ajuste, por cada N bloques asignados hay estadísticamente 0,5·N bloques libres perdidos por fragmentación, lo que significa que hasta un tercio de la memoria puede quedar inutilizable.

Un matiz importante para más adelante: la paginación elimina totalmente la fragmentación externa (porque todos los bloques miden lo mismo, así que cualquier hueco sirve para cualquier página) pero introduce fragmentación interna acotada. Con páginas de 4 KB:

Fragmentación interna media por región: 4.096 / 2 = 2.048 bytes
Con 180 procesos y ~20 regiones cada uno:
180 × 20 × 2.048 = 7,4 MB de 8 GB = 0,09 %

Cambiar un tercio de la memoria por un 0,09 % es un negocio excelente, y es la razón de fondo por la que todos los sistemas modernos paginan.

Compactación y por qué casi nunca se usa

La solución obvia a la fragmentación externa: mover los procesos para juntar todos los huecos en uno solo.

Antes:
[Núcleo 400][ libre 600 ][backup 500][ libre 500 ][api 300][ libre 260 ]

Después de compactar:
[Núcleo 400][backup 500][api 300][═══════ libre 1360 ═══════]

Ahora el proceso de 900 MB sí cabe. La compactación solo es posible con vinculación en tiempo de ejecución: si las direcciones estuvieran fijadas en la carga, mover un proceso lo rompería. Con base y límite basta con copiar los bytes y actualizar el registro base.

¿Por qué entonces no se usa? Por el coste:

Ancho de banda de memoria típico: 20 GB/s
Mover 800 MB (backup + api):  800 MB / 20 GB/s = 40 ms

Durante esos 40 ms:
- Los procesos movidos no pueden ejecutarse
- El ancho de banda de memoria está saturado, así que TODO va más lento
- 40 ms = 10 quanta enteros de 4 ms

Y esto habría que repetirlo cada vez que la fragmentación vuelva a acumularse, que en un sistema con procesos entrando y saliendo es constantemente. En un servidor con 64 GB, compactar podría costar segundos.

Conclusión: la compactación funciona, pero el remedio es peor que la enfermedad. La solución real fue cambiar el planteamiento: si el problema es exigir que un proceso ocupe un bloque contiguo, eliminemos ese requisito. Eso es la paginación.

Segmentación

Antes de llegar a la paginación, hubo un intento intermedio con una motivación distinta: que la memoria refleje cómo el programador ve su programa.

Un programa no es un bloque uniforme de bytes. Es un conjunto de piezas lógicas: el código, los datos globales, la pila, el montículo, cada biblioteca. La segmentación da a cada una su propio espacio, con su propio tamaño y sus propios permisos.

Una dirección segmentada tiene dos partes:

dirección lógica = <número de segmento, desplazamiento>

Y la traducción usa una tabla de segmentos por proceso, con una base y un límite por cada segmento:

Segmento Nombre Base física Límite Permisos
0 Código 0x0C000000 552.960 (540 KB) r-x
1 Datos globales 0x0C100000 8.192 (8 KB) rw-
2 Montículo 0x0C200000 16.777.216 (16 MB) rw-
3 Pila 0x0D000000 8.388.608 (8 MB) rw-
4 libc (compartido) 0x08000000 2.097.152 (2 MB) r-x

Traducción de <2, 0x1000> (segmento 2, desplazamiento 4096):

0x1000 = 4.096 < 16.777.216  →  válido
física = 0x0C200000 + 0x1000 = 0x0C201000

Traducción de <1, 0x3000> (desplazamiento 12.288 en el segmento de datos de 8 KB):

12.288 > 8.192  →  fuera de límites
→ excepción → SIGSEGV

Las ventajas de la segmentación sobre base y límite simples son reales:

  • Protección por segmento. El segmento de código es r-x: un intento de escribir en él falla. Esto es exactamente lo que impide que un desbordamiento de búfer sobreescriba instrucciones.
  • Compartición natural. El segmento 4 (libc) puede apuntar a la misma base física en 180 procesos. Una sola copia de la biblioteca en RAM.
  • Crecimiento independiente. El montículo puede crecer sin tener que mover la pila.
  • Coincide con la estructura del programa, lo que facilita al enlazador y al depurador.

Pero conserva el pecado original: cada segmento sigue siendo contiguo en memoria física, así que la fragmentación externa persiste. Solo se reduce, porque los segmentos son más pequeños que un proceso entero.

En x86-64 la segmentación existe pero está esencialmente desactivada: los segmentos de código y datos abarcan todo el espacio de direcciones (modelo flat), y solo sobreviven los registros FS y GS, que se usan para el almacenamiento local del hilo y para datos por CPU en el núcleo. La historia eligió la paginación.

Intercambio (swapping) clásico

Última pieza del esquema clásico: ¿qué hacer si los procesos activos no caben todos en RAM?

El intercambio (swapping) consiste en sacar un proceso completo a disco y traerlo de vuelta cuando le toque ejecutarse. Es la función del planificador de medio plazo que mencionamos en 02-02.

sequenceDiagram
    participant P as agregador (en RAM)
    participant K as Núcleo
    participant D as Disco (área de intercambio)
    K->>K: falta memoria para un proceso nuevo
    K->>K: elige una víctima: agregador (bloqueado, baja prioridad)
    K->>D: escribe los 1000 MB del agregador
    Note over K,D: swap out
    K->>K: la RAM queda libre para el proceso nuevo
    Note over P,D: pasa el tiempo
    K->>K: el agregador vuelve a ser ejecutable
    D->>K: lee los 1000 MB
    Note over K,D: swap in
    K->>P: el agregador continúa donde estaba

El coste es brutal. Con un disco mecánico a 100 MB/s:

Sacar 1.000 MB:  1.000 / 100 = 10 segundos
Traerlo de vuelta:               10 segundos
Total por intercambio completo:  20 segundos

Veinte segundos durante los cuales el agregador no existe. Con un SSD NVMe a 3.000 MB/s bajaría a 0,67 segundos, mejor pero aún desmesurado comparado con los microsegundos de un cambio de contexto.

Por eso el intercambio de procesos completos está prácticamente muerto. Lo que hacen los sistemas modernos es intercambiar páginas individuales de 4 KB, no procesos enteros: si el agregador tiene 1 GB pero solo está usando activamente 30 MB, se expulsan las páginas inactivas y se queda el resto. Eso ya es memoria virtual, y es el tema central de la próxima lección.

Aun así, el intercambio clásico sigue vivo en un caso concreto: la hibernación. Cuando suspendes un portátil a disco, el sistema escribe toda la RAM al área de swap. Es exactamente el mismo mecanismo, aplicado a la máquina entera.

La idea de la paginación

Recapitulemos el problema central. La fragmentación externa existe porque exigimos que el espacio de un proceso sea contiguo en memoria física. La compactación intenta arreglar el síntoma. La paginación ataca la causa:

¿Y si un proceso no tuviera que estar contiguo en memoria física?

La idea, en tres pasos:

  1. Divide la memoria física en trozos iguales y pequeños llamados marcos (frames), típicamente de 4 KB.
  2. Divide el espacio lógico de cada proceso en trozos del mismo tamaño llamados páginas.
  3. Coloca cada página en cualquier marco libre, sin importar el orden. Una tabla de páginas por proceso registra qué página está en qué marco.
Espacio lógico del ingestor         Memoria física
┌──────────┐ página 0               ┌──────────┐ marco 0  ← página 2
│          │                        ├──────────┤ marco 1  ← (otro proceso)
├──────────┤ página 1               ├──────────┤ marco 2  ← página 0
│          │                        ├──────────┤ marco 3  ← (libre)
├──────────┤ página 2               ├──────────┤ marco 4  ← página 3
│          │                        ├──────────┤ marco 5  ← página 1
├──────────┤ página 3               ├──────────┤ marco 6  ← (libre)
└──────────┘                        └──────────┘

Las consecuencias son enormes:

Problema Cómo lo resuelve la paginación
Fragmentación externa Desaparece: todos los huecos miden lo mismo, cualquiera sirve
Fragmentación interna Aparece, pero acotada a media página (2 KB) por región
Compactación Innecesaria: nunca hay que mover nada
Compartición Trivial: dos tablas de páginas apuntan al mismo marco
Procesos mayores que la RAM Posible: basta con no tener todas las páginas cargadas
Coste Una tabla de páginas por proceso y una traducción por acceso

Ese último punto es el que hay que resolver bien, y no es trivial: si cada acceso a memoria requiere consultar la tabla de páginas, que también está en memoria, cada acceso costaría el doble. La solución es la TLB, y todo el mecanismo —tablas multinivel, bits de estado, fallos de página, algoritmos de reemplazo— es lo que desarrollaremos en detalle en Memoria Virtual y Paginación.

Por ahora quédate con la idea central: la paginación funciona porque uniformiza el tamaño de los bloques. Cuando todas las piezas son iguales, el problema de encajarlas desaparece.

El mapa de memoria real de un proceso de Meteora

Todo lo anterior deja de ser teoría en cuanto lees /proc/<pid>/maps. Veamos el del ingestor:

$ sudo cat /proc/1842/maps
00400000-00401000 r--p 00000000 fd:01 1573241  /opt/meteora/bin/ingestor
00401000-00489000 r-xp 00001000 fd:01 1573241  /opt/meteora/bin/ingestor
00489000-004a2000 r--p 00089000 fd:01 1573241  /opt/meteora/bin/ingestor
004a2000-004a4000 rw-p 000a1000 fd:01 1573241  /opt/meteora/bin/ingestor
004a4000-004c8000 rw-p 00000000 00:00 0        [heap]
7f2a1c000000-7f2a1c021000 rw-p 00000000 00:00 0
7f2a24a1e000-7f2a24a46000 r--p 00000000 fd:01 2229817  /usr/lib/x86_64-linux-gnu/libc.so.6
7f2a24a46000-7f2a24bce000 r-xp 00028000 fd:01 2229817  /usr/lib/x86_64-linux-gnu/libc.so.6
7f2a24bce000-7f2a24c23000 r--p 001b0000 fd:01 2229817  /usr/lib/x86_64-linux-gnu/libc.so.6
7f2a24c23000-7f2a24c27000 rw-p 00204000 fd:01 2229817  /usr/lib/x86_64-linux-gnu/libc.so.6
7f2a24c27000-7f2a24c34000 rw-p 00000000 00:00 0
7ffd8b3a1000-7ffd8b3c2000 rw-p 00000000 00:00 0        [stack]
7ffd8b3f5000-7ffd8b3f9000 r--p 00000000 00:00 0        [vvar]
7ffd8b3f9000-7ffd8b3fb000 r-xp 00000000 00:00 0        [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

El formato de cada línea, campo a campo:

00401000-00489000   r-xp   00001000   fd:01   1573241   /opt/meteora/bin/ingestor
└──── rango ────┘  └perm┘ └desplaz.┘ └disp.┘ └ inodo ┘  └────── fichero ──────┘
  • Rango: direcciones lógicas de inicio y fin (fin no incluido).
  • Permisos: r lectura, w escritura, x ejecución, y p privada (copy-on-write) o s compartida.
  • Desplazamiento: en qué punto del fichero empieza este mapeo.
  • Dispositivo e inodo: qué fichero se ha mapeado (00:00 y 0 si no es un fichero).

Ahora la interpretación región por región, que es donde está todo el aprendizaje:

Rango Tamaño Permisos Qué es Por qué esos permisos
00400000-00401000 4 KB r--p Cabeceras ELF Solo lectura: son metadatos
00401000-00489000 544 KB r-xp .text, el código Ejecutable pero no escribible
00489000-004a2000 100 KB r--p .rodata, constantes y cadenas Solo lectura: nunca cambian
004a2000-004a4000 8 KB rw-p .data + .bss Escribible pero no ejecutable
004a4000-004c8000 144 KB rw-p [heap], el montículo Crece con malloc
7f2a1c000000-... 132 KB rw-p Arena de malloc vía mmap Bloques grandes o de otro hilo
7f2a24a1e000-... 4 regiones varios libc, con la misma división Compartida entre todos los procesos
7ffd8b3a1000-... 132 KB rw-p [stack], la pila Crece hacia abajo
[vvar] / [vdso] 24 KB r--p/r-xp Código del núcleo en usuario Acelera gettimeofday() sin syscall

Cinco observaciones que merecen atención:

  1. Ninguna región es a la vez escribible y ejecutable. Esa separación estricta se llama W^X (write xor execute) y es una defensa fundamental: aunque un atacante consiga inyectar código en el montículo o la pila, no podrá ejecutarlo porque esas regiones no tienen el bit x. Es una aplicación directa de la protección por región que vimos en segmentación, implementada aquí sobre páginas. Volverá en el módulo 5.

  2. El binario ocupa cuatro regiones, no una. El cargador ELF separa las secciones según sus permisos, exactamente por lo anterior.

  3. libc aparece con la misma estructura de cuatro regiones, y sus partes r-xp están compartidas físicamente con los otros 179 procesos del sistema. Una sola copia del código de libc en RAM sirve para todos. Aquí está la compartición que la segmentación prometía, lograda con paginación.

  4. [vdso] es un truco brillante. El núcleo mapea un pequeño trozo de su propio código en el espacio de cada proceso, para que llamadas muy frecuentes como gettimeofday() o clock_gettime() se resuelvan sin cruzar al modo núcleo. Recuerda el coste del syscall que calculamos en 01-06: entre 50 y 500 ns. El vDSO lo reduce a unos pocos nanosegundos, y por eso existe.

  5. La pila está en 0x7ffd... y el código en 0x0040..., con un abismo entre ambos. Ese hueco enorme no cuesta nada: son direcciones lógicas sin traducción asignada, y no consumen ni un byte de RAM. Reservar espacio de direcciones es gratis; solo cuesta la memoria física efectivamente respaldada.

pmap presenta lo mismo con los tamaños ya calculados, que es más cómodo en el día a día:

$ sudo pmap -x 1842
1842:   /opt/meteora/bin/ingestor --puerto 9010
Address           Kbytes     RSS   Dirty Mode  Mapping
0000000000400000       4       4       0 r---- ingestor
0000000000401000     544     412       0 r-x-- ingestor
0000000000489000     100      64       0 r---- ingestor
00000000004a2000       8       8       8 rw--- ingestor
00000000004a4000     144     144     144 rw---   [ anon ]
00007f2a24a1e000     160     160       0 r---- libc.so.6
00007f2a24a46000    1568     692       0 r-x-- libc.so.6
00007ffd8b3a1000     132      24      24 rw---   [ stack ]
----------------  ------  ------  ------
total kB           18204   13108     892

Las tres columnas numéricas dicen cosas muy distintas y confundirlas lleva a diagnósticos equivocados:

  • Kbytes: espacio de direcciones reservado. Es la suma que da el VSZ de ps.
  • RSS: cuánto de eso está realmente en RAM. Fíjate en libc.so.6: reserva 1.568 KB de código pero solo 692 KB están cargados. El resto son funciones de libc que este proceso nunca ha llamado, y que por tanto nunca se han traído del disco.
  • Dirty: páginas modificadas, que no pueden descartarse sin escribirlas antes en algún sitio. El código nunca está sucio (nunca se modifica), así que puede expulsarse sin coste: si vuelve a hacer falta, se relee del binario.

Esa distinción entre página limpia y sucia es la que gobierna qué se expulsa primero cuando falta memoria, y es una de las claves de la próxima lección.

Errores Comunes y Consejos

Confundir fragmentación interna con externa. Regla mnemotécnica: la interna está dentro de lo que te han dado (te sobra sitio en tu bloque); la externa está fuera, entre bloques ajenos (hay sitio pero no en un solo trozo). Particiones fijas y paginación producen interna; particiones variables y segmentación, externa.

Creer que VSZ es la memoria que usa un proceso. No lo es. Un proceso puede reservar 100 GB de espacio de direcciones en una máquina de 8 GB sin problema, porque las direcciones son gratis. La memoria real es RSS, y aun así con matices: las páginas compartidas de libc se cuentan en el RSS de todos los procesos que la usan, así que sumar los RSS da un total muy superior a la RAM instalada.

Pensar que el mejor ajuste es el mejor. El nombre engaña. Deja migajas inservibles y obliga a recorrer toda la lista de huecos. El primer ajuste es igual de bueno en aprovechamiento y bastante más rápido.

Asumir que segmentación y paginación son alternativas excluyentes. Históricamente se combinaron: x86 de 32 bits hacía segmentación y paginación (la dirección pasaba por la tabla de segmentos y el resultado por la de páginas). En x86-64 la segmentación quedó reducida a un modelo plano, pero no desapareció del todo: FS y GS siguen usándose.

Interpretar mal una "violación de segmento". El mensaje es histórico y confunde: hoy casi siempre significa que se ha accedido a una dirección sin traducción válida en la tabla de páginas, no a un segmento fuera de límites. Para diagnosticarla, compara la dirección que falló con las regiones de /proc/<pid>/maps: si cae en un hueco, es un puntero corrupto; si cae en una región r--p y era una escritura, es un intento de escribir en memoria de solo lectura (típicamente, modificar una cadena literal).

Consejo práctico: cuando un proceso "consume mucha memoria", el orden correcto de investigación es pmap -x <pid> para ver qué región crece. Si crece [heap], es malloc sin free. Si crecen regiones [anon] sueltas, son bloques grandes mapeados con mmap. Si crece [stack], hay recursión descontrolada. Cada caso tiene una causa y un arreglo distintos, y el mapa te lo dice sin tocar el código.

Ejercicios

Ejercicio 1: simular estrategias de asignación

meteo-01 tiene los siguientes huecos libres, en este orden de memoria:

H1: 150 MB   H2: 400 MB   H3: 250 MB   H4: 320 MB   H5: 180 MB

Llegan estas peticiones, en orden: A = 230 MB, B = 140 MB, C = 310 MB, D = 190 MB.

  1. Resuelve la asignación con primer ajuste, mejor ajuste y peor ajuste.
  2. Para cada estrategia, indica cuántas peticiones se satisfacen y cuál es la fragmentación externa resultante.
  3. ¿Cuál habría funcionado mejor? ¿Puedes concluir de aquí cuál es superior en general?

Ejercicio 2: calcular fragmentación en los dos esquemas

Los cuatro procesos de Meteora necesitan: ingestor 180 MB, agregador 490 MB, meteo-api 300 MB, backup 60 MB.

  1. Calcula la fragmentación interna total con particiones fijas de 512 MB.
  2. Calcula la fragmentación interna total con paginación de 4 KB, suponiendo que cada proceso tiene 15 regiones de memoria.
  3. Compara ambos resultados en valor absoluto y porcentaje.
  4. Explica por qué la paginación no produce fragmentación externa.

Ejercicio 3: interpretar un mapa de memoria

Este es el mapa resumido de meteo-api tras 22 horas en marcha:

$ sudo pmap -x 1901 | tail -12
Address           Kbytes     RSS   Dirty Mode  Mapping
0000000000400000     820     680       0 r-x-- meteo-api
00000000006c9000      16      16      16 rw--- meteo-api
0000000001a40000  118784  118784  118784 rw---   [ anon ]
00007f8c14000000    1024      12       0 r-x-- libssl.so.3
00007f8c18a00000    8192     512     512 rw-s- /dev/shm/meteora-cache
00007ffe3c21a000     132      36      36 rw---   [ stack ]
----------------  ------  ------  ------
total kB          148320  135140  119348
  1. ¿Qué región domina el consumo y qué representa probablemente?
  2. La región de libssl.so.3 reserva 1.024 KB pero solo tiene 12 KB en RAM. ¿Es un problema? ¿Por qué ocurre?
  3. El modo de /dev/shm/meteora-cache es rw-s-. ¿Qué significa la s y qué implica?
  4. Si el sistema necesitara liberar memoria urgentemente, ¿qué páginas de este proceso podría expulsar sin escribir nada a disco? Calcula cuántos KB son.
  5. Con estos datos, ¿dirías que meteo-api tiene una fuga de memoria? Justifica qué comprobación harías.

Soluciones

Solución 1

Primer ajuste (el primer hueco donde quepa, recorriendo desde H1):

Petición Huecos disponibles Elegido Resto
A = 230 150, 400, 250, 320, 180 H2 (400) H2 → 170
B = 140 150, 170, 250, 320, 180 H1 (150) H1 → 10
C = 310 10, 170, 250, 320, 180 H4 (320) H4 → 10
D = 190 10, 170, 250, 10, 180 H3 (250) H3 → 60

4 de 4 satisfechas. Huecos finales: 10, 170, 10, 60, 180 = 430 MB libres, mayor bloque 180 MB.

Mejor ajuste (el hueco más pequeño donde quepa):

Petición Huecos disponibles Elegido Por qué Resto
A = 230 150, 400, 250, 320, 180 H3 (250) El más pequeño de los que sirven (250 < 320 < 400) H3 → 20
B = 140 150, 400, 20, 320, 180 H1 (150) 150 es el menor donde cabe H1 → 10
C = 310 10, 400, 20, 320, 180 H4 (320) 320 < 400 H4 → 10
D = 190 10, 400, 20, 10, 180 H2 (400) El único donde cabe H2 → 210

4 de 4 satisfechas. Huecos finales: 10, 210, 20, 10, 180 = 430 MB libres, mayor bloque 210 MB.

Peor ajuste (el hueco más grande):

Petición Huecos disponibles Elegido Resto
A = 230 150, 400, 250, 320, 180 H2 (400) H2 → 170
B = 140 150, 170, 250, 320, 180 H4 (320) H4 → 180
C = 310 150, 170, 250, 180, 180 ninguno falla
D = 190 150, 170, 250, 180, 180 H3 (250) H3 → 60

3 de 4 satisfechas. C queda sin servir pese a haber 930 MB libres antes de intentarlo.

Resumen:

Estrategia Satisfechas Libre total Mayor bloque Fragmentación externa
Primer ajuste 4/4 430 MB 180 MB 250 MB
Mejor ajuste 4/4 430 MB 210 MB 220 MB
Peor ajuste 3/4 620 MB 250 MB 370 MB

3. Análisis. Primer y mejor ajuste empatan en peticiones servidas; el mejor ajuste deja el mayor bloque contiguo algo más grande (210 frente a 180), lo que le da ventaja marginal para una petición futura.

El peor ajuste falla, y su fallo es instructivo: al destinar el hueco de 400 MB a una petición de 230 MB, destruyó el único hueco donde luego habría cabido C (310 MB). Esa es la crítica de fondo al peor ajuste: consume sistemáticamente el recurso más escaso, que son los huecos grandes.

¿Se puede concluir cuál es superior en general? No. Este ejercicio muestra una secuencia concreta. Cambiando el orden de las peticiones el resultado se invierte: si C llegara primero, el peor ajuste la serviría sin problema. Las conclusiones válidas vienen de simulaciones con miles de secuencias aleatorias, y esas dicen que primer y mejor ajuste son estadísticamente equivalentes en aprovechamiento, que el primer ajuste es más rápido —no recorre toda la lista— y que el peor ajuste es sistemáticamente inferior. El primer ajuste gana por velocidad, no por aprovechamiento.

Solución 2

1. Particiones fijas de 512 MB.

Proceso Necesita Particiones Asignado Desperdicio interno
ingestor 180 MB 1 512 MB 332 MB
agregador 490 MB 1 512 MB 22 MB
meteo-api 300 MB 1 512 MB 212 MB
backup 60 MB 1 512 MB 452 MB
Total 1.030 MB 4 2.048 MB 1.018 MB
Fragmentación interna = 1.018 / 2.048 = 49,7 % del espacio asignado

Casi la mitad. Y observa qué desigual es el reparto: el agregador (490 MB) desperdicia solo 22 MB, mientras que backup (60 MB) desperdicia 452 MB. La fragmentación interna con particiones fijas castiga desproporcionadamente a los procesos pequeños, que son mayoría en cualquier sistema real.

2. Paginación de 4 KB con 15 regiones por proceso.

La clave del cálculo: dentro de una región, todas las páginas están llenas salvo la última, que en promedio se llena hasta la mitad.

Desperdicio medio por región = 4.096 / 2 = 2.048 bytes
Regiones totales = 4 procesos × 15 regiones = 60
Fragmentación interna = 60 × 2.048 bytes = 122.880 bytes = 120 KB
Fragmentación interna = 120 KB / 1.030 MB = 0,0114 %

3. Comparación.

Esquema Fragmentación interna Porcentaje Factor
Particiones fijas 512 MB 1.018 MB 49,7 %
Paginación 4 KB 0,12 MB 0,011 % 8.483 veces menos

La diferencia no es de grado, es de naturaleza. Y la razón es puramente aritmética: el desperdicio por fragmentación interna es proporcional al tamaño del bloque de asignación. Con bloques de 512 MB desperdicias hasta 512 MB por proceso; con bloques de 4 KB desperdicias como mucho 4 KB por región. Reducir el bloque en un factor de 131.072 reduce el desperdicio en la misma proporción.

Esto también explica por qué las páginas grandes (2 MB) no son gratis: mejoran la TLB pero multiplican por 512 la fragmentación interna. Lo veremos en la próxima lección.

4. Por qué la paginación no produce fragmentación externa.

La fragmentación externa aparece cuando hay memoria libre suficiente pero no en un bloque contiguo del tamaño necesario. Requiere dos condiciones simultáneas:

  1. Que las peticiones tengan tamaños distintos.
  2. Que la asignación deba ser contigua.

La paginación elimina las dos:

  • Todas las unidades miden exactamente lo mismo (4 KB), así que cualquier marco libre sirve para cualquier página. No existe el concepto de "no cabe": si hay un marco libre, la página cabe en él.
  • No se exige contigüidad: las páginas 0, 1, 2 y 3 de un proceso pueden estar en los marcos 847, 12, 5.301 y 92. La tabla de páginas se encarga de que el proceso vea un espacio continuo.

Formulado de manera exacta: con paginación, si hay N marcos libres, se puede satisfacer cualquier petición de hasta N páginas, sin importar dónde estén esos marcos. Esa garantía es imposible con asignación contigua, y es la razón por la que también desaparece la necesidad de compactar.

Solución 3

1. La región dominante.

0000000001a40000  118784  118784  118784 rw---   [ anon ]

118.784 KB = 116 MB, el 88 % del RSS total del proceso (118.784 de 135.140). Es memoria anónima —no respaldada por ningún fichero—, escribible, privada, y con RSS = Kbytes = Dirty: está entera en RAM y entera modificada.

Que empiece en 0x1a40000, justo por encima del binario, indica que es el montículo expandido con brk (o una arena grande de malloc). Por su tamaño y por tratarse de meteo-api, lo más probable es una caché de respuestas o un conjunto de lecturas cargadas en memoria para servir consultas sin tocar /var/lib/meteora/lecturas/.

Un contraste útil: 116 MB a 24 bytes por Lectura son unos 5,07 millones de lecturas, cerca de siete días de datos a 800 lecturas/segundo.

2. La región de libssl.so.3 con 12 KB de 1.024 KB.

No es un problema en absoluto: es exactamente lo que debe ocurrir.

Ocurre por la paginación por demanda: cuando se mapea una biblioteca, el núcleo no lee su código del disco. Solo crea las entradas en la tabla de páginas marcadas como no presentes. Una página se trae de disco únicamente cuando el proceso intenta ejecutarla y se produce un fallo de página.

meteo-api ha usado 12 KB (tres páginas) de OpenSSL. Todo lo demás —algoritmos de cifrado que no emplea, funciones de gestión de certificados, código de compatibilidad— nunca se ha tocado y por tanto nunca ha ocupado RAM.

El ahorro agregado es enorme: si 20 procesos mapean libssl y cada uno usa unas pocas páginas distintas, el consumo real es una fracción mínima de los 20 × 1.024 KB nominales. Y como el código es de solo lectura, las páginas que sí se cargan se comparten entre todos.

3. El modo rw-s-.

La s significa compartida (shared), frente a la p de privada que llevan todas las demás. La diferencia es fundamental:

p (privada) s (compartida)
Al escribir Copy-on-write: se copia la página Se escribe en la página común
Ven los cambios Solo este proceso Todos los que la mapean
Se propaga al fichero No

Como está sobre /dev/shm/ (un sistema de ficheros en RAM), se trata de memoria compartida entre procesos: probablemente los cuatro trabajadores de meteo-api que veíamos en el pstree de 02-01 comparten una única caché de 8 MB en lugar de tener cuatro copias.

Implicación importante: escribir ahí desde varios procesos a la vez exige sincronización. Sin ella, dos trabajadores actualizando la misma entrada la corromperían. Ese es precisamente el terreno del módulo 3.

4. Páginas expulsables sin escribir a disco.

Son las páginas limpias (Dirty = 0) respaldadas por un fichero: si se necesitan otra vez, se releen del binario original, así que descartarlas es gratis.

Región RSS Dirty ¿Limpia y respaldada? Expulsable sin coste
meteo-api r-x 680 0 680 KB
meteo-api rw 16 16 No, sucia 0
[anon] 118.784 118.784 No, anónima y sucia 0
libssl.so.3 r-x 12 0 12 KB
/dev/shm/... rw-s 512 512 Sucia (y en RAM, tmpfs) 0
[stack] 36 36 No, anónima y sucia 0
Total expulsable sin escritura = 680 + 12 = 692 KB

Solo 692 KB de 135 MB. Y ahí está la lección incómoda: el 99,5 % de la memoria de este proceso es anónima y sucia, así que liberarla exige escribirla al área de swap, con el coste de disco que eso implica. Si meteo-01 no tuviera swap configurado, esa memoria sería directamente irrecuperable y el sistema tendría que recurrir al OOM killer.

Nota adicional sobre los 512 KB de /dev/shm: al residir en tmpfs, no tienen respaldo en disco; solo pueden ir a swap, nunca descartarse.

5. ¿Hay fuga de memoria?

Con estos datos no se puede afirmar. Una foto no distingue una caché legítima de una fuga: ambas se ven igual, como una región anónima grande y sucia.

La comprobación correcta es medir la evolución en el tiempo:

$ for i in $(seq 1 12); do
    printf "%s  RSS=%s kB\n" "$(date +%H:%M)" "$(awk '/VmRSS/{print $2}' /proc/1901/status)"
    sleep 300
  done

La interpretación de la serie resultante:

Patrón observado Diagnóstico
Sube y se estabiliza en un techo Caché con límite. Correcto
Sube en escalones y baja al liberar Uso normal del montículo
Sube linealmente sin parar Fuga confirmada
Sube solo bajo carga y no baja después Fuga proporcional a las peticiones

Un cálculo que orienta desde el primer momento: 116 MB tras 22 horas son unos 5,3 MB/hora. Si ese ritmo fuera constante, en una semana serían 890 MB y en un mes 3,8 GB, lo que en una máquina de 8 GB acabaría en el OOM killer. Que la respuesta dependa de si el ritmo se mantiene o se aplana es justamente por lo que hace falta medir dos veces.

Comprobaciones complementarias: pmap -x repetido para ver qué región crece; sudo cat /proc/1901/status | grep VmPeak para conocer el máximo histórico; y en el código, revisar si la caché tiene una política de expulsión o crece sin límite, que es la causa más frecuente de este perfil.

Conclusión

Gestionar la memoria significa resolver a la vez reubicación, protección, compartición, organización lógica y capacidad, y con una restricción durísima: la protección debe comprobarse en cada acceso, lo que obliga a resolverla en hardware.

La pieza clave es la separación entre direcciones lógicas y físicas. Vincularlas en tiempo de ejecución —y no en compilación ni en carga— es lo que permite que la MMU traduzca en cada acceso, que un proceso pueda moverse, que exista ASLR y que dos procesos usen la misma dirección 0x401000 sin pisarse. El esquema mínimo que lo consigue son los registros base y límite: una comparación y una suma, protección completa, e imposible de eludir porque solo pueden cargarse desde modo núcleo.

La asignación contigua —particiones fijas o variables, con primer, mejor o peor ajuste— funciona pero choca contra la fragmentación: interna (hasta el 49,7 % del espacio asignado en nuestro cálculo con particiones de 512 MB) o externa (hasta un tercio de la memoria, por la regla del 50 %). La compactación la resuelve, pero costaría 40 ms cada vez, así que no es una solución. La segmentación aportó lo mejor de la asignación contigua —permisos por región y compartición— sin curar la fragmentación externa, y el intercambio de procesos completos murió en cuanto se hicieron los números: 20 segundos por un agregador de 1 GB.

La salida no fue mejorar la asignación contigua, sino abandonar el requisito de contigüidad. Si todas las piezas miden lo mismo, encajarlas deja de ser un problema: eso es la paginación. Y todo esto deja de ser teoría al leer /proc/<pid>/maps y pmap -x, donde ves código compartido y no escribible, libc con solo el 44 % de su código realmente cargado, W^X protegiendo pila y montículo, y la diferencia entre Kbytes, RSS y Dirty que decide qué se puede expulsar y qué no.

Queda la parte grande: cómo funciona realmente la paginación. Cómo se traduce una dirección con número de página y desplazamiento, por qué una tabla plana de 64 bits sería imposible y cómo lo arreglan las tablas multinivel, qué es la TLB y cuánto rendimiento depende de ella, qué ocurre exactamente cuando una página no está en RAM, qué página expulsar cuando no queda hueco y por qué un sistema puede entrar en hiperpaginación y dejar de avanzar. Todo eso, y mmap() mapeando 2026-08-31.dat en memoria, está en Memoria Virtual y Paginación.

Fundamentos de Sistemas Operativos

Módulo 1: Introducción a los Sistemas Operativos

Módulo 2: Gestión de Recursos

Módulo 3: Concurrencia

Módulo 4: Estructuras de Archivos

Módulo 5: Protección y Seguridad del Sistema

Módulo 6: Virtualización y Contenedores

Módulo 7: Administración y Diagnóstico en la Práctica

© Copyright 2026. Todos los derechos reservados