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
- El problema: memoria compartida sin invasiones
- Direcciones lógicas y direcciones físicas
- Las tres fases de vinculación de direcciones
- Reubicación con registro base y límite
- La MMU: el traductor en el camino crítico
- Asignación contigua: particiones fijas y variables
- Estrategias de asignación: primer, mejor y peor ajuste
- Fragmentación interna y externa, con números
- Compactación y por qué casi nunca se usa
- Segmentación
- Intercambio (swapping) clásico
- La idea de la paginación
- 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:
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 | Sí |
Es la que ves en /proc/pid/maps |
Sí | 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 | Sí (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 → SIGSEGVEjemplo 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:
- Todo el proceso debe estar en RAM y en un bloque contiguo. Si necesita 8 MB, hace falta un hueco de 8 MB seguidos.
- No permite permisos diferenciados. Todo el espacio tiene el mismo tratamiento: no puedes marcar el código como no escribible.
- 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, cadapusha 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-apinecesita 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
agregadornecesita 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:
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:
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:
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):
Traducción de <1, 0x3000> (desplazamiento 12.288 en el segmento de datos de 8 KB):
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:
- Divide la memoria física en trozos iguales y pequeños llamados marcos (frames), típicamente de 4 KB.
- Divide el espacio lógico de cada proceso en trozos del mismo tamaño llamados páginas.
- 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:
rlectura,wescritura,xejecución, ypprivada (copy-on-write) oscompartida. - Desplazamiento: en qué punto del fichero empieza este mapeo.
- Dispositivo e inodo: qué fichero se ha mapeado (
00:00y0si 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:
-
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. -
El binario ocupa cuatro regiones, no una. El cargador ELF separa las secciones según sus permisos, exactamente por lo anterior.
-
libc aparece con la misma estructura de cuatro regiones, y sus partes
r-xpestá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. -
[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 comogettimeofday()oclock_gettime()se resuelvan sin cruzar al modo núcleo. Recuerda el coste delsyscallque calculamos en 01-06: entre 50 y 500 ns. El vDSO lo reduce a unos pocos nanosegundos, y por eso existe. -
La pila está en
0x7ffd...y el código en0x0040..., 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 elVSZdeps.RSS: cuánto de eso está realmente en RAM. Fíjate enlibc.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:
Llegan estas peticiones, en orden: A = 230 MB, B = 140 MB, C = 310 MB, D = 190 MB.
- Resuelve la asignación con primer ajuste, mejor ajuste y peor ajuste.
- Para cada estrategia, indica cuántas peticiones se satisfacen y cuál es la fragmentación externa resultante.
- ¿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.
- Calcula la fragmentación interna total con particiones fijas de 512 MB.
- Calcula la fragmentación interna total con paginación de 4 KB, suponiendo que cada proceso tiene 15 regiones de memoria.
- Compara ambos resultados en valor absoluto y porcentaje.
- 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
- ¿Qué región domina el consumo y qué representa probablemente?
- La región de
libssl.so.3reserva 1.024 KB pero solo tiene 12 KB en RAM. ¿Es un problema? ¿Por qué ocurre? - El modo de
/dev/shm/meteora-cacheesrw-s-. ¿Qué significa lasy qué implica? - 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.
- Con estos datos, ¿dirías que
meteo-apitiene 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 |
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
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:
- Que las peticiones tengan tamaños distintos.
- 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.
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 | Sí |
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 | Sí | 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 | Sí | 12 KB |
/dev/shm/... rw-s |
512 | 512 | Sucia (y en RAM, tmpfs) | 0 |
[stack] |
36 | 36 | No, anónima y sucia | 0 |
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
doneLa 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
- Conceptos Básicos de Sistemas Operativos
- Historia y Evolución de los Sistemas Operativos
- Tipos de Sistemas Operativos
- Funciones Principales de un Sistema Operativo
- Arquitectura del Núcleo: Monolítico, Microkernel e Híbrido
- Modo Usuario, Modo Núcleo y Llamadas al Sistema
Módulo 2: Gestión de Recursos
- Gestión de Procesos
- Planificación de la CPU
- Gestión de Memoria
- Memoria Virtual y Paginación
- Gestión de Almacenamiento
- Gestión de Dispositivos
- Controladores, Interrupciones y Operaciones de E/S
Módulo 3: Concurrencia
- Conceptos de Concurrencia
- Hilos y Procesos
- Comunicación entre Procesos (IPC)
- Sincronización y Exclusión Mutua
- Problemas Clásicos de Concurrencia
- Interbloqueos: Prevención, Detección y Recuperación
Módulo 4: Estructuras de Archivos
- Sistemas de Archivos
- Estructuras de Directorios
- Particiones, Montaje y Sistema de Archivos Virtual
- Gestión de Archivos
- Asignación de Espacio, Journaling e Integridad
- Seguridad y Permisos de Archivos
Módulo 5: Protección y Seguridad del Sistema
- Principios de Protección y Control de Acceso
- Usuarios, Autenticación y Escalada de Privilegios
- Amenazas Comunes y Endurecimiento del Sistema
- Auditoría, Registros y Respuesta a Incidentes
Módulo 6: Virtualización y Contenedores
- Virtualización: Hipervisores y Máquinas Virtuales
- Contenedores: Namespaces y cgroups
- El Sistema Operativo en la Nube
- Sistemas Operativos Móviles y de Tiempo Real
