wandres.dev
EL PAGE ALLOCATOR · buddy, órdenes, GFP

La memoria física: struct page y el PFN

Cómo el kernel ve la RAM: la trocea en marcos de tamaño fijo, le pone a cada uno un descriptor struct page y los numera con el PFN. Las macros de conversión entre página, PFN y dirección física, y el mapa de memoria que los enlaza.

⏱ 15 min

Para el kernel, la RAM no es un océano continuo de bytes: es un vector de marcos de tamaño fijo, y cada marco lleva su ficha de identidad. Antes de que exista un solo asignador —buddy, slab, vmalloc— hay que dominar la pieza atómica sobre la que todos se apoyan: la página física, su descriptor struct page y el número que la nombra, el PFN.

🎯 Al terminar esta lección sabrás
  • Entender por qué el kernel trocea la RAM en marcos de PAGE_SIZE.
  • Leer los campos esenciales de struct page y saber lo que cuesta.
  • Convertir entre struct page, PFN y dirección física con las macros correctas.
  • Comprender el mapa de memoria (mem_map y vmemmap) que enlaza marco y descriptor.

El marco de página: la unidad indivisible

La MMU del procesador no traduce bytes sueltos, sino bloques alineados de tamaño fijo llamados marcos (page frames). En x86-64 ese tamaño es 4 KiB, y toda la gestión de memoria física del kernel se hace en esa granularidad: nunca se reserva medio marco. La constante es PAGE_SIZE, derivada de PAGE_SHIFT:

/* arch/x86/include/asm/page_types.h */
#define PAGE_SHIFT		12
#define PAGE_SIZE		(_AC(1, UL) << PAGE_SHIFT)   /* 4096 */
#define PAGE_MASK		(~(PAGE_SIZE - 1))

PAGE_SHIFT vale 12 porque 2^12 = 4096. Con PAGE_MASK se alinea cualquier dirección al inicio de su marco mediante addr & PAGE_MASK, y el desplazamiento dentro del marco es addr & ~PAGE_MASK. Trabajar en potencias de dos convierte multiplicaciones y divisiones en desplazamientos de bits: el kernel jamás divide entre PAGE_SIZE, desplaza PAGE_SHIFT posiciones.

struct page: una ficha por marco

Por cada marco físico existe exactamente un descriptor struct page. No guarda los datos del marco —esos están en la RAM que describe— sino su estado: si está libre o en uso, quién lo mapea, a qué se dedica. Es una de las estructuras más densas del kernel, un mosaico de uniones donde cada asignador reinterpreta los mismos bytes:

/* include/linux/mm_types.h (muy simplificado) */
struct page {
	unsigned long flags;		/* PG_locked, PG_dirty, zona, nodo NUMA... */
	union {
		struct {		/* page cache y paginas anonimas */
			struct list_head lru;	  /* cola del reclaim o del buddy */
			struct address_space *mapping;
			pgoff_t index;
			unsigned long private;	  /* el buddy guarda aqui el orden */
		};
		struct {		/* cabeza o cola de pagina compuesta */
			unsigned long compound_head;
		};
		/* ... slab, ptdesc, ZONE_DEVICE ... */
	};
	atomic_t _mapcount;		/* cuantas PTE de usuario la mapean */
	atomic_t _refcount;		/* referencias del nucleo; 0 = libre */
} _struct_page_alignment;

El detalle que lo cambia todo: sizeof(struct page) es 64 bytes. Como hay un descriptor por cada marco de 4096 bytes, el propio mapa de páginas consume 64/4096 = 1,56% de toda la RAM instalada. En una máquina de 1 TiB son 16 GiB solo en struct page. Por eso cada bit de flags se pelea con uñas y dientes, y por eso Linux 7.x empuja hacia struct folio: un folio agrupa varias páginas contiguas bajo una sola ficha y adelgaza esa sobrecarga.

Los dos contadores atómicos son el corazón del ciclo de vida: _refcount cuenta referencias del kernel —cuando cae a cero, la página vuelve al asignador—, y _mapcount cuenta cuántas tablas de páginas de usuario la referencian.

El PFN y las macros de conversión

El PFN (page frame number) es el índice del marco: la dirección física dividida entre el tamaño de página, es decir, la dirección física desplazada PAGE_SHIFT bits a la derecha. Es el nombre canónico de un marco, independiente de dónde esté mapeado. El kernel ofrece un juego cerrado de macros para viajar entre las tres representaciones —descriptor, PFN y dirección física—:

unsigned long pfn  = page_to_pfn(page);      /* pagina  -> PFN                */
struct page  *p    = pfn_to_page(pfn);        /* PFN     -> pagina             */
phys_addr_t   phys = page_to_phys(page);      /* pagina  -> direccion fisica   */
void         *virt = page_address(page);      /* pagina  -> virtual (mapa directo) */
struct page  *p2   = virt_to_page(virt);      /* virtual -> pagina             */

