wandres.dev
SLAB Y SLUB · kmalloc, caches de objetos

Por qué un slab: objetos pequeños sobre el buddy

El asignador de páginas buddy solo sabe repartir páginas enteras, y una estructura de 48 bytes desperdicia el 98 por ciento de una página. El slab es la respuesta: una caché de objetos del mismo tamaño tallada sobre las páginas del buddy. Linaje Bonwick, SLAB, SLUB.

⏱ 14 min

El asignador buddy es la roca sobre la que se apoya toda la memoria del kernel, pero reparte en una única moneda: la página. Cuando lo que necesitas es un objeto de 48 bytes —y necesitas millones por segundo— pedir una página entera por cada uno es un derroche grotesco. El slab nace para llenar ese hueco: convierte páginas en cachés de objetos pequeños del mismo tamaño, sin desperdicio y con reutilización casi gratuita.

🎯 Al terminar esta lección sabrás
  • Ver por qué el asignador buddy no sirve para objetos pequeños.
  • Definir con precisión caché, slab y objeto, y la relación entre ellos.
  • Entender los tres estados de un slab: lleno, parcial y vacío.
  • Situar el linaje Bonwick, SLAB, SLOB y SLUB, y el estado en Linux 7.x.

El buddy solo sabe de páginas

El asignador de páginas buddy es un genio con una sola talla. Entrega bloques de páginas físicamente contiguas cuyo tamaño es una potencia de dos —el orden—, y su unidad mínima, el orden cero, es una página: 4 KiB en x86-64. Si pides menos, da igual: recibes una página completa.

#include <linux/gfp.h>
#include <linux/mm.h>

struct conexion {
	u64            id;
	unsigned int   estado;
	void          *buffer;
	/* ~48 bytes en total */
};

/* pido sitio para una 'conexion'... y el buddy me entrega 4096 bytes */
struct page *pg = alloc_pages(GFP_KERNEL, 0);   /* orden 0 = una pagina */
struct conexion *c = page_address(pg);
/* 4096 - 48 = 4048 bytes tirados: 98,8 % de fragmentacion interna */

Dos patologías hacen inviable construir el kernel así. La primera es la fragmentación interna: 4048 de 4096 bytes muertos por objeto. La segunda es la presión sobre el buddy: sus listas por orden y por zona están protegidas por locks y por las colas de páginas por-CPU; martillearlo millones de veces por segundo para trocitos diminutos serializa el sistema entero. Y hay una tercera carencia, más sutil: el buddy no tiene noción de tipo ni de reutilización. Cada página que devuelves y vuelves a pedir llega fría en la caché y sin inicializar.

Pon números a la presión. El buddy mantiene once listas de bloques libres por zona —de orden 0 a orden 10—, cada una bajo el zone->lock, y para amortiguar la contención cachea páginas de orden cero en colas por-CPU. Aun así, cada alloc_pages y cada free_pages es una operación relativamente cara: comprueba marcas de agua, puede despertar a kswapd y, en el peor caso, fusiona o parte bloques para mantener la invariante buddy. Encaminar por ahí cada kmalloc de 32 bytes sería como pedir una hipoteca para comprar un café.

ℹ️
Fragmentación interna contra externa

Interna es el espacio malgastado dentro de una asignación que te queda grande (los 4048 bytes de arriba). Externa es la incapacidad de satisfacer una petición grande porque la memoria libre está troceada en huecos pequeños dispersos. El slab ataca de raíz la interna —todos los objetos de una caché miden lo mismo, encajan sin holgura— y evita la externa porque nunca pide al buddy más de unas pocas páginas por slab.

El slab: una caché de objetos del mismo tamaño

La idea, que Jeff Bonwick formalizó en SunOS en 1994, es interponer una capa entre el buddy y quien pide memoria. Esa capa gestiona cachés, y cada caché sirve objetos de un único tamaño o tipo. Tres términos, jerárquicos:

  • Objeto: la unidad que entregas al llamante, de tamaño fijo dentro de su caché.
  • Slab: una o varias páginas contiguas pedidas al buddy y talladas en un array de objetos idénticos. Es la unidad de crecimiento y de devolución al buddy.
  • Caché (struct kmem_cache): la colección de slabs que sirven un mismo tipo de objeto, más los metadatos para encontrar el siguiente objeto libre.

En código, ese contrato se declara una sola vez y luego se explota millones de veces sin volver a mirar al buddy:

/* se declara la cache una vez, con el tamano del objeto */
struct kmem_cache *cache = kmem_cache_create("mi_objeto",
					     sizeof(struct mi_objeto),
					     0, SLAB_HWCACHE_ALIGN, NULL);

