How Does the Linux Kernel Split and Merge Pages? – Linux Device Drivers Course Online


← Previous Lecture

Next Lecture →

Buddy System Page Allocator in the Linux Kernel
Free Linux Kernel Development Course | Free Linux Device Drivers Course | EmbeddedPathashala
Level
Beginner to Intermediate
Topic
Kernel Memory Management
Updated For
Modern Linux Kernels (6.x)

free linux kernel development course
free linux device drivers course
free embedded systems course
buddy system page allocator
kernel memory management

If you are following our free Linux kernel development course, this lecture explains the buddy system page allocator — the lowest-level physical memory allocator inside the Linux kernel. Every byte of RAM a device driver, a process, or the kernel itself ever uses is ultimately handed out through this mechanism. Understanding it is essential for anyone serious about a free Linux device drivers course or a free embedded systems course, because memory bugs in drivers almost always trace back to a misunderstanding of how pages are allocated and freed.

What You Will Learn

Why the kernel allocates memory in pages, not bytes
What a node and a zone are
How the free-list “order” system works
Splitting and merging of buddy blocks
Internal fragmentation and how to avoid it
Modern kernel APIs you should actually use
Common mistakes driver developers make

Prerequisites

Before this lecture you should be comfortable with:

Basic C programming
Virtual vs physical memory concept
Building and booting a custom kernel
Basics of kernel modules (LKM)

Why Memory Is Allocated in Pages

The CPU’s Memory Management Unit (MMU) does not understand “bytes” the way user-space programmers think about malloc(). The MMU works with fixed-size chunks called pages, almost always 4 KB on most architectures (some platforms support larger base page sizes). Because of this hardware reality, the Linux kernel’s lowest level allocator — the buddy system page allocator (often called the Buddy System Allocator or BSA) — only ever hands out memory in whole pages, and more specifically, in power-of-2 groups of pages.

This single design decision drives everything else in this lecture: how free lists are organized, how splitting and merging happen, and why certain allocation sizes are wasteful.

Node, Zone, and the Concept of an “Order”

On any real system, physical memory is logically divided into nodes (each node usually maps to a physical memory bank, especially important on NUMA systems with multiple CPU sockets) and within each node, into zones (such as ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM on 32-bit systems, or ZONE_MOVABLE on modern kernels). Every distinct node:zone combination on the system maintains its own independent set of free lists.

Each free list is indexed by what the kernel calls an order. An order simply tells you how many contiguous pages are grouped together as one chunk:

Order vs Chunk Size (Page Size = 4 KB)
Order 0
1 page
4 KB
Order 1
2 pages
8 KB
Order 2
4 pages
16 KB
Order 5
32 pages
128 KB
Order 10
1024 pages
4 MB (typical max)

The highest usable order is bounded by a kernel constant. It is worth noting that this constant’s meaning changed in kernel 6.5: previously MAX_ORDER was the highest valid order itself, but from 6.5 onward it represents an exclusive upper bound (the highest valid order is now MAX_ORDER - 1). If you are reading older training material or an older textbook, keep this version detail in mind so your math doesn’t end up off by one order.

How an Allocation Request Actually Flows

Picture a driver asking the kernel for some pages. Conceptually, here is the order-search logic the allocator follows:

Buddy Allocator Search Flow
Compute required order from size
↓
Check free list at that order
↓
Chunk found → allocate & return
List empty → check next higher order
↓
Split larger chunk into two buddies, keep one, queue the other

In plain language: the allocator picks the smallest order list that satisfies the request. If that exact list is empty, it borrows from the next bigger list and splits that chunk into two equal “buddy” halves — one half goes to the requester, the other half is queued back onto the lower order list for future use. If even the bigger lists are empty, the kernel may trigger memory reclaim or compaction before failing the request.

The Power of Merging (Coalescing)

The real elegance of this algorithm shows up at free time, not allocation time. When a chunk is freed, the kernel checks whether the chunk’s “buddy” (its size-matched, address-adjacent neighbour) is also free. If it is, the two are merged back into one larger chunk and pushed up to the next order’s list. This process can repeat several times, naturally healing fragmentation without any separate defragmentation pass — a property that makes the buddy system genuinely elegant compared to a flat free-list allocator.

Internal Fragmentation: The Honest Downside

Because the allocator only deals in power-of-2 sized chunks, an odd-sized request gets rounded up. A request for slightly more than 128 KB, for example, is satisfied from the 256 KB order list, wasting close to half the allocated memory. This waste is called internal fragmentation, and it is the well-known weak point of any binary buddy allocator.

Requested Size Order Used Actual Allocation Wasted
128 KB 5 128 KB 0%
132 KB 6 256 KB ~48%
5 MB (jumbo request) N/A Rejected by buddy allocator Use vmalloc() instead

