wandres.dev
RECLAIM Y PRESIÓN DE MEMORIA · LRU, kswapd, OOM

Cuando falta memoria: qué es el reclaim

Por qué la RAM libre casi nula no es un problema sino el diseño: el kernel llena la RAM de page cache y la recupera bajo demanda. La frontera entre memoria reclamable (cache limpia, anónima vía swap) y no reclamable, struct scan_control y la prioridad de escaneo en mm/vmscan.c.

⏱ 16 min

Abre free -h en un servidor cargado y verás la RAM libre rozando el cero. No es un síntoma: es el diseño. La memoria libre es memoria desperdiciada, así que el kernel la rellena de page cache y, cuando una asignación la reclama, no entra en pánico: la recupera. Casi toda la RAM es, en realidad, una caché que el kernel puede vaciar cuando le conviene. A ese vaciado selectivo lo llamamos reclaim, y vive en mm/vmscan.c.

🎯 Al terminar esta lección sabrás
  • Entender por qué una memoria libre próxima a cero es sana, no alarmante.
  • Distinguir memoria reclamable (page cache, anónima vía swap) de la no reclamable.
  • Leer struct scan_control y el concepto de prioridad de escaneo.
  • Ubicar el bucle de reclaim y sus contadores en /proc/vmstat.

La RAM libre es RAM desperdiciada

Cuando un proceso lee un archivo, el kernel guarda esas páginas en el page cache por si vuelven a hacer falta. No las suelta al terminar la lectura: las retiene mientras haya sitio, porque una página en RAM ahorra un viaje al disco. El resultado es que, en régimen estacionario, casi toda la RAM está ocupada por cache. Eso aparece como used en algunas herramientas y asusta a quien no conoce el modelo.

La cifra que de verdad importa no es MemFree sino MemAvailable: una estimación de cuánta memoria puede entregarse a una nueva carga sin empujar el sistema al swap, contando la parte del cache que es trivialmente recuperable. Un sistema con MemFree bajo y MemAvailable alto está sano; tiene la RAM trabajando, no ociosa.

grep -E 'MemTotal|MemFree|MemAvailable|Cached|Dirty' /proc/meminfo
# MemTotal:       32725132 kB
# MemFree:          412300 kB   <- casi nada libre: es lo normal
# MemAvailable:   21983764 kB   <- lo que de verdad puedes pedir
# Cached:         20114952 kB   <- page cache recuperable
# Dirty:             48820 kB   <- sucio: exige writeback antes de soltarse

Reclamable frente a no reclamable

El reclaim solo puede recuperar lo que tiene a dónde volver. La memoria de usuario se parte en dos grandes familias con destinos muy distintos:

  • Respaldada por archivo (page cache): tiene un origen en disco. Si la página está limpia —idéntica al disco—, reclamarla es gratis: se descarta y punto. Si está sucia, hay que escribirla primero (pageout) y solo entonces liberarla.
  • Anónima (heap, pila, mmap privado): no tiene archivo detrás. Para expulsarla, el kernel ha de inventarle un hogar: el swap (nivel 25.4). Sin swap, la memoria anónima es prácticamente irrecuperable, y la presión recae entera sobre el cache de archivo.

Y luego está lo que el reclaim no puede tocar: pilas de kernel, la mayoría de las tablas de páginas, páginas fijadas por mlock (que van a la lista LRU_UNEVICTABLE), páginas pinneadas por DMA y casi todo el slab. La excepción del slab son las cachés reclamables —dentries e inodos— que se vacían por otra vía, los shrinkers.

flowchart TD
RAM[Toda la RAM] --> REC[Reclamable]
RAM --> NOREC[No reclamable]
REC --> FILE[Page cache respaldada por archivo]
REC --> ANON[Anonima heap pila mmap privado]
FILE --> LIMPIA[Limpia se descarta al instante]
FILE --> SUCIA[Sucia exige writeback antes]
ANON --> SWAP[Necesita swap para expulsarse]
NOREC --> KERN[Pilas de kernel y tablas de paginas]
NOREC --> MLOCK[Paginas mlocked unevictable]

El plan de reclaim: struct scan_control

Toda operación de reclaim viaja dentro de una struct scan_control. Es el cuaderno de bitácora que dice cuánto recuperar, qué permisos tiene el reclaim y con cuánta agresividad debe actuar. Vale la pena leerla porque en sus campos está condensada media política de memoria del kernel:

/* mm/vmscan.c */
struct scan_control {
	/* cuántas páginas debe recuperar esta pasada */
	unsigned long nr_to_reclaim;

	/* el cgroup objetivo, o NULL para el reclaim global */
	struct mem_cgroup *target_mem_cgroup;

	/* coste relativo de reclamar anónimas frente a archivo */
	unsigned long anon_cost;
	unsigned long file_cost;

	unsigned int may_writepage:1;   /* ¿escribir páginas sucias? */
	unsigned int may_unmap:1;       /* ¿desmapear páginas de procesos? */
	unsigned int may_swap:1;        /* ¿expulsar anónimas al swap? */
	unsigned int proactive:1;       /* reclaim adelantado, no por presión */