/* a partir de aqui, asignar y liberar es trabajo local en la slab */
struct mi_objeto *o = kmem_cache_alloc(cache, GFP_KERNEL);
kmem_cache_free(cache, o);
flowchart LR
BUD[asignador buddy] -->|alloc_slab_page| SLAB[slab = N paginas contiguas]
SLAB --> OBJ[array de objetos del mismo tamano]
CACHE[kmem_cache] -->|listas de slabs| SLAB
style BUD fill:#89b4fa,color:#11111b
style CACHE fill:#cba6f7,color:#11111b
style OBJ fill:#a6e3a1,color:#11111b

La ganancia decisiva no es solo el ahorro de espacio, es la reutilización con estado. Cuando liberas un objeto, no se devuelve al buddy: vuelve a la lista de libres de su slab, caliente en la caché de la CPU y —si la caché tiene constructor— ya inicializado a su estado base. La siguiente asignación del mismo tipo lo recupera en un puñado de instrucciones, sin tocar el buddy y sin reconstruir sus campos invariantes. En un kernel que crea y destruye struct file, struct dentry o struct sk_buff a ritmo frenético, amortizar la inicialización y evitar el viaje al buddy es la diferencia entre escalar y ahogarse.

¿Cuántos objetos entran en una slab? El asignador elige el orden de páginas de cada slab buscando un equilibrio: suficientes objetos por slab para amortizar los metadatos y las transiciones de estado, sin pedir bloques tan grandes que estresen al buddy o desperdicien memoria por redondeo. Un objeto de 64 bytes entra 64 veces en una slab de una página; uno de 320 bytes puede justificar una slab de orden 1 o 2 para que el número de objetos por slab —y con él la eficiencia— no se desplome. En SLUB ese cálculo lo hace calculate_order al crear la caché, y su resultado lo lees en la columna pagesperslab de /proc/slabinfo.

Los tres estados de un slab

Dentro de una caché, cada slab está en uno de tres estados según cuántos de sus objetos estén en uso. La caché los rastrea para asignar siempre por el camino más barato.

🟥

Lleno

Todos sus objetos están asignados. No puede servir nada más; la caché ni lo mira hasta que se libere algo.

🟨

Parcial

Tiene objetos libres y objetos en uso. Es el estado preferido: asignar de aquí no toca el buddy.

🟩

Vacío

Ninguno en uso. Candidato a devolverse al buddy cuando sobra memoria, o a conservarse como reserva caliente.

Los objetos individuales al asignarse y liberarse empujan a su slab entre estos tres estados, y ese tránsito determina cuándo se toca el buddy:

flowchart LR
V[slab vacia] -->|primera asignacion| P[slab parcial]
P -->|se ocupa el ultimo hueco| L[slab llena]
L -->|se libera un objeto| P
P -->|se libera el ultimo objeto| V
V -->|bajo presion de memoria| BUD[devuelta al buddy]
style P fill:#f9e2af,color:#11111b
style L fill:#f38ba8,color:#11111b
style V fill:#a6e3a1,color:#11111b

El invariante que rige toda petición es simple: buscar primero en las slabs parciales. Solo si no hay ninguna parcial se promueve una vacía o se pide una slab nueva al buddy. Y cuando una slab parcial se llena por completo, migra al conjunto de llenas; cuando una se vacía del todo, pasa a vacía y quizá se libere. Ese baile de estados, que en SLUB se refina con listas por-CPU y por-nodo NUMA, es lo que mantiene el camino caliente lejos del buddy casi siempre.

Menos viajes al buddy, y el coloreado de caché

La segunda gran ventaja del slab, además de eliminar el desperdicio, es absorber la inmensa mayoría de asignaciones y liberaciones sin bajar al buddy. Mientras una caché tenga slabs parciales o vacías en reserva, servir y devolver objetos es trabajo puramente local; el buddy solo interviene cuando la caché crece o se encoge de verdad. En una carga típica, casi ninguna operación de memoria del kernel llega a tocar el asignador de páginas.

El SLAB clásico añadía un truco hoy abandonado: el coloreado de caché. Como todos los objetos de una slab arrancan en desplazamientos regulares, dos slabs alineadas igual harían que sus primeros objetos compitieran por los mismos conjuntos de la caché de la CPU. SLAB desplazaba —coloreaba— el inicio de cada slab unos bytes para repartir los objetos entre conjuntos distintos y así rebajar los fallos de caché por conflicto. SLUB lo eliminó por simplicidad y porque las cachés asociativas modernas y el prefetching lo hacen menos rentable: un recordatorio de que cada decisión de un asignador es un compromiso medible, no un dogma.

💡
La regla mental del asignador correcto

Para menos de una página y un tipo que asignas mucho, una caché slab propia. Para menos de una página y tamaño variable u ocasional, kmalloc. Para una o varias páginas contiguas, el buddy vía alloc_pages. Para regiones grandes que no exigen contigüidad física, vmalloc. Elegir la capa equivocada —un kmalloc gigante donde tocaba vmalloc, o alloc_pages para un objeto diminuto— es uno de los errores de novato más caros en rendimiento y en fiabilidad.

Lo que el slab también cuesta