Modern Kernel APIs You Should Actually Call

You will rarely call the buddy allocator directly. Instead, modern kernel code uses well-defined wrapper APIs:

struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
void        *page_address(struct page *page);
unsigned long get_free_page(gfp_t gfp_mask);
void         __free_pages(struct page *page, unsigned int order);

For odd-sized requests where you want the kernel to round up internally but free exactly what you used (avoiding the fragmentation problem described above), use:

void *alloc_pages_exact(size_t size, gfp_t gfp_mask);
void  free_pages_exact(void *virt, size_t size);

In day-to-day driver work, you usually won’t touch the page allocator at all — instead you reach for kmalloc() for small physically contiguous allocations (which itself is built on top of the slab/slub allocator, which in turn gets its backing pages from the buddy allocator) or vmalloc() for large virtually-contiguous allocations. This layered design — buddy allocator at the bottom, slab/slub in the middle, kmalloc/vmalloc as the driver-facing API — is one of the most important architectural facts to internalize in this free Linux kernel development course.

NUMA Awareness

On multi-socket servers, accessing memory attached to a remote CPU socket is slower than accessing local memory. Because every node maintains its own zone free lists, the buddy allocator naturally prefers the free list belonging to the node the requesting thread is currently running on. Only if that node is exhausted does the kernel fall back to other nodes, following a per-node fallback order maintained internally by the memory management subsystem.

Real-World Use Cases

DMA buffer allocation in device drivers
Page cache backing for file I/O
Huge page reservation for databases and VMs
Network driver receive/transmit ring buffers
Backing store for the slab/slub allocator itself

Common Mistakes and Troubleshooting

Mistake Why It Hurts Fix
Requesting a huge order directly via alloc_pages() High orders frequently fail under memory pressure Use vmalloc() for large, non-contiguous-OK buffers
Forgetting matching free order Memory leaks or corruption of neighbouring buddies Always free with the exact same order used at allocation
Ignoring GFP flags context (atomic vs sleepable) Allocation inside interrupt context can deadlock Use GFP_ATOMIC in non-sleepable contexts, GFP_KERNEL otherwise

Best Practices

Prefer kmalloc/vmalloc unless you have a specific page-level reason
Use alloc_pages_exact() for odd, non-power-of-2 sizes
Always check return values for NULL
Free memory in the same context type it was allocated
Profile with /proc/buddyinfo when debugging fragmentation

Performance and Security Considerations

Performance: Splitting and merging are O(log n) operations relative to the order, making the buddy allocator predictable even under load — a major reason it has remained the kernel’s core allocator for decades. Security: Improperly sized or unchecked allocations can lead to heap-adjacent memory disclosure or corruption in driver code; always validate sizes coming from user space before passing them into any allocation path.

Summary / Key Takeaways

Buddy allocator hands out memory in power-of-2 page chunks
Each node:zone has its own independent free lists by order
Splitting happens on allocation, merging happens on free
Odd-sized requests cause internal fragmentation
Use alloc_pages_exact() or kmalloc/vmalloc in real driver code

Conclusion

The buddy system page allocator is the silent foundation underneath every memory allocation in the Linux kernel. As part of this free Linux kernel development course and free Linux device drivers course, mastering its order-based free lists, the split/merge dance, and where it fits relative to slab/slub and vmalloc will make every future memory management lecture in this series click into place much faster.

FAQ

Q1. Why does the kernel use power-of-2 sized chunks?
It makes splitting and merging mathematically simple and fast, since every chunk can be cleanly halved or doubled.

Q2. What is a “buddy” exactly?
Two same-size, physically adjacent chunks produced by splitting one larger chunk are called buddies of each other.

Q3. What changed with MAX_ORDER in newer kernels?
Since kernel 6.5, MAX_ORDER became an exclusive bound rather than the highest valid order itself — double check this when reading older references.

Q4. Should drivers call alloc_pages() directly?
Usually no — kmalloc() and vmalloc() are the recommended driver-facing APIs; reach for alloc_pages() only for page-level control.

Q5. What is internal fragmentation?
Wasted memory that occurs when a request is rounded up to the next power-of-2 chunk size.

Q6. How can I inspect free list state on a running system?
Read /proc/buddyinfo, which shows free chunk counts per order per zone.

Q7. Does NUMA change how the buddy allocator behaves?
Yes — each node maintains independent free lists, and the allocator prefers the requesting thread’s local node first.

Q8. What replaces the buddy allocator for huge contiguous DMA buffers?
CMA (Contiguous Memory Allocator) is commonly layered on top for large contiguous DMA regions.


← Previous Lecture
Next Lecture →

Leave a Reply

Your email address will not be published. Required fields are marked *