El swap y swappiness: expulsar lo que no tiene hogar
La memoria de archivo tiene a dónde volver; la anónima no, así que el kernel le inventa un hogar: el swap. El viaje de una página anónima al disco con add_to_swap y try_to_unmap, la fórmula de swappiness en get_scan_count, el PTE de swap que la trae de vuelta y el thrashing como castigo del sobrecompromiso.
Una página de archivo siempre puede irse: tiene una copia en disco esperándola. Una página anónima —tu malloc, tu pila, tu montón— no tiene ningún sitio a donde volver. Para poder expulsarla, el kernel ha de fabricarle un hogar: el swap. Ese hogar prestado es un arma de doble filo. Bien dosificado por swappiness, libera RAM para lo que de verdad importa; mal dosificado, hunde al sistema en el thrashing, donde cada acceso paga la latencia del disco y el trabajo útil se detiene.
- Trazar el viaje de una página anónima al disco:
add_to_swap,try_to_unmap, escritura. - Entender
swappinessy la fórmula de balance deget_scan_count. - Ver cómo el PTE de swap guarda la referencia y cómo la falta de página la restaura.
- Reconocer el thrashing, medirlo con la PSI y conocer zswap, zram y los límites de cgroup.
El viaje de una página anónima al disco
Cuando el reclaim decide expulsar una página anónima, dentro de shrink_folio_list (nivel 25.1) se desata una secuencia precisa. Primero hay que reservarle sitio en el área de swap; luego romper todos los mapeos que la apuntan, sustituyéndolos por una marca que recuerde dónde fue a parar; por último, escribirla:
/* mm/vmscan.c — dentro de shrink_folio_list, rama anónima */
if (folio_test_anon(folio) && !folio_test_swapcache(folio)) {
if (!folio_alloc_swap(folio, ...)) /* reservar slot de swap */
goto activate_locked; /* sin swap: devolver a la activa */
}
/* sustituir cada PTE que mapea el folio por una entrada de swap */
try_to_unmap(folio, ttu_flags);
/* escribir el folio a su slot de swap */
pageout(folio, mapping, &plug); /* -> swap_writepage */
El paso decisivo es try_to_unmap: recorre el rmap inverso —de página a cada PTE que la mapea, quizá en varios procesos que comparten la memoria— y reemplaza cada entrada de tabla por una entrada de swap no presente. La página queda huérfana de mapeos y, una vez escrita, se libera. Fíjate en el fallback: si no hay swap donde reservar, la anónima vuelve a la lista activa. Sin swap, esa memoria es sencillamente inexpulsable, y toda la presión recae en el cache de archivo.
flowchart LR A[Pagina anonima en la LRU inactiva] --> B[folio_alloc_swap reserva slot] B --> C[try_to_unmap cambia el PTE por entrada de swap] C --> D[swap_writepage escribe al disco] D --> E[Pagina liberada] E -. mas tarde falta de pagina .-> F[do_swap_page la trae de vuelta] F --> G[cuenta como pswpin y como refault]
swappiness: anónimas frente a archivo
Bajo presión, el reclaim ha de repartir el escaneo entre las anónimas (caras de expulsar: una escritura a swap) y las de archivo (baratas si están limpias). Ese reparto lo gobierna vm.swappiness, un número de 0 a 200 —MAX_SWAPPINESS— con valor por defecto 60. Más alto, más dispuesto está el kernel a expulsar anónimas al swap; más bajo, más prefiere descartar cache de archivo antes de tocar el swap. La política se materializa en get_scan_count:
/* mm/vmscan.c — get_scan_count, el reparto de presión */
total_cost = sc->anon_cost + sc->file_cost;
anon_cost = total_cost + sc->anon_cost;
file_cost = total_cost + sc->file_cost;
total_cost = anon_cost + file_cost;
/* swappiness sube el peso de reclamar anónimas */
ap = swappiness * (total_cost + 1);
ap /= anon_cost + 1;
/* (200 - swappiness) sube el peso de reclamar archivo */
fp = (MAX_SWAPPINESS - swappiness) * (total_cost + 1);
fp /= file_cost + 1;
Lo elegante es que swappiness no actúa solo: se combina con anon_cost y file_cost, medidas de coste que el kernel deriva de las rotaciones y los refaults reales (nivel 25.2). El reparto es proporcional a swappiness e inversamente proporcional al coste observado de cada lista. Si el cache de archivo está refaultando sin parar, su coste sube y el kernel deja de castigarlo aunque swappiness sea bajo. Es una política que se autocorrige con la evidencia, no una constante ciega.
Los extremos importan: swappiness = 0 hace que el kernel evite el swap hasta el último momento, recurriendo a él solo para no invocar al OOM killer; útil en bases de datos que prefieren gestionar su propia memoria. Valores por encima de 100 —posibles desde que el rango llega a 200— permiten reclamar anónimas más agresivamente que archivo, útil cuando el swap es rápido (NVMe, zram) y el cache de archivo es oro.
El PTE de swap y el regreso
Cuando un proceso vuelve a tocar una dirección cuya página fue swapeada, la MMU encuentra un PTE no presente y dispara una falta de página. El manejador reconoce que no es un mapeo inválido sino una entrada de swap, y trae la página de vuelta:
/* mm/memory.c — falta de página sobre una dirección swapeada */
entry = pte_to_swp_entry(vmf->orig_pte); /* el PTE guardaba el slot */
folio = swap_cache_get_folio(entry, ...);
if (!folio) {
/* leer del disco: cuenta como pswpin y como un refault */
folio = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vmf);
}
El área de swap la describe un struct swap_info_struct por dispositivo, con sus slots agrupados en clusters para dar localidad a las escrituras. La lectura de vuelta puede arrastrar páginas vecinas por readahead, apostando —como en el nivel 25.2— a que la localidad espacial predice el uso próximo. Cada vuelta cuenta como pswpin en /proc/vmstat; cada expulsión, como pswpout.
Entre la reserva del slot y la escritura al disco, el folio vive en la swap cache, un espacio de direcciones especial indexado por entrada de swap. Su papel es coordinar concurrencia: si dos hilos faltan a la vez sobre la misma página swapeada, uno lee de disco y el otro encuentra el folio ya en la cache en vez de duplicar la E/S. También sostiene el estado mientras la escritura está en vuelo, de modo que una página aún no confirmada en disco no se pierda ni se lea a medias.
Thrashing: el castigo del sobrecompromiso
Si el conjunto de trabajo supera la RAM, el sistema entra en thrashing: expulsa una página que necesitará enseguida, la vuelve a traer, expulsa otra para hacerle sitio, y así en un bucle donde casi todo el tiempo se gasta paginando y casi nada progresa. La CPU parece ociosa mientras el disco arde y la latencia se dispara. El kernel detecta esta patología por los refaults —páginas que vuelven demasiado pronto— y la refleja en la línea full de la PSI (nivel 25.3):
cat /proc/pressure/memory
# full avg10=41.7 ... <- 42% del tiempo NADIE progresó: thrashing severo
# límites de swap por cgroup v2: acotar la presión por servicio
cat /sys/fs/cgroup/carga.slice/memory.swap.max # tope duro de swap
cat /sys/fs/cgroup/carga.slice/memory.swap.high # estrangula antes del tope
cat /sys/fs/cgroup/carga.slice/memory.swap.events # max, high, fail
La respuesta moderna no es solo más disco, sino swap más rápido. zswap intercepta las páginas camino del swap y las comprime en un pool dentro de la propia RAM, convirtiendo un viaje al disco en una descompresión; solo cuando el pool se llena escribe al dispositivo real. zram va más lejos y crea un dispositivo de swap comprimido enteramente en RAM, sin disco. Ambos cambian latencia de milisegundos por ciclos de CPU, un intercambio casi siempre ganador. En cgroup v2, memory.swap.max y memory.swap.high acotan cuánto swap puede consumir cada servicio, impidiendo que uno arrastre a los demás al thrashing.
Swap en disco
El slot vive en una partición o archivo. Barato y enorme, pero cada falta paga latencia de almacenamiento: el nivel más lento de la jerarquía.
zswap
Comprime las páginas camino del swap en un pool en RAM y solo desborda al disco cuando el pool se llena. Un nivel intermedio que cambia E/S por CPU.
zram
Un dispositivo de swap comprimido enteramente en RAM, sin disco detrás. Ideal cuando no hay swap real o se busca la latencia mínima posible.
Un error común es creer que añadir mucho swap protege del OOM killer. Lo que hace es correr el umbral: el sistema aguanta más carga a cambio de degradarse en thrashing mucho antes de morir. A menudo un OOM rápido y limpio es preferible a media hora de máquina inutilizable paginando sin avanzar. Por eso systemd-oomd y los gestores de la nube usan la PSI para matar carga mientras el thrashing empieza, en vez de esperar a que el kernel agote hasta el swap.
La intuición ingenua —“swap es memoria de más, prestada del disco”— es justo la que lleva a configurarlo mal. La visión correcta, la que gobierna el diseño de memoria de 2026, es que toda la memoria es una jerarquía de niveles con distinta latencia y coste, y el swap es simplemente el escalón más lento y barato de esa pila. Registros, cachés L1 a L3, RAM, zram comprimido, NVMe, disco: un mismo dato vive en el nivel que su temperatura justifica, y el reclaim no hace otra cosa que demover datos fríos a niveles más lentos para dejar sitio caliente en los rápidos. Esta óptica reencuadra todo. swappiness no es “cuánto swap usar”, sino la pendiente con que empujas datos fríos hacia abajo en la jerarquía. zswap no es un truco, es un nivel intermedio nuevo entre RAM y disco. Y la memoria por CXL —RAM de otro nodo alcanzable por el bus, que el kernel trata ya como un tier de reclaim propio— es solo un escalón más que encaja en el mismo modelo sin cambiar una línea de la teoría. El thrashing, entonces, tiene una definición precisa y aterradora: es lo que ocurre cuando el conjunto de trabajo caliente no cabe en el nivel rápido y el sistema paga latencia de nivel lento en cada acceso. No es un fallo del swap; es la jerarquía cobrándote por haberle mentido sobre cuánto cabía arriba.
- Enumera los tres pasos que expulsan una página anónima y di qué guarda el PTE al final.
- Con la fórmula de
get_scan_count, razona qué reparto produceswappiness0, 60 y 200. - Explica por qué
anon_costyfile_costhacen queswappinessno sea una constante ciega. - Configura
zramcomo swap, midepswpin/pswpouten/proc/vmstaty compáralo con swap en disco. - Provoca thrashing con
stress-ng --vmexcediendo la RAM y observa la líneafullde/proc/pressure/memory.