Ninguna abstracción es gratis, y la honestidad intelectual obliga a nombrar el precio. El primero es la fragmentación interna residual: kmalloc redondea a clases de tamaño, así que un objeto de 65 bytes sigue ocupando 96 (nivel 21.2). El segundo, más sutil, es la fragmentación por slab anclada: una slab no puede volver al buddy mientras retenga un solo objeto vivo. Si asignas mil objetos, liberas 999 y dejas uno por slab, acabas con mil slabs medio vacías que el kernel no puede recuperar. Es el equivalente slab de la fragmentación externa, y castiga a cachés de vida larga como las de dentry o inode.

El tercer coste es deliberado: las cachés por-CPU y las listas de slabs parciales retienen memoria libre como reserva caliente, que no figura como disponible hasta que la presión obliga a soltarla. Para eso el kernel registra shrinkers: bajo escasez, kswapd y el reclaimer piden a cada subsistema que drene sus cachés reclamables (SLAB_RECLAIM_ACCOUNT), devolviendo las slabs vacías al buddy. El slab, por tanto, no es solo un asignador: es un participante activo en el ciclo de reclamación de memoria del sistema.

⚠️
Un objeto vivo ancla una slab entera

La consecuencia práctica: la vida de tus objetos importa tanto como su número. Miles de objetos longevos esparcidos por muchas slabs impiden liberar esas slabs aunque el grueso esté vacío. Agrupar por vida útil —efímeros juntos, longevos aparte, idealmente en cachés distintas— es una técnica real para combatir la fragmentación slab en subsistemas que asignan mucho y liberan a destiempo.

El linaje: SLAB, SLOB y SLUB

Durante años, Linux ofreció una sola APIkmalloc, kmem_cache_create y compañía— con tres implementaciones intercambiables detrás, elegidas en tiempo de compilación:

  • SLAB: el diseño original de Bonwick portado por Mark Hemment, con colas por-CPU y coloreado de caché. Potente pero con metadatos pesados.
  • SLOB: minimalista, para sistemas empotrados con muy poca RAM.
  • SLUB: escrito por Christoph Lameter en 2007, con metadatos mínimos incrustados en el propio descriptor de página, listas por-CPU sin locks y fusión de cachés. Se volvió el predeterminado.

En los kernels modernos la historia se cerró: SLOB se eliminó en 6.4 (2023) y SLAB se eliminó en 6.8 (2024). En Linux 7.x, en 2026, SLUB es el único asignador de objetos que queda. Toda la API pública, sin embargo, sigue idéntica y estable: el código que escribiste contra kmalloc hace quince años compila y funciona igual hoy.

¿Por qué ganó SLUB? Por sus metadatos frugales —sin arrays de gestión aparte, la freelist vive incrustada en los objetos libres—, por su ruta rápida por-CPU sin locks que escala a cientos de núcleos, y por la introspección que expone en /sys/kernel/slab. SLAB, más veterano, arrastraba estructuras de control voluminosas y un modelo de colas más difícil de razonar. Cuando el hardware saltó de cuatro a cientos de núcleos, el diseño de SLUB envejeció mejor. Los niveles 21.2 a 21.5 de este track diseccionan justo esa API y sus tripas SLUB.

La caché de objetos: la abstracción que hizo escalar al kernel

Detente en la magnitud del cambio conceptual. El buddy piensa en términos de espacio físico: páginas, órdenes, contigüidad. El slab introduce una idea ortogonal y más rica: el objeto tipado con ciclo de vida. Al declarar “voy a crear y destruir millones de cosas de este tamaño exacto”, le das al asignador la información que necesita para optimizar de formas que un malloc anónimo jamás podría: talla las páginas sin desperdicio, mantiene los objetos calientes en caché entre usos, preserva su estado inicializado, agrupa por nodo NUMA y por CPU, y solo baja al buddy cuando de verdad hace falta más terreno. Esta es la razón profunda de que el kernel gestione con soltura las estructuras más volátiles del sistema —descriptores de fichero, entradas de dentry, buffers de red— a ritmos que colapsarían cualquier asignador de propósito general. No es un truco de rendimiento: es un cambio de nivel de abstracción, de “dame bytes” a “gestiona el ciclo de vida de este tipo por mí”. Interiorizar esa distinción es el primer paso para entender toda la gestión de memoria del núcleo.

⚔️ Mide el desperdicio y nombra las piezas
  1. Calcula el porcentaje de fragmentación interna al servir un objeto de 40, 100 y 200 bytes con páginas del buddy de 4 KiB.
  2. Define con tus palabras la diferencia entre caché, slab y objeto, y dibuja cómo se anidan.
  3. Explica por qué liberar un objeto a su slab, en vez de al buddy, es más barato que volver a pedirlo.
  4. Enumera los tres estados de un slab y razona por qué la asignación prefiere siempre las slabs parciales.
  5. Sitúa SLAB, SLOB y SLUB en la línea temporal, y di cuál es el único que sobrevive en Linux 7.x y desde qué versión.