	s8 order;           /* orden de la asignación que lo disparó */
	s8 priority;        /* prioridad de escaneo: DEF_PRIORITY..0 */
	s8 reclaim_idx;     /* zona más alta que podemos reclamar */
	gfp_t gfp_mask;

	unsigned long nr_scanned;    /* páginas miradas */
	unsigned long nr_reclaimed;  /* páginas recuperadas */
};

El campo clave es priority. Arranca en DEF_PRIORITY (12) y marca cuántas páginas se escanean por vuelta: aproximadamente el tamaño de la lista desplazado a la derecha por la prioridad. Con prioridad 12 se mira una fracción diminuta; si esa pasada no libera bastante, la prioridad baja a 11, luego 10, y cada decremento duplica el número de páginas escaneadas. El reclaim empieza siendo un roce suave y solo se vuelve agresivo si el sistema no cede memoria.

El bucle que endurece la presión

El corazón del reclaim directo es un bucle que va bajando la prioridad hasta conseguir su cuota o agotar los intentos. Simplificado:

/* mm/vmscan.c — do_try_to_free_pages */
do {
	sc->nr_scanned = 0;
	shrink_zones(zonelist, sc);          /* -> shrink_node -> shrink_lruvec */

	if (sc->nr_reclaimed >= sc->nr_to_reclaim)
		break;                       /* ya tenemos bastante */

	/* bajar la prioridad = escanear el doble en la vuelta siguiente */
} while (--sc->priority >= 0);

Bajo shrink_zones se despliega la cascada real: shrink_node() recorre cada nodo NUMA, shrink_lruvec() reparte la presión entre las listas LRU (nivel 25.2), shrink_list() procesa cada lista y shrink_folio_list() decide, folio a folio, si se descarta, se escribe o se conserva. Es en shrink_folio_list donde una página limpia de archivo se libera sin más y una anónima se manda al swap.

Junto a las páginas viajan los shrinkers, el mecanismo con el que los subsistemas ofrecen liberar objetos de slab. Una caché registra sus funciones y el reclaim las llama con presión proporcional a la prioridad:

/* include/linux/shrinker.h — reclamar objetos de slab, no páginas */
struct shrinker {
	unsigned long (*count_objects)(struct shrinker *,
	                               struct shrink_control *sc);
	unsigned long (*scan_objects)(struct shrinker *,
	                              struct shrink_control *sc);
	long batch;
	int seeks;   /* coste de recrear el objeto; DEFAULT_SEEKS = 2 */
};
/* API moderna: shrinker_alloc() + shrinker_register() */

Así se vacían las cachés de dentries e inodos: count_objects dice cuántos hay, scan_objects libera una tanda. Puedes forzarlo a mano escribiendo en /proc/sys/vm/drop_caches, herramienta útil para depurar pero venenosa en producción, porque tira cache caliente que costará releer.

ℹ️
nr_reclaimed no es lo mismo que nr_scanned

Una pasada de reclaim puede escanear muchas páginas y recuperar pocas: son páginas referenciadas hace nada, sucias a medio escribir o bloqueadas. Cuando pgscan crece mucho más deprisa que pgsteal en /proc/vmstat, el reclaim está trabajando duro para arrancar muy poco: señal temprana de que el conjunto de trabajo no cabe en RAM y el thrashing (nivel 25.4) acecha.

La RAM es una caché de sí misma

Aquí está el cambio de marco que reorganiza toda la gestión de memoria. No hay dos poblaciones —la memoria “usada” y la “libre”— sino una sola: todo es caché de algo que vive, o podría vivir, en almacenamiento más lento. El page cache es caché del disco; la memoria anónima es caché de su propia copia en swap; hasta el ejecutable de un proceso es caché de su archivo en el filesystem. Desde esta óptica, la memoria libre no es un recurso que “conservar”, sino capacidad de caché sin explotar, es decir, desperdicio puro. El trabajo del kernel no es tener memoria libre, sino mantener en RAM lo que más se va a usar y expulsar lo que menos. Reclaim no es una emergencia: es el latido continuo que decide, millones de veces por segundo, qué merece seguir caliente. Cuando interiorizas que el sistema entero es una jerarquía de cachés y que la RAM es solo el nivel más rápido y más pequeño, dejas de temer al reclaim y empiezas a leerlo como lo que es: la política de desalojo de la caché más importante de la máquina.

⚔️ Mide tu propia caché
  1. Compara MemFree y MemAvailable en /proc/meminfo y explica por qué difieren tanto.
  2. Lee un archivo grande con cat archivo > /dev/null, mira crecer Cached, y observa que MemFree baja sin que el sistema sufra.
  3. Escribe 1 en /proc/sys/vm/drop_caches y comprueba cómo Cached se desploma; razona por qué eso es malo en producción.
  4. Clasifica en reclamable o no reclamable: una página mmap de un archivo, la pila de un hilo, un buffer malloc, una página mlock.
  5. Observa pgscan_kswapd, pgsteal_kswapd y pgscan_direct en /proc/vmstat bajo carga y razona qué te dice su cociente.