En la lección anterior llegamos a la idea de la paginación y la dejamos planteada: si todas las piezas miden lo mismo, encajarlas deja de ser un problema. Ahora toca cumplir la promesa. Esta es la lección más densa del módulo y probablemente la más rentable de todo el curso, porque la memoria virtual es el mecanismo que explica una cantidad enorme de cosas que verás en producción: por qué un proceso reserva 400 MB y solo usa 31, por qué la primera petición a meteo-api tras un despliegue es lenta y las siguientes no, por qué un servidor que empieza a paginar deja de responder de golpe, y por qué el OOM killer elige lo que elige.
Vas a ver la traducción de direcciones con números concretos, calcular el tamaño imposible de una tabla plana de 64 bits, medir cuánto rendimiento depende de la TLB, seguir un fallo de página paso a paso, resolver los algoritmos de reemplazo sobre una misma cadena de referencias y mapear el fichero 2026-08-31.dat en memoria con mmap(). Al final sabrás interpretar free -h, vmstat y el rastro del OOM killer con criterio.
Contenido
- Páginas y marcos
- Traducción de direcciones, con un ejemplo numérico completo
- La tabla de páginas y sus bits de control
- Por qué una tabla plana es imposible en 64 bits
- Tablas multinivel
- La TLB y el tiempo efectivo de acceso
- Páginas grandes (huge pages)
- Paginación por demanda y el fallo de página
- Algoritmos de reemplazo de páginas
- Asignación de marcos, hiperpaginación y conjunto de trabajo
- Ficheros mapeados y memoria compartida con
mmap() - Copy-on-write revisitado
- Swap,
swappinessy el OOM killer - Medición:
VSZfrente aRSS,/proc/meminfo,free -h,vmstat
Páginas y marcos
Los dos términos son la base de todo el vocabulario y conviene fijarlos sin ambigüedad:
| Página | Marco (frame) | |
|---|---|---|
| Dónde vive | Espacio lógico del proceso | Memoria física |
| Tamaño | 4 KB (típico) | 4 KB (el mismo, obligatoriamente) |
| Cuántas hay | Hasta 2^36 en x86-64 | RAM / 4 KB |
| Se numeran desde | 0, por proceso | 0, globalmente |
Comprueba el tamaño de página de tu sistema:
Y los números de meteo-01:
Espacio lógico por proceso: 2^47 bytes = 128 TB → 2^35 páginas RAM instalada: 8 GB = 8.589.934.592 bytes → 2.097.152 marcos
Hay treinta y cuatro mil millones de veces más páginas posibles que marcos disponibles. Esa desproporción es exactamente lo que hace posible la memoria virtual: la inmensa mayoría de las páginas nunca existirá, y las que existan no tienen por qué estar todas en RAM a la vez.
Traducción de direcciones, con un ejemplo numérico completo
Una dirección lógica se parte en dos campos:
┌────────────────────────┬──────────────────────┐ │ Número de página (p) │ Desplazamiento (d) │ └────────────────────────┴──────────────────────┘
- El desplazamiento ocupa tantos bits como haga falta para direccionar dentro de una página. Con páginas de 4 KB = 2^12 bytes, son 12 bits.
- El número de página ocupa el resto.
La traducción tiene tres pasos:
- Extraer
pydde la dirección lógica. - Buscar en la tabla de páginas la entrada
p, que contiene el número de marcof. - La dirección física es
f × tamaño_página + d.
El desplazamiento no se traduce nunca. Página y marco miden lo mismo, así que la posición dentro de la página es idéntica a la posición dentro del marco. Solo cambia qué bloque, no dónde dentro del bloque.
Ejemplo completo
Trabajemos con un espacio lógico pequeño para poder verlo todo: direcciones de 16 bits y páginas de 4 KB.
Dirección lógica: 16 bits Página: 4 KB = 2^12 → desplazamiento de 12 bits Número de página: 16 − 12 = 4 bits → 16 páginas (0 a 15)
Tabla de páginas del ingestor:
| Página | Marco | Presente |
|---|---|---|
| 0 | 5 | Sí |
| 1 | 9 | Sí |
| 2 | 2 | Sí |
| 3 | — | No |
| 4 | 7 | Sí |
Traducir la dirección lógica 0x2A5C.
Paso 1, descomponer:
0x2A5C en binario: 0010 1010 0101 1100
└──┘ └────────────┘
p d
p = 0010₂ = 2
d = 1010 0101 1100₂ = 0xA5C = 2652Comprobación aritmética, que suele ser más rápida:
Paso 2, consultar la tabla: página 2 → marco 2, presente.
Paso 3, componer la dirección física:
Ha coincidido con la lógica por casualidad (la página 2 está en el marco 2). Probemos otra.
Traducir 0x105C:
p = 0x105C / 4096 = 4188 / 4096 = 1 d = 4188 − 4096 = 92 Tabla: página 1 → marco 9 física = 9 × 4096 + 92 = 36864 + 92 = 36956 = 0x905C
Observa un detalle revelador: 0x105C → 0x905C. Los tres dígitos hexadecimales de la derecha no cambian (05C), porque son el desplazamiento. Solo cambia el dígito de la izquierda: 1 → 9, de página a marco. En hexadecimal la traducción es visualmente evidente cuando el tamaño de página es potencia de 16.
Traducir 0x3200:
Aquí no hay traducción posible. La MMU genera una excepción y el núcleo toma el control. Qué hace entonces es el apartado 8.
La tabla de páginas y sus bits de control
Cada entrada de la tabla de páginas (PTE, Page Table Entry) ocupa 8 bytes en x86-64 y contiene mucho más que un número de marco:
| Bit | Nombre | Qué significa | Quién lo pone |
|---|---|---|---|
| 0 | P (Present) | La página está en RAM | El núcleo |
| 1 | R/W | 0 = solo lectura, 1 = lectura/escritura | El núcleo |
| 2 | U/S | 0 = solo modo núcleo, 1 = accesible en usuario | El núcleo |
| 3 | PWT | Política de caché write-through | El núcleo |
| 4 | PCD | Caché deshabilitada (para E/S mapeada) | El núcleo |
| 5 | A (Accessed) | Se ha accedido a la página | El hardware |
| 6 | D (Dirty) | Se ha escrito en la página | El hardware |
| 7 | PS (Page Size) | Es una página grande (2 MB o 1 GB) | El núcleo |
| 8 | G (Global) | No se invalida al cambiar de proceso | El núcleo |
| 12-51 | Número de marco | Los 40 bits del marco físico | El núcleo |
| 63 | NX (No eXecute) | La página no puede ejecutarse | El núcleo |
Los que importan de verdad y por qué:
Bit P (presente). Es el interruptor de toda la memoria virtual. Si vale 0, cualquier acceso provoca un fallo de página. El núcleo lo usa para tres situaciones distintas, y distingue entre ellas con los bits restantes de la entrada, que quedan libres cuando P=0: página nunca cargada, página expulsada a swap, o dirección simplemente inválida.
Bits R/W y NX. Aquí es donde se implementa la protección por región que veíamos en /proc/<pid>/maps. La región r-xp del código tiene R/W=0 (no escribible); las regiones rw-p de pila y montículo tienen NX=1 (no ejecutable). Ese par de bits es la política W^X.
Bits A y D. Son especiales porque los escribe el hardware, no el sistema operativo. Cada vez que la CPU accede a una página pone A=1; cada vez que escribe pone D=1. El núcleo los lee y los limpia periódicamente. Sin ellos sería imposible implementar los algoritmos de reemplazo: el núcleo no tiene forma de observar cada acceso a memoria, así que necesita que el hardware le deje esa pista.
Bit D (sucio). Determina el coste de expulsar la página. Si D=0, la página no ha cambiado desde que se cargó, así que se puede descartar sin más (si vuelve a hacer falta se relee del fichero). Si D=1, hay que escribirla en swap antes de reutilizar el marco. Es exactamente la distinción de la columna Dirty de pmap que vimos en 02-03, y la diferencia de coste es de cero frente a milisegundos.
Por qué una tabla plana es imposible en 64 bits
Hagamos el cálculo que justifica toda la complejidad que viene después.
En x86-64 se usan actualmente 48 bits de dirección virtual (los 16 superiores son extensión de signo). Con páginas de 4 KB:
Bits para el desplazamiento: 12 Bits para el número de página: 48 − 12 = 36 Número de páginas posibles: 2^36 = 68.719.476.736 Tamaño de una tabla plana: 68.719.476.736 entradas × 8 bytes = 549.755.813.888 bytes = 512 GB
512 GB de tabla de páginas. Por proceso. En una máquina de 8 GB con 180 procesos, harían falta 92 TB solo para las tablas.
Es absurdo, y lo es por una razón muy concreta: la tabla plana reserva una entrada para cada página posible, incluidas las que nunca existirán. Recuerda el mapa de memoria del ingestor: el código está en 0x400000 y la pila en 0x7ffd8b3a1000. Entre ambos hay un abismo de 140 TB de direcciones que jamás se usarán, y una tabla plana necesitaría una entrada para cada una.
El proceso real usa unas 4.500 páginas de los 68.000 millones posibles: el 0,0000065 %. La estructura debe aprovechar esa dispersión extrema.
Tablas multinivel
La solución es hacer la tabla jerárquica y dispersa: dividir el número de página en varios campos, cada uno indexando un nivel, y crear solo las tablas de nivel inferior que realmente hagan falta.
x86-64 usa cuatro niveles (cinco en las CPU más recientes). El número de página de 36 bits se parte en cuatro campos de 9 bits:
┌────────┬────────┬────────┬────────┬──────────────┐ │ PML4 │ PDPT │ PD │ PT │ Desplaz. 12 │ │ 9 bits │ 9 bits │ 9 bits │ 9 bits │ │ └────────┴────────┴────────┴────────┴──────────────┘
Cada nivel tiene 2^9 = 512 entradas de 8 bytes = exactamente 4.096 bytes, una página. Ese encaje no es casual: cada tabla ocupa justo una página, lo que simplifica enormemente su gestión.
flowchart LR
CR3["Registro CR3<br/>(por proceso)"] --> PML4
PML4["PML4<br/>512 entradas"] -->|índice 9 bits| PDPT
PDPT["PDPT<br/>512 entradas"] -->|índice 9 bits| PD
PD["Directorio<br/>512 entradas"] -->|índice 9 bits| PT
PT["Tabla de páginas<br/>512 entradas"] -->|índice 9 bits| MARCO["Marco físico<br/>+ desplazamiento"]
El ahorro es espectacular. Para un proceso que usa código en la parte baja y pila en la alta:
1 tabla PML4: 4 KB
2 tablas PDPT (una por zona): 8 KB
2 tablas PD: 8 KB
~10 tablas PT (para ~5.000 páginas): 40 KB
───────
Total: 60 KB60 KB frente a 512 GB. Un factor de ocho millones.
El precio: una traducción requiere cuatro accesos a memoria (uno por nivel) más el acceso a los datos. Cinco accesos donde antes había uno. A ~100 ns por acceso a RAM, eso serían 500 ns por cada lectura de memoria: el sistema sería 5 veces más lento que sin paginación. Inaceptable.
Esa es la razón exacta por la que existe la TLB.
La TLB y el tiempo efectivo de acceso
La TLB (Translation Lookaside Buffer) es una caché asociativa, dentro de la MMU, que guarda las traducciones página→marco usadas recientemente. Es pequeña (entre 64 y 1.536 entradas en CPU modernas) y rapidísima (menos de 1 ns).
El flujo:
- La CPU genera una dirección lógica.
- La MMU busca el número de página en la TLB.
- Acierto de TLB: se obtiene el marco directamente. Coste ≈ 0.
- Fallo de TLB: hay que recorrer los cuatro niveles de tablas en memoria y luego insertar el resultado en la TLB.
El cálculo del tiempo efectivo de acceso (TEA) es la fórmula que hay que saber hacer:
donde h es la tasa de aciertos, t_TLB el tiempo de consulta de la TLB, t_mem el acceso a memoria y n el número de niveles.
Con valores realistas (t_TLB = 1 ns, t_mem = 100 ns, n = 4):
| Tasa de aciertos | Cálculo | TEA | Degradación |
|---|---|---|---|
| 100 % | 1 + 100 | 101 ns | 1,00× |
| 99 % | 0,99×101 + 0,01×501 | 105 ns | 1,04× |
| 95 % | 0,95×101 + 0,05×501 | 121 ns | 1,20× |
| 90 % | 0,90×101 + 0,10×501 | 141 ns | 1,40× |
| 70 % | 0,70×101 + 0,30×501 | 221 ns | 2,19× |
| 50 % | 0,50×101 + 0,50×501 | 301 ns | 2,98× |
Detalle del caso del 99 %:
Acierto (99 %): 1 ns (TLB) + 100 ns (dato) = 101 ns Fallo (1 %): 1 ns + 4×100 ns (tablas) + 100 ns (dato) = 501 ns TEA = 0,99 × 101 + 0,01 × 501 = 99,99 + 5,01 = 105,0 ns
La conclusión práctica es contundente: con un 99 % de aciertos pierdes solo un 4 %; con un 70 % pierdes más del doble de rendimiento. La TLB no es una optimización menor, es lo que hace viable la paginación.
Y por eso importa tanto el coste del cambio de contexto que calculamos en 02-01: al cambiar cr3, las entradas de TLB del proceso anterior dejan de valer y se invalidan. El proceso entrante arranca con la TLB vacía y una ráfaga de fallos. Los PCID (identificadores de contexto de proceso) mitigan esto etiquetando cada entrada con el proceso al que pertenece, de modo que no haga falta vaciarla entera.
Puedes ver los fallos de TLB reales:
$ sudo perf stat -e dTLB-load-misses,dTLB-loads -p 1877 sleep 10
Performance counter stats for process id '1877':
12.847.331 dTLB-load-misses # 0,84% of all dTLB cache accesses
1.529.204.882 dTLB-loads
10,003 segundos time elapsedUn 0,84 % de fallos, es decir un 99,16 % de aciertos. Perfectamente sano. Si vieras un 15 % de fallos, tendrías un proceso con un patrón de acceso muy disperso, y ahí es donde entran las páginas grandes.
Páginas grandes (huge pages)
El problema aparece cuando un proceso trabaja con conjuntos de datos enormes. El agregador procesando un día entero de lecturas:
Datos de un día: 17 MB Páginas de 4 KB necesarias: 17.000.000 / 4.096 ≈ 4.150 páginas Entradas de TLB disponibles (típico L2 TLB): 1.536
No caben. Recorrer los 17 MB provoca fallos de TLB continuos, porque las traducciones se expulsan entre sí. Y con 30 días de histórico serían 124.000 páginas: la TLB sería inútil.
Las páginas grandes resuelven esto usando un tamaño de página mayor: 2 MB o 1 GB en x86-64. Se consigue parando la jerarquía un nivel antes (una entrada del directorio de páginas apunta directamente a un bloque de 2 MB en lugar de a una tabla).
| 4 KB | 2 MB | 1 GB | |
|---|---|---|---|
| Páginas para 17 MB | 4.150 | 9 | 1 |
| Entradas de TLB usadas | 4.150 | 9 | 1 |
| Niveles de traducción | 4 | 3 | 2 |
| Fragmentación interna media | 2 KB | 1 MB | 512 MB |
| Granularidad de expulsión a swap | Fina | Gruesa | Inviable |
Los 17 MB del agregador pasan de 4.150 entradas de TLB a 9. Con eso caben de sobra y los fallos de TLB desaparecen prácticamente.
En Linux hay dos formas de usarlas:
$ cat /sys/kernel/mm/transparent_hugepage/enabled [always] madvise never $ grep -i huge /proc/meminfo AnonHugePages: 215040 kB HugePages_Total: 0 HugePages_Free: 0 Hugepagesize: 2048 kB
- THP (Transparent Huge Pages): el núcleo promueve automáticamente regiones grandes a páginas de 2 MB. Los 215 MB de
AnonHugePagesindican que ya está ocurriendo. Es cómodo, pero tiene un coste conocido: el demoniokhugepagedcompacta memoria en segundo plano y puede introducir pausas. Por eso muchas bases de datos recomiendan ponerlo enmadviseonever. - Huge pages explícitas: se reservan al arranque y la aplicación las pide expresamente. Más control, menos comodidad.
Regla de decisión: las páginas grandes ayudan cuando el conjunto de trabajo es grande y se recorre de forma dispersa; estorban cuando la memoria es escasa y hay que paginar, porque expulsar 2 MB de golpe es muy caro.
Paginación por demanda y el fallo de página
Aquí llega la pieza que convierte la paginación en memoria virtual: no hace falta que todas las páginas de un proceso estén en RAM.
La paginación por demanda consiste en no cargar nada hasta que se necesite. Al arrancar meteo-api, el núcleo no lee sus 820 KB de código: crea las entradas con P=0 y deja que los fallos de página traigan lo que haga falta. Esto explica los datos que vimos en 02-03: libssl con 12 KB de 1.024 KB cargados.
Cuando la CPU accede a una página con P=0:
sequenceDiagram
participant P as Proceso
participant M as MMU
participant K as Núcleo
participant D as Disco
P->>M: acceso a la dirección 0x3200
M->>M: consulta la tabla: bit P = 0
M->>K: excepción de fallo de página (#PF)<br/>dirección en CR2
K->>K: ¿es una dirección válida del proceso?
alt Dirección inválida
K->>P: SIGSEGV → violación de segmento
else Dirección válida
K->>K: busca un marco libre
alt No hay marcos libres
K->>K: elige una víctima (algoritmo de reemplazo)
K->>D: si está sucia, la escribe en swap
end
K->>D: lee la página del fichero o del swap
D-->>K: datos (0,1 - 10 ms)
K->>K: actualiza la PTE: marco y P = 1
K->>P: reintenta la instrucción que falló
end
Un detalle esencial del final: la instrucción que falló se vuelve a ejecutar desde cero. El proceso no se entera de nada. Para él, ese acceso a memoria simplemente tardó mucho.
El coste real de un fallo de página
Aquí es donde los números duelen. Hay dos tipos de fallo muy distintos:
| Tipo | Qué ocurre | Coste | Ejemplo |
|---|---|---|---|
| Fallo menor (minor) | La página está en RAM pero no mapeada en este proceso | 1-3 µs | Caché de páginas, COW, biblioteca ya cargada |
| Fallo mayor (major) | Hay que leerla del disco | 0,1-10 ms | Primera lectura de un fichero, página en swap |
La diferencia es de tres a cuatro órdenes de magnitud:
Fallo menor con SSD NVMe: ~2 µs Fallo mayor con SSD NVMe: ~100 µs (50 veces más) Fallo mayor con disco duro: ~8 ms (4.000 veces más)
Y ahora el cálculo que explica por qué la tasa de fallos mayores tiene que ser minúscula. Con acceso normal a memoria de 100 ns y fallo mayor de 8 ms:
Tasa de fallos p |
TEA | Degradación |
|---|---|---|
| 0 | 100 ns | 1× |
| 1 en 1.000.000 | 108 ns | 1,08× |
| 1 en 100.000 | 180 ns | 1,8× |
| 1 en 10.000 | 900 ns | 9× |
| 1 en 1.000 | 8.100 ns | 81× |
Con un fallo mayor por cada mil accesos, el sistema va 81 veces más lento. Para mantener la degradación por debajo del 10 % hace falta menos de un fallo por cada millón de accesos.
Esto no es una curiosidad académica: es exactamente lo que le ocurre a un servidor que empieza a paginar. No se degrada suavemente, cae por un precipicio. Y es la razón de la hiperpaginación que veremos en el apartado 10.
Puedes ver los fallos de tus procesos:
$ ps -eo pid,min_flt,maj_flt,comm -u meteora
PID MINFL MAJFL COMMAND
1842 84213 3 ingestor
1877 291045 112 agregador
1901 138922 8 meteo-apiLos fallos menores son normales y abundantes: forman parte del funcionamiento habitual. Los mayores son los que importan: 112 en el agregador tras 22 horas es perfectamente sano. Si vieras 400.000, el proceso estaría leyendo constantemente de swap y ese sería el problema a resolver.
Algoritmos de reemplazo de páginas
Cuando ocurre un fallo de página y no quedan marcos libres, hay que expulsar a alguien. La elección determina cuántos fallos habrá después.
Resolveremos todos los algoritmos sobre la misma cadena de referencias, que es la única forma honesta de compararlos:
Óptimo (OPT)
Expulsa la página que tardará más en volver a usarse. Es imposible de implementar (requiere conocer el futuro), pero sirve como cota inferior con la que comparar.
| Ref | Marcos | Fallo | Víctima y por qué |
|---|---|---|---|
| 7 | [7,-,-] | ● | — |
| 0 | [7,0,-] | ● | — |
| 1 | [7,0,1] | ● | — |
| 2 | [2,0,1] | ● | 7 (no vuelve hasta el final) |
| 0 | [2,0,1] | ya está | |
| 3 | [2,0,3] | ● | 1 (vuelve en la posición 14) |
| 0 | [2,0,3] | ya está | |
| 4 | [2,4,3] | ● | 0 (vuelve más tarde que 2 y 3) |
| 2 | [2,4,3] | ||
| 3 | [2,4,3] | ||
| 0 | [2,0,3] | ● | 4 (no vuelve nunca) |
| 3 | [2,0,3] | ||
| 2 | [2,0,3] | ||
| 1 | [2,0,1] | ● | 3 (no vuelve) |
| 2 | [2,0,1] | ||
| 0 | [2,0,1] | ||
| 1 | [2,0,1] | ||
| 7 | [7,0,1] | ● | 2 (no vuelve) |
| 0 | [7,0,1] | ||
| 1 | [7,0,1] |
9 fallos.
FIFO
Expulsa la página que lleva más tiempo cargada, sin importar su uso.
| Ref | Marcos (orden de llegada) | Fallo |
|---|---|---|
| 7 | [7] | ● |
| 0 | [7,0] | ● |
| 1 | [7,0,1] | ● |
| 2 | [0,1,2] | ● sale 7 |
| 0 | [0,1,2] | |
| 3 | [1,2,3] | ● sale 0 |
| 0 | [2,3,0] | ● sale 1 |
| 4 | [3,0,4] | ● sale 2 |
| 2 | [0,4,2] | ● sale 3 |
| 3 | [4,2,3] | ● sale 0 |
| 0 | [2,3,0] | ● sale 4 |
| 3 | [2,3,0] | |
| 2 | [2,3,0] | |
| 1 | [3,0,1] | ● sale 2 |
| 2 | [0,1,2] | ● sale 3 |
| 0 | [0,1,2] | |
| 1 | [0,1,2] | |
| 7 | [1,2,7] | ● sale 0 |
| 0 | [2,7,0] | ● sale 1 |
| 1 | [7,0,1] | ● sale 2 |
15 fallos. Un 67 % más que el óptimo.
La anomalía de Belady
FIFO tiene un defecto que desafía la intuición: darle más memoria puede aumentar los fallos. Con esta cadena:
Con 3 marcos:
| Ref | 1 | 2 | 3 | 4 | 1 | 2 | 5 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Fallo | ● | ● | ● | ● | ● | ● | ● | ● | ● |
9 fallos.
Con 4 marcos:
| Ref | 1 | 2 | 3 | 4 | 1 | 2 | 5 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Fallo | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● |
10 fallos. Más memoria, más fallos.
La causa: FIFO no tiene la propiedad de pila (que el conjunto de páginas con n marcos esté contenido en el de n+1 marcos). Al añadir un marco, el orden de expulsión cambia por completo y puede expulsar justo lo que iba a hacer falta. LRU y OPT sí tienen esa propiedad y por eso están libres de la anomalía.
Es la razón principal por la que FIFO puro no se usa en ningún sistema real: un algoritmo del que no puedes afirmar "con más RAM irá mejor" es inaceptable.
LRU (menos usada recientemente)
Expulsa la página que lleva más tiempo sin usarse. Se apoya en el principio de localidad temporal: lo que se ha usado hace poco probablemente se vuelva a usar.
| Ref | Marcos (más reciente a la derecha) | Fallo |
|---|---|---|
| 7 | [7] | ● |
| 0 | [7,0] | ● |
| 1 | [7,0,1] | ● |
| 2 | [0,1,2] | ● sale 7 |
| 0 | [1,2,0] | |
| 3 | [2,0,3] | ● sale 1 |
| 0 | [2,3,0] | |
| 4 | [3,0,4] | ● sale 2 |
| 2 | [0,4,2] | ● sale 3 |
| 3 | [4,2,3] | ● sale 0 |
| 0 | [2,3,0] | ● sale 4 |
| 3 | [2,0,3] | |
| 2 | [0,3,2] | |
| 1 | [3,2,1] | ● sale 0 |
| 2 | [3,1,2] | |
| 0 | [1,2,0] | ● sale 3 |
| 1 | [2,0,1] | |
| 7 | [0,1,7] | ● sale 2 |
| 0 | [1,7,0] | |
| 1 | [7,0,1] |
12 fallos. Entre el óptimo (9) y FIFO (15).
El problema de LRU es su coste de implementación. Requiere ordenar las páginas por último acceso, y eso significa actualizar una estructura en cada acceso a memoria. Sería necesario hardware que, en cada lectura, moviese una entrada al principio de una lista de miles de elementos. Ninguna CPU lo hace, porque sería carísimo.
Aproximaciones a LRU: segunda oportunidad y algoritmo del reloj
Como LRU exacto es inviable, se aproxima usando el bit A (accedido) que el hardware sí mantiene gratis.
El algoritmo del reloj organiza los marcos en un círculo con un puntero:
- El puntero apunta a un marco candidato.
- Si su bit A = 0, se expulsa y el puntero avanza.
- Si su bit A = 1, se le da una segunda oportunidad: se pone A = 0 y el puntero avanza al siguiente, sin expulsar nada.
- Se repite hasta encontrar un marco con A = 0.
┌───────┐
┌───→│ P3 A=1│───┐
│ └───────┘ ↓
┌───────┐ ┌───────┐
│ P0 A=0│ │ P4 A=1│
└───────┘ └───────┘
↑ ┌───────┐ │
└────│ P2 A=0│←──┘
└───────┘
↑ punteroLa intuición es exacta: una página con A=1 se ha usado desde la última vuelta del puntero, así que probablemente se siga usando. Una con A=0 no se ha tocado en toda una vuelta: es buena candidata.
Una variante mejor usa dos bits, A y D, que combinan reciente uso y coste de expulsión:
| A | D | Interpretación | Prioridad de expulsión |
|---|---|---|---|
| 0 | 0 | Ni usada ni modificada | 1ª: la mejor víctima, se descarta gratis |
| 0 | 1 | No usada pero modificada | 2ª: hay que escribirla, pero no la quieren |
| 1 | 0 | Usada, no modificada | 3ª: se descarta gratis pero se está usando |
| 1 | 1 | Usada y modificada | 4ª: la peor víctima |
Linux usa una variante refinada: dos listas LRU (activa e inactiva) por zona de memoria, con promoción entre ellas según los accesos, más el bit A para el envejecimiento. Es LRU aproximado, con el coste amortizado a casi cero.
Comparación final
| Algoritmo | Fallos | Frente al óptimo | Implementable | Anomalía de Belady |
|---|---|---|---|---|
| Óptimo | 9 | — | No | No |
| LRU | 12 | +33 % | Solo aproximado | No |
| Reloj (2ª oportunidad) | ~13 | +44 % | Sí, barato | No |
| FIFO | 15 | +67 % | Sí, trivial | Sí |
Y aquí está la conclusión práctica que se suele pasar por alto: la diferencia entre el mejor algoritmo posible y uno decente es un 33 %; la diferencia entre tener suficiente RAM y no tenerla es un factor de 81. Optimizar el algoritmo de reemplazo importa mucho menos que dimensionar bien la memoria.
Asignación de marcos, hiperpaginación y conjunto de trabajo
Con 180 procesos y 2 millones de marcos, ¿cuántos marcos le tocan a cada proceso?
Asignación equitativa: marcos / procesos. Simple e injusta: meteo-api con 116 MB de datos recibiría lo mismo que un sshd de 9 MB.
Asignación proporcional: repartir según el tamaño de cada proceso.
Y una distinción importante:
- Reemplazo local: un proceso que falla solo puede robar marcos a sí mismo. Su rendimiento es predecible pero no aprovecha la memoria ociosa de otros.
- Reemplazo global: puede robar a cualquiera. Mejor uso global, pero el rendimiento de un proceso depende del comportamiento de los demás. Linux usa reemplazo global.
Hiperpaginación (thrashing)
Aquí está el fenómeno más importante de esta sección. Si un proceso no tiene marcos suficientes para su conjunto de trabajo, entra en un ciclo destructivo:
flowchart TD
A["Proceso con pocos marcos"] --> B["Fallo de página"]
B --> C["Expulsa una página<br/>que necesitará enseguida"]
C --> D["Se bloquea esperando el disco"]
D --> E["La CPU queda ociosa"]
E --> F["El sistema cree que puede<br/>admitir más procesos"]
F --> G["Menos marcos por proceso"]
G --> B
El bucle se realimenta: cuanta menos CPU se usa, más procesos se admiten, y menos memoria queda para cada uno.
Los síntomas son inconfundibles y merece la pena memorizarlos:
| Métrica | Valor en hiperpaginación | Por qué |
|---|---|---|
| Uso de CPU | Muy bajo (5-15 %) | Todos los procesos esperan disco |
%iowait |
Muy alto (60-90 %) | Solo hay actividad de disco |
| Fallos mayores | Miles por segundo | Cada acceso falla |
Columna si/so de vmstat |
Cientos de MB/s | Swap constante en ambos sentidos |
| Carga media | Altísima | Muchos procesos en estado D |
| Sensación | El sistema "no responde" | Ni siquiera acepta un ssh |
La combinación CPU al 10 % con carga media de 40 es el diagnóstico. Si vieras esto en meteo-01:
$ vmstat 1 3 procs -----------memory---------- ---swap-- -----io---- --system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 24 4194300 22140 1024 81920 48932 51204 62104 51988 8421 21044 4 9 2 85 0 0 27 4194300 19008 1024 79872 52108 49872 64220 50104 9102 23811 3 11 1 85 0
Los datos hablan solos: 85 % de %wa (espera de E/S), 27 procesos bloqueados en la columna b, y si/so en torno a 50.000 KB/s en ambas direcciones. El sistema está trayendo y llevando las mismas páginas continuamente. No hay ajuste de configuración que salve esto: falta memoria.
El modelo del conjunto de trabajo
La solución conceptual la formuló Peter Denning en 1968. El conjunto de trabajo W(t, Δ) es el conjunto de páginas referenciadas por un proceso en las últimas Δ referencias.
Δ = 10.000 referencias
Referencias: ...2 6 1 5 7 7 7 5 1 6 2 3 4 1 2 3 4 4 4 3 4 4 4 1 3 2 3 4 4 4 4 3...
└──────── ventana Δ ────────┘
W = {1, 2, 5, 6, 7} W = {3, 4}La regla es simple y potente:
Y cuando eso ocurre, la única solución correcta es suspender procesos —reducir el grado de multiprogramación— para que los que queden tengan su conjunto de trabajo completo. Es contraintuitivo pero cierto: ejecutar menos procesos hace que el sistema avance más.
Linux no implementa el modelo de Denning literalmente, pero su presión de memoria y el OOM killer persiguen el mismo objetivo: cuando el sistema no puede sostener a todos, elimina a alguien en lugar de dejar que nadie avance.
Ficheros mapeados y memoria compartida con mmap()
mmap() es una de las llamadas al sistema más potentes de UNIX: hace que un fichero aparezca como memoria.
En lugar de open + read + copiar a un búfer, se mapea el fichero en el espacio de direcciones y se accede a él como si fuera un array.
/* leer_lecturas.c — compilar: gcc -Wall -o leer_lecturas leer_lecturas.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <stdint.h>
struct Lectura {
uint32_t estacion_id;
uint32_t timestamp;
float temperatura;
float humedad;
float presion;
uint32_t _reservado; /* completa los 24 bytes */
};
int main(void) {
const char *ruta = "/var/lib/meteora/lecturas/2026-08-31.dat";
int fd = open(ruta, O_RDONLY);
if (fd == -1) { perror("open"); return 1; }
struct stat st;
if (fstat(fd, &st) == -1) { perror("fstat"); return 1; }
size_t n = st.st_size / sizeof(struct Lectura);
printf("Fichero de %ld bytes = %zu lecturas\n", (long)st.st_size, n);
/* Mapear el fichero completo en memoria */
struct Lectura *lecturas = mmap(NULL, st.st_size,
PROT_READ, MAP_PRIVATE, fd, 0);
if (lecturas == MAP_FAILED) { perror("mmap"); return 1; }
close(fd); /* el mapeo sobrevive al cierre del descriptor */
/* Recorrerlo como si fuera un array normal */
double suma = 0.0;
float maxima = -100.0f;
for (size_t i = 0; i < n; i++) {
suma += lecturas[i].temperatura;
if (lecturas[i].temperatura > maxima)
maxima = lecturas[i].temperatura;
}
printf("Temperatura media: %.2f °C\n", suma / n);
printf("Temperatura máxima: %.2f °C\n", maxima);
munmap(lecturas, st.st_size);
return 0;
}$ ./leer_lecturas Fichero de 17280000 bytes = 720000 lecturas Temperatura media: 18.43 °C Temperatura máxima: 34.70 °C
Qué hace cada parte y por qué importa:
mmap(NULL, tamaño, PROT_READ, MAP_PRIVATE, fd, 0)pide al núcleo que asocie el fichero a una región del espacio de direcciones.NULLdeja que el núcleo elija la dirección;PROT_READla hace de solo lectura;MAP_PRIVATEsignifica que las escrituras (si las hubiera) serían copy-on-write y no llegarían al fichero.- No se lee nada del disco en este momento. El núcleo solo crea entradas de tabla de páginas con P=0. Los 17 MB llegan por demanda, cuando el bucle los toque: cada acceso a una página nueva provoca un fallo de página que la trae.
close(fd)no invalida el mapeo. El mapeo mantiene su propia referencia al fichero. Es un detalle que sorprende y que permite cerrar descriptores sin perder el acceso.lecturas[i].temperaturaes aritmética de punteros normal. El compilador genera un acceso a memoria; la MMU y el núcleo hacen el resto. No hay ninguna llamada al sistema en el bucle.
Ese último punto es la razón de fondo para usar mmap. Compara con la alternativa de read(), aplicando lo que calculamos en 01-06:
read() en bloques de 4 KB |
mmap() |
|
|---|---|---|
| Llamadas al sistema | 17.280.000 / 4.096 = 4.219 | 1 |
| Coste de syscalls a 1 µs | 4.219 µs = 4,2 ms | 1 µs |
| Copias de los datos | 2 (disco→caché→búfer de usuario) | 1 (disco→caché) |
| Memoria adicional | El búfer del proceso | Ninguna |
| Si dos procesos leen el mismo fichero | Dos copias en RAM | Comparten las mismas páginas |
| Acceso aleatorio | lseek + read |
Indexación directa |
La última fila es especialmente valiosa para Meteora: si agregador y meteo-api mapean ambos 2026-08-31.dat, las páginas de la caché del núcleo se comparten físicamente. Un solo juego de 17 MB en RAM sirve a los dos procesos.
Cuándo mmap no conviene: para lecturas secuenciales de un solo paso de ficheros muy grandes, read() con un búfer grande puede ser igual o mejor, porque mmap provoca un fallo de página por cada 4 KB (miles de excepciones), y porque un fichero mayor que la RAM mapeado entero puede provocar presión de memoria.
mmap para memoria compartida
Con MAP_SHARED en lugar de MAP_PRIVATE, las escrituras sí se propagan: al fichero y a todos los procesos que lo mapeen.
int fd = open("/dev/shm/meteora-cache", O_RDWR | O_CREAT, 0640);
ftruncate(fd, 8 * 1024 * 1024);
void *cache = mmap(NULL, 8 * 1024 * 1024,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);Esto es exactamente la región rw-s- que vimos en el pmap de meteo-api en 02-03: los cuatro trabajadores comparten una única caché de 8 MB. Los mecanismos de comunicación entre procesos y la sincronización que esto exige corresponden a Comunicación entre Procesos y Sincronización.
Copy-on-write revisitado
Ahora que conoces los bits de la tabla de páginas, el COW de fork() que vimos en 02-01 se puede explicar exactamente:
fork()copia las tablas de páginas del padre al hijo.- En ambas copias, todas las páginas escribibles se marcan con R/W = 0 (solo lectura), y el núcleo anota internamente que son COW.
- Ambos procesos apuntan a los mismos marcos físicos. Cero copias de datos.
- Cuando cualquiera de los dos escribe, la MMU detecta R/W=0 y genera un fallo de página de protección.
- El núcleo lo distingue de un error real (comprueba que la región es COW y no de solo lectura genuina), copia esa única página de 4 KB, la asigna en exclusiva al que escribió y le pone R/W=1.
- Si el contador de referencias del marco original baja a 1, el otro proceso también recupera R/W=1: ya no hay nada que proteger.
Los números para meteo-api con sus 148 MB:
Copia ingenua: 148 MB / 20 GB/s = 7,4 ms
COW: ~37.000 entradas de tabla de páginas ≈ 300 KB
0,3 MB / 20 GB/s + gestión ≈ 0,5 ms
Factor de mejora: ~15×
Si el hijo hace execve() inmediatamente: se copian ~0 páginasY ahora entiendes también por qué fork() puede parecer barato y luego costar: si el hijo escribe por toda la memoria heredada, acabarás pagando la copia página a página, con un fallo de página por cada 4 KB. Esa es la queja clásica contra fork() en procesos enormes, y la razón de que existan alternativas como posix_spawn() y vfork().
Swap, swappiness y el OOM killer
El área de intercambio (swap) es el espacio en disco donde se guardan las páginas anónimas expulsadas.
$ swapon --show
NAME TYPE SIZE USED PRIO
/dev/sda3 partition 4G 512M -2
$ free -h
total used free shared buff/cache available
Mem: 7,8Gi 3,1Gi 412Mi 528Mi 4,3Gi 3,9Gi
Swap: 4,0Gi 512Mi 3,5GiInterpretación de free -h, que es donde casi todo el mundo se equivoca:
| Columna | Qué es | Trampa habitual |
|---|---|---|
total |
RAM instalada | — |
used |
Usada por procesos | — |
free |
Completamente sin usar | 412 MB no significa que falte memoria |
buff/cache |
Caché de páginas y búferes | Se libera al instante si hace falta |
available |
Lo que un proceso nuevo puede obtener | Esta es la columna que importa |
Los 412 MB de free alarman a mucha gente sin motivo. Los 4,3 GB de buff/cache son páginas de ficheros que el núcleo mantiene por si vuelven a hacer falta, y son desechables al instante. La memoria realmente disponible son los 3,9 GB de available.
RAM libre no es RAM aprovechada. Un sistema con memoria libre está desperdiciando la oportunidad de cachear. Lo correcto es que
freesea bajo yavailablesea alto.
swappiness
Controla la agresividad con que el núcleo expulsa páginas anónimas frente a descartar caché de ficheros:
| Valor | Comportamiento | Adecuado para |
|---|---|---|
| 0 | Swap solo para evitar el OOM killer | Bases de datos con RAM suficiente |
| 1-10 | Muy reacio a hacer swap | Servidores sensibles a la latencia |
| 60 | Por defecto, equilibrado | Escritorio, uso general |
| 100 | Trata igual anónimas y de fichero | Cargas con mucha lectura de ficheros |
Para meteo-01 un valor de 10 sería razonable: es preferible descartar caché de páginas (recuperable con una lectura secuencial rápida) antes que mandar a swap la caché de respuestas de meteo-api (que provocaría fallos mayores en el camino crítico de las peticiones).
$ sudo sysctl -w vm.swappiness=10 $ echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.d/99-meteora.conf
Un aviso importante: swappiness=0 no desactiva el swap, y desactivar el swap por completo tampoco es buena idea. Sin swap, las páginas anónimas inactivas —que en cualquier sistema son muchas— se quedan ocupando RAM eternamente, y ante presión de memoria el núcleo pasa directamente al OOM killer sin tener alternativas intermedias.
El OOM killer
Cuando no hay memoria ni marcos que expulsar, el núcleo activa el Out Of Memory killer: elige un proceso y lo mata.
La elección se basa en una puntuación:
oom_score es proporcional a la memoria consumida, con ajustes: penaliza a los procesos grandes y de usuarios normales, y protege a los de root y al PID 1. oom_score_adj va de −1000 (nunca matarlo) a +1000 (matarlo primero) y es lo que puedes ajustar tú:
# Proteger el ingestor: perder lecturas es irreversible $ echo -900 | sudo tee /proc/1842/oom_score_adj # Sacrificar primero el backup $ echo 800 | sudo tee /proc/3901/oom_score_adj
El rastro que deja en el registro:
$ sudo dmesg -T | grep -A4 'Out of memory' [Sun Aug 31 04:12:33 2026] agregador invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0 [Sun Aug 31 04:12:33 2026] Mem-Info: [Sun Aug 31 04:12:33 2026] active_anon:1842103 inactive_anon:204118 isolated_anon:0 [Sun Aug 31 04:12:33 2026] Tasks state (memory values in pages): [Sun Aug 31 04:12:33 2026] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name [Sun Aug 31 04:12:33 2026] [ 1877] 998 1877 1204832 1180221 9539584 0 0 agregador [Sun Aug 31 04:12:33 2026] [ 1901] 998 1901 37080 33785 274432 0 0 meteo-api [Sun Aug 31 04:12:33 2026] Out of memory: Killed process 1877 (agregador) total-vm:4819328kB, anon-rss:4720884kB, file-rss:0kB, shmem-rss:0kB, UID:998 pgtables:9316kB oom_score_adj:0
Cómo leerlo línea a línea, que es lo que hay que saber hacer en una guardia:
agregador invoked oom-killer: quien provocó el OOM fue elagregadoral pedir memoria. No implica necesariamente que sea el culpable, aunque aquí lo es.- La tabla de tareas lista todos los procesos con su
rssen páginas. Elagregadortiene 1.180.221 páginas × 4 KB = 4,5 GB.meteo-apitiene 33.785 × 4 KB = 132 MB. Killed process 1877 (agregador)conanon-rss:4720884kB: se eligió al que más memoria anónima consumía, que es el criterio habitual.anon-rssfrente afile-rss: los 4,7 GB son anónimos, es decir, memoria dinámica sin respaldo en fichero. Matar el proceso los libera de inmediato. Si fuerafile-rss, matarlo apenas liberaría nada.
Esa cifra de 4,5 GB en un agregador que en condiciones normales usa 31 MB es el diagnóstico completo: procesar un día entero cargando todo en memoria en lugar de por bloques. La corrección es de diseño —mmap o lectura por bloques—, no de configuración.
Recuerda además lo que vimos en 02-01: cuando el OOM killer mata un proceso, este muere por SIGKILL (señal 9), lo que tu supervisor detectaría con WIFSIGNALED(estado) y WTERMSIG(estado) == 9.
Medición: VSZ frente a RSS, /proc/meminfo, free -h, vmstat
Cerramos con las herramientas y cómo interpretarlas sin equivocarse.
$ ps -eo pid,vsz,rss,comm -u meteora
PID VSZ RSS COMMAND
1842 408212 18204 ingestor
1877 412308 31456 agregador
1901 486300 148320 meteo-api| Métrica | Qué mide | Cuándo la usas | Trampa |
|---|---|---|---|
| VSZ | Espacio de direcciones reservado | Casi nunca | Incluye lo nunca tocado; reservar es gratis |
| RSS | Páginas realmente en RAM | Estimación rápida | Cuenta las compartidas en cada proceso |
| PSS | RSS con las compartidas divididas | Sumar consumo de varios procesos | Solo en smaps |
| USS | Memoria exclusiva del proceso | Cuánto se libera al matarlo | Solo en smaps |
El problema del RSS con un ejemplo concreto: si meteo-api tiene 4 trabajadores y cada uno reporta 148 MB de RSS, sumar da 592 MB, pero el consumo real puede ser 200 MB porque comparten código, libc y la caché de /dev/shm. PSS lo resuelve dividiendo cada página compartida entre quienes la usan:
94 MB reales frente a 148 MB de RSS. Para contabilizar memoria en un servidor con procesos que comparten mucho, PSS es la métrica correcta.
/proc/meminfo da la foto global:
$ grep -E 'MemTotal|MemAvailable|Cached|Dirty|Writeback|AnonPages|Mapped|Slab|SwapTotal|SwapFree' /proc/meminfo MemTotal: 8122448 kB MemAvailable: 4089216 kB Cached: 4198400 kB Dirty: 28160 kB Writeback: 0 kB AnonPages: 3021312 kB Mapped: 412160 kB Slab: 298432 kB SwapTotal: 4194300 kB SwapFree: 3670012 kB
Las líneas que informan de verdad:
MemAvailable: 3,9 GB. La única que responde a "¿cuánta memoria puedo usar?".AnonPages2,9 GB: memoria anónima de los procesos. Solo puede ir a swap.Cached4 GB: páginas de fichero. Desechables al instante.Dirty28 MB: modificadas pero aún no escritas a disco. Si esta cifra sube mucho, hay un cuello de botella de escritura. Su vaciado forzado es lo que hacefsync().Slab298 MB: estructuras del propio núcleo (inodos, dentries,task_struct). No es memoria de procesos.
Y vmstat para ver la dinámica:
$ vmstat 2 3 procs -----------memory---------- ---swap-- -----io---- --system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 0 524288 421904 1024 4198400 0 0 142 288 4211 8877 12 4 83 1 0 1 0 524288 419872 1024 4199424 0 0 88 412 4402 9104 14 5 80 1 0 3 1 524288 418112 1024 4200448 0 16 204 1128 5108 10944 16 6 76 2 0
Las columnas críticas para memoria:
si/so(swap in / swap out, en KB/s): la métrica más importante. Unswpdde 512 MB consi/soa cero es inofensivo: son páginas viejas expulsadas hace tiempo que nadie reclama. Lo grave essi/sosostenidos: ahí sí hay hiperpaginación.b: procesos bloqueados en E/S ininterrumpible (el estadoDde 02-01).wa: porcentaje de CPU esperando E/S.
La regla de diagnóstico que resume la lección: mira si/so, no swpd. Tener swap ocupado es normal; tener swap en movimiento constante significa que falta RAM.
Errores Comunes y Consejos
Alarmarse porque free es bajo. La columna correcta es available. Un sistema sano tiene poca memoria libre y mucha caché, porque la RAM ociosa es RAM desperdiciada.
Confundir swpd con estar paginando. Que haya 512 MB en swap no dice nada por sí solo: pueden llevar días ahí sin que nadie los reclame. Lo que indica un problema son si/so sostenidos en vmstat.
Desactivar el swap "para que el sistema no se ralentice". Sin swap, el núcleo pierde su herramienta intermedia y ante presión de memoria pasa directamente al OOM killer. La configuración razonable es tener swap y bajar swappiness, no eliminarlo.
Sumar los RSS de varios procesos. Da un total inflado porque las páginas compartidas se cuentan varias veces. Para sumar, usa PSS.
Creer que el OOM killer mata al culpable. Mata al que tiene la mayor puntuación, que suele ser el más grande. Si el agregador provoca la escasez y meteo-api es el que más memoria tiene, muere meteo-api. Protege lo crítico con oom_score_adj negativo.
Activar THP en todo. Las páginas transparentes de 2 MB ayudan a cargas con conjuntos de trabajo grandes, pero khugepaged compactando memoria puede introducir pausas de decenas de milisegundos. Las bases de datos suelen recomendar madvise o never justo por eso.
Interpretar un SIGSEGV como un bug del sistema. Es la MMU haciendo su trabajo: el proceso accedió a una dirección sin traducción válida o sin los permisos adecuados. Compara la dirección con /proc/<pid>/maps para saber si fue un puntero corrupto (cae en un hueco) o una escritura en zona de solo lectura.
Consejo de diagnóstico: ante una sospecha de problema de memoria, este es el orden que funciona: free -h (mirar available), vmstat 1 (mirar si/so y wa), ps -eo pid,rss,maj_flt --sort=-rss | head (quién consume y quién falla), pmap -x <pid> (qué región crece) y dmesg -T | grep -i oom (si ya ha muerto alguien). Cinco órdenes y tienes el cuadro completo.
Ejercicios
Ejercicio 1: traducción de direcciones y tamaño de tablas
Un sistema tiene direcciones lógicas de 32 bits y páginas de 4 KB.
- ¿Cuántos bits ocupan el número de página y el desplazamiento? ¿Cuántas páginas hay como máximo?
- Con esta tabla de páginas, traduce las direcciones lógicas
0x00003ABC,0x00001234y0x00006000:
| Página | Marco | Presente |
|---|---|---|
| 0 | 0x0A | Sí |
| 1 | 0x1F | Sí |
| 2 | 0x03 | Sí |
| 3 | 0x2C | Sí |
| 4 | — | No |
- Calcula el tamaño de una tabla plana con entradas de 4 bytes. Compáralo con el caso de 64 bits del apartado 4 de la lección.
- Con una TLB del 96 % de aciertos, acceso a memoria de 80 ns, TLB de 2 ns y 2 niveles de tabla, calcula el tiempo efectivo de acceso.
Ejercicio 2: comparar algoritmos de reemplazo
El agregador genera esta cadena de referencias a páginas:
- Calcula los fallos de página con 3 marcos para: óptimo, FIFO y LRU. Muestra el estado de los marcos en cada paso.
- Repite con 4 marcos.
- ¿Qué algoritmo presenta la anomalía de Belady? Demuéstralo con tus números.
- Si cada fallo mayor cuesta 8 ms, calcula el tiempo total perdido en cada caso con 3 marcos.
Ejercicio 3: diagnosticar un servidor con problemas de memoria
meteo-01 responde con lentitud extrema. Recoges estos datos:
$ free -h
total used free shared buff/cache available
Mem: 7,8Gi 7,4Gi 102Mi 12Mi 298Mi 118Mi
Swap: 4,0Gi 3,8Gi 204Mi
$ vmstat 2 3
procs -----------memory---------- ---swap-- -----io---- --system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 31 3985408 104448 512 305152 42104 39882 51204 40118 9821 24102 3 8 1 88 0
1 29 3985408 102112 512 303104 44210 41004 53108 41220 10104 25811 2 9 1 88 0
0 33 3985408 101888 512 301056 43108 40112 52004 40988 9902 24998 3 8 1 88 0
$ ps -eo pid,rss,maj_flt,comm --sort=-rss | head -5
PID RSS MAJFL COMMAND
1877 4720884 892104 agregador
1901 148320 41022 meteo-api
1842 18204 8104 ingestor- Diagnostica qué le pasa al sistema. Nombra el fenómeno y justifícalo con al menos cuatro datos.
- ¿Por qué el uso de CPU es del 3 % si el sistema va lentísimo?
- Identifica la causa raíz. ¿Cuánta memoria debería usar el
agregador, sabiendo que procesa un día de lecturas? - Propón tres soluciones: una inmediata, una de configuración y una de diseño. Indica cuál es la correcta.
- Escribe el fragmento de código que resolvería el problema de raíz.
Soluciones
Solución 1
1. Descomposición de la dirección.
Página de 4 KB = 2^12 bytes → desplazamiento = 12 bits Número de página = 32 − 12 = 20 bits Páginas máximas = 2^20 = 1.048.576
2. Traducciones.
0x00003ABC:
En binario: 0000 0000 0000 0000 0011 | 1010 1011 1100
p = 0x00003 = 3 | d = 0xABC = 2748
Aritméticamente: 0x3ABC = 15036; 15036 / 4096 = 3; 15036 % 4096 = 2748
Tabla: página 3 → marco 0x2C = 44, presente
física = 44 × 4096 + 2748 = 180.224 + 2.748 = 182.972 = 0x0002CABCAtajo hexadecimal: los tres dígitos de la derecha (ABC) se conservan y los de la izquierda pasan de 00003 a 0002C.
0x00001234:
p = 0x00001 = 1, d = 0x234 = 564 Tabla: página 1 → marco 0x1F = 31 física = 31 × 4096 + 564 = 126.976 + 564 = 127.540 = 0x0001F234
0x00006000:
p = 0x00006 = 6, d = 0 Tabla: la página 6 no existe (solo hay entradas 0-4) → FALLO DE PÁGINA por dirección inválida → El núcleo comprueba /proc/pid/maps: no pertenece a ninguna región → SIGSEGV: violación de segmento
Es importante distinguir este caso del de la página 4, que sí existe pero tiene Presente = No: allí el fallo sería recuperable (el núcleo la traería de swap o del fichero y reintentaría la instrucción), mientras que aquí es un error genuino del programa.
3. Tamaño de la tabla plana en 32 bits.
Comparación con 64 bits:
| 32 bits | 64 bits (48 usados) | |
|---|---|---|
| Bits de página | 20 | 36 |
| Entradas | 1.048.576 | 68.719.476.736 |
| Bytes por entrada | 4 | 8 |
| Tabla plana | 4 MB | 512 GB |
| Con 180 procesos | 720 MB | 92 TB |
Y aquí está la observación clave del ejercicio: 4 MB por proceso ya es demasiado. Con 180 procesos son 720 MB solo de tablas, casi el 9 % de los 8 GB de meteo-01, y la inmensa mayoría de esas entradas estarían vacías. Por eso incluso los sistemas de 32 bits usaban tablas multinivel (x86 de 32 bits tenía dos niveles). La tabla plana nunca fue viable; en 64 bits pasa de inviable a directamente absurda.
4. Tiempo efectivo de acceso.
Acierto (96 %): 2 ns (TLB) + 80 ns (dato) = 82 ns
Fallo (4 %): 2 ns + 2 × 80 ns (dos niveles) + 80 ns (dato) = 242 ns
TEA = 0,96 × 82 + 0,04 × 242
= 78,72 + 9,68
= 88,4 nsDegradación frente al acceso puro (80 ns): 88,4 / 80 = 1,105, un 10,5 %.
Vale la pena observar cuánto pesa ese 4 % de fallos: aporta 9,68 ns de los 88,4 totales, un 11 % del tiempo, siendo solo el 4 % de los accesos. Es la aritmética típica de las cachés, y explica por qué mejorar del 96 % al 99 % de aciertos merece la pena:
Solución 2
1. Con 3 marcos.
Óptimo (expulsa la que tardará más en reaparecer):
| Ref | 1 | 2 | 3 | 4 | 1 | 2 | 5 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| M1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | 3 |
| M2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 4 | 4 | |
| M3 | 3 | 4 | 4 | 4 | 5 | 5 | 5 | 5 | 5 | 5 | ||
| Fallo | ● | ● | ● | ● | ● | ● | ● |
Las decisiones de expulsión: en la posición 4 sale 3 (reaparece en la 10, más tarde que 1 y 2); en la 7 sale 4 (reaparece en la 11); en la 10 sale 1 (no vuelve nunca); en la 11 sale 2 (tampoco vuelve).
7 fallos.
FIFO:
| Ref | 1 | 2 | 3 | 4 | 1 | 2 | 5 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| M1 | 1 | 1 | 1 | 4 | 4 | 4 | 5 | 5 | 5 | 5 | 5 | 5 |
| M2 | 2 | 2 | 2 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | 3 | |
| M3 | 3 | 3 | 3 | 2 | 2 | 2 | 2 | 2 | 4 | 4 | ||
| Fallo | ● | ● | ● | ● | ● | ● | ● | ● | ● |
9 fallos.
LRU:
| Ref | 1 | 2 | 3 | 4 | 1 | 2 | 5 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| M1 | 1 | 1 | 1 | 4 | 4 | 4 | 5 | 5 | 5 | 3 | 3 | 3 |
| M2 | 2 | 2 | 2 | 1 | 1 | 1 | 1 | 1 | 1 | 4 | 4 | |
| M3 | 3 | 3 | 3 | 2 | 2 | 2 | 2 | 2 | 2 | 5 | ||
| Fallo | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● |
10 fallos.
2. Con 4 marcos.
Óptimo:
| Ref | 1 | 2 | 3 | 4 | 1 | 2 | 5 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| M1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 4 | 4 |
| M2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | |
| M3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | ||
| M4 | 4 | 4 | 4 | 5 | 5 | 5 | 5 | 5 | 5 | |||
| Fallo | ● | ● | ● | ● | ● | ● |
Con 4 marcos caben 1, 2, 3 y 4 desde el principio. Al llegar el 5 se expulsa el 4, porque reaparece en la posición 11, más tarde que 1, 2 y 3. En la posición 10 el 3 ya está residente, así que no hay fallo. En la 11 hay que traer el 4 y se expulsa el 1, que no vuelve a usarse.
6 fallos.
FIFO:
| Ref | 1 | 2 | 3 | 4 | 1 | 2 | 5 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| M1 | 1 | 1 | 1 | 1 | 1 | 1 | 5 | 5 | 5 | 5 | 4 | 4 |
| M2 | 2 | 2 | 2 | 2 | 2 | 2 | 1 | 1 | 1 | 1 | 5 | |
| M3 | 3 | 3 | 3 | 3 | 3 | 3 | 2 | 2 | 2 | 2 | ||
| M4 | 4 | 4 | 4 | 4 | 4 | 4 | 3 | 3 | 3 | |||
| Fallo | ● | ● | ● | ● | ● | ● | ● | ● | ● | ● |
10 fallos.
LRU:
| Ref | 1 | 2 | 3 | 4 | 1 | 2 | 5 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| M1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 5 |
| M2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | |
| M3 | 3 | 3 | 3 | 3 | 5 | 5 | 5 | 5 | 4 | 4 | ||
| M4 | 4 | 4 | 4 | 4 | 4 | 4 | 3 | 3 | 3 | |||
| Fallo | ● | ● | ● | ● | ● | ● | ● | ● |
Traza de las expulsiones: al llegar el 5 (posición 7), el orden de uso es 3, 4, 1, 2, así que sale el 3. En la posición 10 hace falta el 3 y el menos usado recientemente es el 4. En la 11 hace falta el 4 y sale el 5. En la 12 hace falta el 5 y sale el 1. Fíjate en el patrón: las tres últimas referencias fallan porque LRU acaba de expulsar justo lo que se pide a continuación.
8 fallos.
3. La anomalía de Belady.
| Algoritmo | 3 marcos | 4 marcos | ¿Mejora con más memoria? |
|---|---|---|---|
| Óptimo | 7 | 6 | Sí (−1) |
| FIFO | 9 | 10 | NO: empeora (+1) |
| LRU | 10 | 8 | Sí (−2) |
FIFO presenta la anomalía: pasar de 3 a 4 marcos aumenta los fallos de 9 a 10.
La explicación estructural: FIFO no cumple la propiedad de pila. Formalmente, un algoritmo la cumple si el conjunto de páginas residentes con n marcos es siempre un subconjunto del conjunto con n+1 marcos. LRU y OPT la cumplen porque su decisión depende del patrón de referencias, que no cambia al añadir marcos. FIFO decide por orden de llegada, y ese orden se altera completamente al cambiar el número de marcos: con 4 marcos, las páginas 1 y 2 sobreviven más tiempo y acaban siendo expulsadas justo antes de volver a usarse.
Es un resultado importante porque rompe una intuición que parecía segura, y es la razón práctica por la que ningún sistema real usa FIFO puro: no puedes prometer que ampliar la RAM mejorará el rendimiento.
4. Tiempo perdido con 3 marcos.
| Algoritmo | Fallos | Tiempo perdido | Frente al óptimo |
|---|---|---|---|
| Óptimo | 7 | 7 × 8 ms = 56 ms | — |
| FIFO | 9 | 9 × 8 ms = 72 ms | +28,6 % |
| LRU | 10 | 10 × 8 ms = 80 ms | +42,9 % |
Una observación honesta sobre estos números: aquí LRU sale peor que FIFO, lo que contradice la intuición general. Es un artefacto de esta cadena concreta, diseñada precisamente para exhibir la anomalía de Belady. Con cadenas reales, que presentan localidad temporal fuerte, LRU supera claramente a FIFO. Es un buen recordatorio de que una cadena de doce referencias no demuestra nada sobre el comportamiento general de un algoritmo; para eso hacen falta trazas reales de millones de referencias.
Lo que sí demuestra la comparación es el orden de magnitud del problema: 12 accesos a memoria que deberían costar 1,2 microsegundos han costado entre 56 y 80 milisegundos. Un factor de 50.000. Cuando faltan marcos, el algoritmo de reemplazo es lo de menos.
Solución 3
1. Diagnóstico: hiperpaginación (thrashing).
Los datos que lo confirman, uno a uno:
| Evidencia | Valor | Qué significa |
|---|---|---|
available |
118 Mi de 7,8 Gi | Memoria prácticamente agotada |
| Swap usado | 3,8 Gi de 4,0 Gi | El swap también está lleno |
si/so |
~42.000 / ~40.000 KB/s | 40 MB/s en ambas direcciones simultáneamente |
wa |
88 % | La CPU no hace nada más que esperar disco |
b |
29-33 procesos | Casi todo el sistema en estado D |
MAJFL del agregador |
892.104 | Casi un millón de fallos mayores |
El dato definitivo es si y so altos a la vez. Si solo hubiera so, el sistema estaría liberando memoria de forma ordenada. Que entre y salga a la vez a 40 MB/s significa que las mismas páginas se expulsan y se vuelven a traer sin parar: el conjunto de trabajo no cabe en RAM y cada página expulsada se necesita inmediatamente después. Es la definición exacta del bucle de realimentación del diagrama de la lección.
2. Por qué la CPU está al 3 %.
Porque no hay nada que ejecutar. Casi todos los procesos están en estado D, bloqueados esperando que el disco traiga una página. La columna r (ejecutables) marca 0 o 1, mientras que b (bloqueados) marca 31.
Este es el patrón más engañoso de todos: CPU casi ociosa con el sistema completamente parado. Quien mire solo el uso de CPU concluirá que el servidor está bien y buscará el problema en otro sitio. La combinación que hay que reconocer al instante es us+sy bajos + wa altísimo + b alto.
Un cálculo que lo dimensiona: 892.104 fallos mayores del agregador, a unos 100 µs cada uno con SSD, son 89 segundos de espera pura. Con disco mecánico a 8 ms serían ~2 horas de disco.
3. Causa raíz.
El agregador tiene 4.720.884 KB = 4,5 GB de RSS en una máquina de 7,8 GB. Es él solo el 60 % de la RAM total. Ni meteo-api (148 MB) ni ingestor (18 MB) son relevantes en comparación.
Cuánto debería usar:
Un día de lecturas: 17 MB (dato del glosario del curso) Estructuras de agregación: Estaciones × horas × 5 campos ≈ 50 × 24 × 5 × 8 bytes = 48 KB Búferes de trabajo, libc, código: ~15 MB Consumo razonable: 30-50 MB Consumo real: 4.500 MB Factor de exceso: ~100×
Y con 24 bytes por lectura, esos 4,5 GB equivalen a unos 196 millones de lecturas: más de 270 días de datos. El diagnóstico es inmediato: el agregador está cargando en memoria todo el histórico en lugar del día que necesita procesar, muy probablemente por un readdir sobre /var/lib/meteora/lecturas/ sin filtro de fecha, acumulándolo todo en una estructura en memoria.
4. Tres soluciones.
Inmediata (recuperar el servidor ahora):
Libera 4,5 GB al instante. El sistema deja de paginar en segundos. Es una tirita, no una cura: volverá a ocurrir en la próxima ejecución.
De configuración (contener el daño):
# Limitar la memoria del servicio con systemd $ sudo systemctl edit meteora-agregador [Service] MemoryMax=512M MemoryHigh=384M # Proteger a los procesos críticos del OOM killer $ echo -900 | sudo tee /proc/1842/oom_score_adj # ingestor $ echo -500 | sudo tee /proc/1901/oom_score_adj # meteo-api # Reducir la tendencia a hacer swap $ sudo sysctl -w vm.swappiness=10
Con MemoryMax=512M (un límite de cgroup, tema de 06-02), el agregador que intente pasar de 512 MB muere él solo sin arrastrar al resto del sistema. Esto convierte una caída global en un fallo aislado, que es exactamente lo que se quiere. No arregla el bug, pero lo contiene.
De diseño (la correcta):
Procesar por flujo, no cargando todo. Dos variantes válidas: leer por bloques con read(), o mapear el fichero del día con mmap() y dejar que la paginación por demanda gestione la memoria.
5. Código que lo resuelve.
Versión con mmap(), que aplica directamente lo visto en la lección:
/* agregar_dia.c — procesa UN día sin cargarlo entero en memoria propia */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <stdint.h>
struct Lectura {
uint32_t estacion_id;
uint32_t timestamp;
float temperatura;
float humedad;
float presion;
uint32_t _reservado;
};
#define MAX_ESTACIONES 64
#define HORAS_DIA 24
struct Acumulador {
double suma_temp;
double suma_hum;
double suma_pres;
uint32_t n;
};
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "uso: %s AAAA-MM-DD\n", argv[0]);
return 1;
}
char ruta[256];
snprintf(ruta, sizeof ruta,
"/var/lib/meteora/lecturas/%s.dat", argv[1]);
int fd = open(ruta, O_RDONLY);
if (fd == -1) { perror("open"); return 1; }
struct stat st;
if (fstat(fd, &st) == -1) { perror("fstat"); close(fd); return 1; }
struct Lectura *lec = mmap(NULL, st.st_size,
PROT_READ, MAP_PRIVATE, fd, 0);
if (lec == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
close(fd);
/* Aviso al núcleo: lo recorreremos en orden y no lo releeremos */
madvise(lec, st.st_size, MADV_SEQUENTIAL);
/* Acumuladores: 64 × 24 × 32 bytes = 48 KB, tamaño FIJO */
static struct Acumulador acc[MAX_ESTACIONES][HORAS_DIA];
memset(acc, 0, sizeof acc);
size_t n = st.st_size / sizeof(struct Lectura);
for (size_t i = 0; i < n; i++) {
uint32_t est = lec[i].estacion_id % MAX_ESTACIONES;
uint32_t hora = (lec[i].timestamp / 3600) % HORAS_DIA;
struct Acumulador *a = &acc[est][hora];
a->suma_temp += lec[i].temperatura;
a->suma_hum += lec[i].humedad;
a->suma_pres += lec[i].presion;
a->n++;
}
munmap(lec, st.st_size);
for (int e = 0; e < MAX_ESTACIONES; e++)
for (int h = 0; h < HORAS_DIA; h++)
if (acc[e][h].n > 0)
printf("%s %02d:00 est=%d n=%u T=%.2f H=%.1f P=%.1f\n",
argv[1], h, e, acc[e][h].n,
acc[e][h].suma_temp / acc[e][h].n,
acc[e][h].suma_hum / acc[e][h].n,
acc[e][h].suma_pres / acc[e][h].n);
return 0;
}Por qué esto resuelve el problema de raíz:
- Un solo día por ejecución. La ruta se construye con la fecha recibida: es imposible cargar el histórico completo por accidente.
- Los acumuladores tienen tamaño fijo: 64 × 24 × 32 bytes = 48 KB, independiente del volumen de datos. Esta es la clave del arreglo: la memoria del proceso ya no crece con la entrada.
mmapno consume RSS propio. Las páginas mapeadas pertenecen a la caché de páginas del núcleo, que es desechable bajo presión de memoria (son limpias y respaldadas por fichero, como vimos en 02-03). Si falta RAM, el núcleo las descarta sin escribir nada y las relee después. Frente a los 4,5 GB anónimos de la versión anterior —que solo podían ir a swap—, esto cambia por completo el comportamiento del sistema bajo presión.madvise(MADV_SEQUENTIAL)le dice al núcleo que el acceso será secuencial. El núcleo activa lectura anticipada agresiva y descarta antes las páginas ya recorridas. Es una optimización pequeña de escribir y muy efectiva en recorridos completos.
Consumo resultante:
RSS del proceso: ~15 MB (código + libc + acumuladores) Caché de páginas: hasta 17 MB, liberables al instante Total efectivo: ~32 MB frente a 4.500 MB
Un factor de 140 de reducción, y ninguna página anónima que pueda arrastrar al sistema a la hiperpaginación.
Conclusión
La paginación divide el espacio lógico en páginas y la memoria física en marcos del mismo tamaño, y traduce cada dirección separando número de página y desplazamiento —que nunca se traduce—. Cada entrada de la tabla lleva el marco y unos bits de control que lo gobiernan todo: P habilita la memoria virtual, R/W y NX implementan W^X, y A y D, que escribe el hardware, son la única pista que el núcleo tiene para decidir a quién expulsar y a qué coste.
Una tabla plana en 64 bits ocuparía 512 GB por proceso, así que las tablas son multinivel y dispersas: cuatro niveles de 9 bits, cada tabla ocupando exactamente una página, y 60 KB reales en lugar de 512 GB. El precio son cuatro accesos a memoria por traducción, y por eso existe la TLB: con un 99 % de aciertos pierdes un 4 % de rendimiento, con un 70 % pierdes más del doble. Las páginas grandes reducen 4.150 entradas de TLB a 9 para los 17 MB del agregador, a costa de fragmentación interna y de una granularidad de expulsión grosera.
La paginación por demanda hace que nada se cargue hasta que se toca, lo que explica libc con solo el 44 % de su código en RAM. Un fallo menor cuesta microsegundos y es normal; un fallo mayor cuesta milisegundos, y con uno por cada mil accesos el sistema va 81 veces más lento. Los algoritmos de reemplazo —óptimo como cota, FIFO con su anomalía de Belady, LRU inviable en exacto y el reloj como aproximación barata usando el bit A— se mueven en un margen del 33 %, mucho menos de lo que importa tener RAM suficiente. Cuando el conjunto de trabajo no cabe, aparece la hiperpaginación: CPU al 3 %, wa al 88 % y el sistema parado.
Y todo esto se toca con las manos: mmap() convierte 4.219 llamadas al sistema en una y permite que dos procesos compartan físicamente el mismo 2026-08-31.dat; el copy-on-write de fork() es simplemente R/W=0 más un fallo de protección; swappiness decide qué se sacrifica antes; y el OOM killer deja en dmesg un rastro que se lee proceso a proceso. La regla que resume la parte operativa: mira available y no free, mira si/so y no swpd, y suma PSS y no RSS.
Con esto cerramos la memoria. Nos queda el otro gran recurso que el sistema operativo administra y que ha aparecido en cada fallo mayor de esta lección: el almacenamiento. Cuánto cuesta realmente ese acceso a disco que hemos estado contando en milisegundos, por qué un SSD cambia todas las reglas, cómo se ordenan las peticiones para minimizar el movimiento del cabezal y qué configuración de RAID merece /var/lib/meteora. Es lo que veremos en Gestión de Almacenamiento.
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
