On this page
Physical Memory Management
Coverage: NUMA → Zone (DMA/DMA32/Normal/Movable) → Buddy Allocator → page allocator API → page struct → memory hotplug Kernel version: 2.6 ~ 6.x
Overview
Virtual memory management answers "which virtual address maps to which physical page," while physical memory management answers a more fundamental question: "Who does this 4KB physical page belong to, is it free or allocated, and from which NUMA node should it be taken?"
The core of physical memory management is the buddy allocator, which organizes free pages into contiguous blocks of different orders, efficiently allocating and reclaiming them through splitting and merging. Above the buddy allocator lies slab/slub (small object allocators), and below it lies the NUMA-aware zone and node topology.
NUMA Architecture
UMA (Uniform Memory Access):
Consistent latency for all CPUs accessing all memory
Typical: Single socket, desktop
NUMA (Non-Uniform Memory Access):
Memory is divided into multiple nodes
Each CPU has its own local memory (fast access)
Accessing remote memory is slower (30%~40% slower)
Typical: Multi-socket servers
View: numactl --hardware
ls /sys/devices/system/node/
pg_data_t: NUMA Node Descriptor
// include/linux/mmzone.h
typedef struct pglist_data pg_data_t;
Zone: Physical Memory Partitioning
Why Zones Are Needed
The fundamental reason for zones: Hardware limitations prevent certain physical memory from being used for specific purposes
16-bit ISA DMA: Can only access physical addresses 0~16MB
32-bit PCI DMA: Can only access 0~4GB (unless IOMMU is used)
32-bit kernel: Can only directly map 896MB (highmem)
64-bit: No highmem, but zone structure retains DMA/DMA32 limitations
Zone Types
// include/linux/mmzone.h
;
struct zone
// include/linux/mmzone.h
;
Watermarks and Memory Pressure
// Watermark check during allocation:
// zone_watermark_ok(zone, order, mark, classzone_idx, alloc_flags)
// Calculates whether the zone's free pages satisfy the order allocation and do not fall below mark
// If insufficient → triggers reclaim (kswapd or direct reclaim)
Buddy Allocator
Data Structures
// mm/page_alloc.c
// Each zone has a free_area for each order
// free_area[order]: Linked list of free blocks of size 2^order pages
;
// Migration types (anti-fragmentation):
;
Core Algorithm
// Allocation:
// alloc_pages(gfp_mask, order) → __alloc_pages()
// → get_page_from_freelist() // Try to allocate from a suitable zone
// → rmqueue() // Take from buddy free_area
// → __rmqueue_smallest() // Find the smallest free block >= order
// → expand() // If the found block is larger than needed → split
// The lower half is put back into buddy free_area
// Example: Request order=1 (2 pages = 8KB)
// free_area[2] has 16KB free → expand():
// → Allocate the first 8KB (order 0~1) to the caller
// → Put the remaining 8KB into free_area[1]
// Freeing:
// free_pages(addr, order) → __free_pages()
// → __free_one_page()
// → Check if the buddy (partner) is also free
// → If buddy is free → merge into an order+1 block → recursive merge
// → If buddy is not free → put into the corresponding order's free_area
// Buddy check:
// buddy_pfn = page_pfn ^ (1 << order) // XOR: PFN of the buddy
// If buddy is also free and of the same order → merge
GFP Flags
// include/linux/gfp_types.h
// Control of allocation behavior:
// Zone modifiers:
// Watermark modifiers:
// Reclaim modifiers:
// Common combinations:
// Most common: allows sleeping/reclaim/IO
// Cannot sleep: used in interrupt/atomic context
// User space allocation
// Do not wait: return NULL immediately if allocation fails
Per-CPU Pages (PCP)
// mm/page_alloc.c
// Each CPU has a per-CPU 1-page cache (struct per_cpu_pages)
//
// Why?
// Allocating a single page (order 0) is the most common operation
// If every time you have to take zone->lock (global lock) → severe lock contention
// PCP acts as L1 cache: locally prefetch a batch of pages
// → alloc: PCP has it → take directly (lock-free)
// PCP empty → take a batch from buddy (take zone lock once)
// → free: PCP not full → put back into PCP (lock-free)
// PCP full → put back into buddy (take zone lock once)
//
// Design principle: Batch operations amortize lock overhead
struct page
// include/linux/mm_types.h
// Each physical page has a struct page (4KB page manages 64 bytes → ~1.5% overhead)
;
struct page is an overloaded structure—the same field has different interpretations depending on the page's usage (buddy free, slab, anonymous memory, page cache, THP compound). The kernel uses bits in flags to distinguish them.
Memory Hotplug
// mm/memory_hotplug.c
// Physically adding/removing memory sticks:
// online: add_memory() → __add_memory() → initialize page struct → add to buddy
// offline: remove_memory() → __remove_memory() → migrate used pages → remove from buddy
// DIMM (NVDIMM/CXL):
// Can be made into ZONE_DEVICE (not managed by buddy)
// Or made into normal memory (ZONE_NORMAL), allocated by buddy
Debugging
# Buddy allocator status
# Node 0, zone Normal: 11 4 2 1 0 0 ...
# order 0: 11 blocks (44KB free as 4KB pages)
# order 1: 4 blocks (32KB free as 8KB chunks)
# Zone information
# Contains: per-zone watermarks, free pages, protection
# NUMA statistics
# Memory fragmentation index
# 1.000 = Fully fragmented, unable to allocate contiguous pages
# 0.000 = No fragmentation
# Triggered reclaim statistics
|
References and Further Reading
- Kernel Documentation:
Documentation/mm/page_alloc.rst,Documentation/admin-guide/sysctl/vm.rst - LWN:
- "The design of the buddy allocator" (lwn.net/Articles/259832/)
- "Memory management APIs" series
- Source Files:
mm/page_alloc.c— buddy allocator (~7000 lines)include/linux/mmzone.h— zone, pg_data_t, free_areainclude/linux/mm_types.h— struct pageinclude/linux/gfp_types.h— GFP flags
Keywords: NUMA, Zone, Buddy Allocator, GFP_KERNEL, GFP_ATOMIC, struct page, watermark, kswapd, memory hotplug, compaction