El buddy allocator: bloques, fusión y fragmentación
El asignador de páginas de Linux agrupa marcos en bloques de 2 elevado a n páginas ordenados por orden. Cómo parte un bloque grande para servir uno pequeño, cómo fusiona dos compañeros libres al liberar, y por qué la fragmentación externa es su enemigo natural.
El buddy allocator es el asignador de páginas físicas de Linux: la fuente última de la que beben slab, vmalloc y el page cache. Su idea es de una simplicidad brutal —gestionar la memoria en bloques de 2^n páginas y emparejar cada bloque con su compañero— y de esa idea nacen a la vez su velocidad, su capacidad de defragmentar y su talón de Aquiles.
- Entender cómo el buddy organiza la memoria libre en listas por orden.
- Seguir el reparto (split) de un bloque grande para servir uno pequeño.
- Seguir la fusión (coalescing) de dos compañeros al liberar.
- Reconocer la fragmentación externa y las defensas del kernel contra ella.
Orden, bloques y compañeros
El buddy mantiene, en cada zona, una lista de bloques libres por cada orden. Un bloque de orden n son exactamente 2^n páginas físicamente contiguas y alineadas a esa potencia. El orden 0 es una página; el orden 10 —máximo por defecto— son 1024 páginas, o 4 MiB:
/* include/linux/mmzone.h */
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
struct zone {
/* ... */
struct free_area free_area[NR_PAGE_ORDERS]; /* MAX_PAGE_ORDER + 1 */
/* ... */
};
NR_PAGE_ORDERS es MAX_PAGE_ORDER + 1, y MAX_PAGE_ORDER vale 10 por defecto. Cada free_area[o] encadena por el campo lru todos los bloques libres de orden o.
El nombre del asignador viene de aquí: cada bloque de orden o tiene un único compañero (buddy), el bloque adyacente con el que, unidos, forman un bloque alineado de orden o+1. Su PFN se calcula con un solo XOR:
/* mm/page_alloc.c */
static inline unsigned long
__find_buddy_pfn(unsigned long page_pfn, unsigned int order)
{
return page_pfn ^ (1 << order);
}
Invertir el bit número order del PFN salta exactamente al compañero: si tienes el bloque par, obtienes el impar de al lado, y viceversa. Esa aritmética de un ciclo es la razón de ser del buddy.
Repartir: de un bloque grande a uno pequeño
Cuando pides un bloque de orden low y esa lista está vacía, el asignador sube de orden en orden hasta hallar un bloque libre, lo saca de su lista y lo parte por la mitad repetidamente. Cada mitad sobrante se devuelve a la lista del orden inferior; la otra sigue bajando hasta alcanzar el orden pedido:
/* mm/page_alloc.c — reparto de un bloque de orden high a orden low (simplificado) */
static inline void expand(struct zone *zone, struct page *page,
int low, int high, int migratetype)
{
unsigned long size = 1 << high;
while (high > low) {
high--;
size >>= 1;
/* la mitad alta vuelve a la lista libre del orden inferior */
add_to_free_list(&page[size], zone, high, migratetype);
set_buddy_order(&page[size], high);
}
}
Pedir una sola página cuando solo hay un bloque de orden 4 disponible genera, como residuo, un bloque libre de orden 3, uno de orden 2, uno de orden 1 y uno de orden 0. El buddy prefiere fragmentar el bloque más pequeño que sirva, para conservar intactos los grandes.
flowchart TD O3[bloque libre orden 3 con 8 paginas] -->|split| A2[orden 2 sigue bajando] O3 -->|residuo| B2[orden 2 vuelve a la lista libre] A2 -->|split| A1[orden 1 sigue bajando] A2 -->|residuo| B1[orden 1 vuelve a la lista libre] A1 -->|split| USO[orden 0 pagina entregada] A1 -->|residuo| B0[orden 0 vuelve a la lista libre]
Fusionar: el reflejo al liberar
La operación inversa es la que da al buddy su magia antifragmentación. Al liberar un bloque, el asignador mira si su compañero también está libre y es del mismo orden; si lo está, los fusiona en un bloque del doble de tamaño, y repite la comprobación subiendo de orden mientras pueda:
/* mm/page_alloc.c — nucleo de __free_one_page (simplificado) */
while (order < MAX_PAGE_ORDER) {
buddy_pfn = __find_buddy_pfn(pfn, order);
buddy = page + (buddy_pfn - pfn);
if (!page_is_buddy(page, buddy, order))
break; /* el companero no esta libre: paramos */
del_page_from_free_list(buddy, zone, order);
combined_pfn = buddy_pfn & pfn; /* PFN del bloque fusionado */
page = page + (combined_pfn - pfn);
pfn = combined_pfn;
order++; /* subimos e intentamos de nuevo */
}
set_buddy_order(page, order);
add_to_free_list(page, zone, order, migratetype);
page_is_buddy() es el guardián: solo acepta la fusión si el compañero tiene puesto el flag PageBuddy (está en una lista libre), es exactamente del mismo orden y pertenece a la misma zona. Gracias a esta fusión, un patrón de reservas y liberaciones vuelve a dejar la memoria en bloques grandes en cuanto los compañeros coinciden libres, sin ningún barrido explícito.
Fragmentación externa: el enemigo natural
El buddy elimina casi toda la fragmentación interna (el desperdicio dentro de un bloque, acotado a menos de una página por asignación en el peor caso). Su enemigo es la fragmentación externa: hay muchas páginas libres, pero dispersas, de modo que ningún conjunto de compañeros llega a fusionarse en el bloque grande que alguien necesita. Puedes tener el 40% de la RAM libre y aun así fallar al pedir un bloque de orden 4.
Linux se defiende con dos armas. La primera es agrupar por tipo de migración (MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RECLAIMABLE): cada free_area tiene una lista por tipo, y el asignador mantiene las páginas movibles juntas y separadas de las inamovibles, de forma que las movibles puedan reubicarse en bloque. La segunda es la compactación: el hilo kcompactd y la compactación directa migran páginas movibles para liberar bloques contiguos grandes bajo demanda. Puedes auscultar el estado de las listas libres en /proc/buddyinfo, que imprime, por zona, cuántos bloques libres hay en cada orden:
$ cat /proc/buddyinfo
Node 0, zone Normal 1834 1290 742 301 118 44 12 3 1 0 0
# ord0 ord1 ord2 ord3 ... ord10
Una fila con números altos a la izquierda y ceros a la derecha es el retrato de la fragmentación externa: abundan las páginas sueltas, escasean los bloques grandes.
El tipo de migración no se guarda por página, sino por pageblock: un tramo grande —típicamente el tamaño de una página enorme, 2 MiB en x86-64— que comparte un mismo MIGRATE_*. Agrupar en bloques tan grandes es lo que permite que la compactación reúna extensiones contiguas de páginas movibles. Puedes ver el desglose con cat /proc/pagetypeinfo, que muestra, por zona y por orden, cuántos bloques libres hay de cada tipo de migración: una lente más fina que /proc/buddyinfo para diagnosticar por qué falla una asignación de orden alto pese a haber memoria libre.
Mira la fusión de nuevo y date cuenta de lo que estás viendo: la contabilidad de toda la memoria física de Linux —desde un router doméstico hasta un servidor con terabytes— descansa sobre la identidad buddy_pfn = pfn ^ (1 << order). Esa sola línea garantiza que el compañero de un bloque es único, que fusionar es asociativo y que un bloque alineado de orden n solo puede formarse de una manera. No hay tablas auxiliares, no hay búsqueda: el orden se guarda en page->private, el flag PageBuddy marca lo libre, y un XOR encuentra al vecino. De esa economía nacen las tres propiedades que hacen viable un sistema operativo de propósito general: asignación en tiempo casi constante, defragmentación automática al liberar y un límite duro y demostrable a la fragmentación interna. Y de la misma economía nace su límite: la fragmentación externa, que el kernel no puede abolir, solo combatir con migración y compactación. Entender esta tensión —simplicidad radical a cambio de una fragilidad acotada— es entender por qué encima del buddy hicieron falta slab para los objetos pequeños y compactación para las páginas enormes. El buddy no es un detalle de implementación: es la decisión de diseño de la que cuelga todo el subsistema de memoria.
- Lee
/proc/buddyinfoen reposo y anota el perfil de órdenes de la zona Normal. - Desde un módulo, reserva un bloque grande con
alloc_pages(GFP_KERNEL, 4)y vuelve a leer/proc/buddyinfo: localiza la fila que cambió. - Libera el bloque con
__free_pages(page, 4)y comprueba cómo la fusión reconstruye órdenes altos. - Escribe en papel el PFN del compañero de un bloque de orden 2 cuyo primer PFN sea 0x3C000, aplicando el XOR.
- Provoca fragmentación reservando muchas páginas de orden 0 salteadas y observa cómo caen los órdenes altos en
/proc/buddyinfo.