Bajo el capó son aritmética pura:

#define page_to_phys(page)	((phys_addr_t)page_to_pfn(page) << PAGE_SHIFT)
#define virt_to_page(kaddr)	pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)

Una advertencia capital: page_address() solo devuelve una dirección virtual válida para memoria mapeada directamente (en 64 bits, esencialmente toda la de ZONE_NORMAL). Para memoria alta o para páginas fuera del mapa lineal hay que mapearlas temporalmente con kmap_local_page(). En x86-64 no hay memoria alta, así que el mapa directo cubre casi toda la RAM y page_address() casi siempre funciona.

📝
El mapa directo: __pa, __va y PAGE_OFFSET

En 64 bits el kernel mantiene un mapa lineal de toda la RAM en su espacio virtual, empezando en PAGE_OFFSET. Ahí, dirección física y virtual difieren en una constante: __va(phys) suma PAGE_OFFSET y __pa(virt) la resta. Por eso page_address() es casi gratis para memoria normal —es aritmética, no una búsqueda en tablas—, y por eso virt_to_page() puede componer __pa con un desplazamiento. Este mapa lineal es la razón de que el kernel pueda tratar la RAM física como un vector plano directamente direccionable.

Del PFN al descriptor: el mapa de memoria

¿Cómo pasa pfn_to_page() de un número a un puntero? Depende del modelo de memoria. Con FLATMEM, todos los descriptores viven en un vector global contiguo, mem_map, y la conversión es una resta:

/* include/asm-generic/memory_model.h — FLATMEM */
#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(pg)	((unsigned long)((pg) - mem_map) + ARCH_PFN_OFFSET)

Los sistemas reales usan SPARSEMEM_VMEMMAP, que soporta huecos en la memoria física (hot-plug, rangos reservados) sin desperdiciar descriptores. Coloca un vector virtual vmemmap donde el índice es directamente el PFN, respaldado por tablas de páginas que solo cubren los marcos que existen:

/* SPARSEMEM_VMEMMAP */
#define __pfn_to_page(pfn)	(vmemmap + (pfn))
#define __page_to_pfn(pg)	((unsigned long)((pg) - vmemmap))

Así pfn_to_page(pfn) vuelve a ser una suma de puntero, tan barata como con FLATMEM, pero tolerando una memoria física llena de agujeros.

flowchart LR
RAM[RAM fisica en marcos de 4 KiB] --> M0[marco 0]
RAM --> M1[marco 1]
RAM --> Mn[marco N]
M0 -->|PFN 0| P0[struct page 0]
M1 -->|PFN 1| P1[struct page 1]
Mn -->|PFN N| Pn[struct page N]
P0 --> VM[vmemmap un descriptor por PFN]
P1 --> VM
Pn --> VM
La página es el átomo; todo lo demás es química

Detente en la magnitud de lo que acabas de ver. Cada objeto que el kernel maneja —una task_struct, un socket, un inodo, una pila de kernel, el búfer de un DMA— vive, en última instancia, sobre marcos físicos descritos por un struct page. El asignador de slab que estudiarás después no fabrica memoria: pide páginas al buddy y las trocea. vmalloc no fabrica memoria: pide páginas al buddy y las cose en tablas. El page cache que hace rápido a tu disco es, literalmente, un mar de struct page. Por eso el orden de aprendizaje del kernel es este y no otro: quien entiende la página entiende el sustrato; quien no, memoriza APIs sueltas sin ver el suelo que pisan. Y fíjate en la elegancia económica de la idea: un entero, el PFN, nombra sin ambigüedad cualquier trozo de RAM de la máquina, y una suma de puntero lo convierte en su ficha. Sobre esa correspondencia trivial —PFN, descriptor, dirección— se levantan gigabytes de subsistemas. Cuando abras mm/ y veas pfn_to_page, page_to_pfn, virt_to_page sembrados por todas partes, ya no serán ruido: serán el idioma con el que el kernel habla de la materia de la que está hecho.

⚔️ Palpa la memoria física desde un módulo
  1. Escribe un módulo que reserve una página con alloc_page(GFP_KERNEL) e imprima con pr_info su PFN (page_to_pfn), su dirección física (page_to_phys) y su virtual (page_address).
  2. Verifica a mano que page_to_phys(page) es igual a page_to_pfn(page) << PAGE_SHIFT.
  3. Toma la virtual devuelta, aplícale virt_to_page y comprueba que recuperas el mismo struct page.
  4. Lee sizeof(struct page) y calcula, para la RAM de tu máquina, cuántos MiB gasta el vmemmap.
  5. Contrasta tus PFN con los rangos que muestra /proc/iomem para System RAM.