Allocators and Folios
Foundations taught you which allocator to call. This chapter is about how they work, because from here on you are reading and changing them rather than using them.
Three concepts in the six-part treatment: the buddy allocator, slab, and folios and the page cache.
Concept 1: The Buddy Allocator
1. What problem it solves
Physical memory must be handed out in page-sized and larger power-of-two chunks, quickly, from any CPU, with as little fragmentation as possible, and it must be possible to give a contiguous range to a device that needs one.
Those goals conflict. Handing out pages wherever they fit minimises waste and destroys contiguity; keeping large ranges intact wastes memory. The buddy allocator's answer is to make merging cheap enough to do eagerly.
2. Where it exists in the kernel
rg -n "__alloc_pages\b" -A 40 mm/page_alloc.c | head -50
rg -n "struct free_area \{" -A 6 include/linux/mmzone.h
rg -n "enum migratetype" -A 12 include/linux/mmzone.h
cat /proc/buddyinfo /proc/pagetypeinfo | head -20
3. The mechanism
FREE LISTS: one per ORDER, per MIGRATETYPE, per ZONE, per NODE.
order 0 [ 4 KB] ████████████████████ many
order 1 [ 8 KB] ██████████
order 2 [ 16 KB] ████
order 3 [ 32 KB] █
...
order N [max] (MAX_PAGE_ORDER -- grep it, the name changed in 6.x)
ALLOCATE order 2, none free:
take an order-3 block, split it into two order-2 "buddies",
hand one out, put the other on the order-2 list.
FREE an order-2 block:
is its buddy also free? (its address differs in exactly one bit --
that is why it is called a buddy, and why the check is O(1))
if yes: merge into an order 3. Then check THAT block's buddy. Recurse.
The O(1) buddy check is the whole trick: two blocks of order n are buddies if their page-frame numbers differ only in bit n. Merging is therefore a bit test, not a search, which makes eager merging affordable.
Migratetypes are the anti-fragmentation mechanism, and they are the part people miss:
| Migratetype | Means | Why it is grouped |
|---|---|---|
MOVABLE | The kernel can relocate this page (user anonymous, page cache) | Compaction can defragment a region full of these |
UNMOVABLE | Pinned: kernel data structures, DMA buffers | One of these in the middle of a 2 MB region blocks a huge-page allocation forever |
RECLAIMABLE | Can be freed under pressure (some slab caches) | Reclaim can defragment these |
Grouping by migratetype means unmovable allocations cluster together, leaving other regions defragmentable. This is why a machine with 20 GB free can fail to allocate 2 MB contiguous: the free memory is order-0 blocks scattered among unmovable ones.
cat /proc/pagetypeinfo | head -30 # free blocks by order AND migratetype
grep -E 'compact_' /proc/vmstat # compaction: attempts, successes, failures
4. Zones and nodes
NUMA node 0 NUMA node 1
ZONE_DMA32 (< 4 GB, for old devices)
ZONE_NORMAL (everything else) ZONE_NORMAL
ZONE_MOVABLE (only movable pages) ZONE_MOVABLE
An allocation has a ZONELIST: the preferred zone, then fallbacks in
order. The GFP flags and the NUMA policy decide the list.
cat /proc/zoneinfo | grep -E '^Node|^ pages free|^ min|^ low|^ high' | head -20
numactl --hardware 2>/dev/null || echo "single node"
The min/low/high watermarks are the control loop: crossing low wakes kswapd; crossing min
means an allocating task must reclaim synchronously (direct reclaim).
5. Experiment
CLAIM. Fragmentation is real, visible, and predicts high-order allocation failure better than free memory does.
METHOD. In a module:
for (order = 0; order <= 10; order++) {
struct page *p = alloc_pages(GFP_KERNEL | __GFP_NOWARN, order);
pr_info("order %2d (%5lu KB): %s\n", order,
(PAGE_SIZE << order) >> 10, p ? "ok" : "FAILED");
if (p)
__free_pages(p, order);
}
# Fresh boot:
cat /proc/buddyinfo; insmod ./orders.ko; dmesg | tail -12
# Now fragment memory deliberately, then repeat:
find / -type f -exec cat {} + > /dev/null 2>&1 & # fill the page cache
sleep 60; kill %1
cat /proc/buddyinfo; rmmod orders; insmod ./orders.ko; dmesg | tail -12
# And watch compaction try to fix it:
grep compact /proc/vmstat
echo 1 | sudo tee /proc/sys/vm/compact_memory
grep compact /proc/vmstat
cat /proc/buddyinfo
PREDICT FIRST: on a fresh boot, at which order does allocation start failing? After fragmentation? And does explicit compaction restore the high orders?
6. Failure mode
| Mistake | Symptom |
|---|---|
| Depending on a high-order allocation at runtime | Works at boot, fails after a week of uptime |
Reading MemFree to predict allocation success | Free memory says nothing about contiguity |
Not passing __GFP_NOWARN on an allocation you handle | A page of stack trace in dmesg on every miss |
| Mismatched order on free | Free-list corruption; the machine dies later, elsewhere |
| Allocating unmovable memory in bulk | You permanently fragment regions for everyone else |
| Assuming compaction always works | It cannot move pinned or unmovable pages |
Concept 2: Slab
1. What problem it solves
The page allocator's granularity is a page. Most kernel objects are 32–512 bytes and there are millions of them, allocated and freed constantly, often on the hot path of a syscall.
Slab sits on top of the page allocator and provides: sub-page granularity, per-CPU caches so the fast path takes no lock, and type-specific caches whose objects share a page and stay cache-warm.
2. Where it exists in the kernel
ls mm/slub.c mm/slab_common.c
grep -E 'CONFIG_SLUB|CONFIG_SLAB\b|CONFIG_SLOB' ~/kernel/build/.config
rg -n "kmem_cache_alloc\b" -A 25 mm/slub.c | head -35
head -3 /proc/slabinfo; sudo slabtop -o | head -20
SLUB is the only allocator now — SLOB was removed in 6.4 and SLAB in 6.8. Anything written about choosing between them is stale.
3. The structure
kmem_cache "dentry" (one per object type)
├── object size, alignment, constructor
└── per-CPU:
├── the ACTIVE SLAB: a page of objects, with a freelist
│ Allocation: pop the head of this CPU's freelist.
│ NO LOCK. This is the fast path, and it is a few ns.
└── a partial list, for when the active slab is exhausted
└── per-NODE: partial slabs, and the path to the page allocator
kmalloc(n) picks a size class:
kmalloc-8, -16, -32, ..., -8k (powers of two)
A 33-byte allocation uses kmalloc-64. THIRTY-ONE BYTES ARE WASTED.
Above the largest cache, kmalloc falls through to the page allocator.
grep -E '^kmalloc-' /proc/slabinfo | head -12
rg -n "KMALLOC_MAX_CACHE_SIZE|kmalloc_caches" include/linux/slab.h | head
The internal fragmentation of power-of-two classes is why a named cache matters for a hot object:
/* 1,000,000 of these via kmalloc: 96 bytes each in kmalloc-128 → 32 MB wasted */
struct thing { u64 a, b, c, d, e, f, g, h, i, j, k, l; }; /* 96 bytes */
/* With a named cache: exactly 96 bytes, packed, and visible in slabtop */
cache = kmem_cache_create("mylab_thing", sizeof(struct thing), 0,
SLAB_HWCACHE_ALIGN, NULL);
Flags worth knowing:
| Flag | Effect |
|---|---|
SLAB_HWCACHE_ALIGN | Align to a cache line — for objects touched from several CPUs |
SLAB_ACCOUNT | Charge to the allocating task's memory cgroup |
SLAB_TYPESAFE_BY_RCU | Freed objects may be reused but the memory stays this type, so an RCU reader that races with a free sees a valid object of the right type — it must re-validate identity. Subtle and powerful. |
SLAB_POISON, SLAB_RED_ZONE | Debug: fill freed memory, guard the edges |
4. Experiment
CLAIM. The slab fast path is lock-free and per-CPU, and internal fragmentation is measurable.
METHOD.
/* In a module: time kmalloc/kfree round trips, and compare a named cache. */
u64 t0 = ktime_get_ns();
for (i = 0; i < 1000000; i++) kfree(kmalloc(96, GFP_KERNEL));
pr_info("kmalloc(96) x1e6: %llu ns\n", ktime_get_ns() - t0);
cache = kmem_cache_create("labthing", 96, 0, 0, NULL);
t0 = ktime_get_ns();
for (i = 0; i < 1000000; i++) kmem_cache_free(cache, kmem_cache_alloc(cache, GFP_KERNEL));
pr_info("kmem_cache(96) x1e6: %llu ns\n", ktime_get_ns() - t0);
PREDICT FIRST: nanoseconds per kmalloc/kfree pair. Then measure the memory difference:
grep -E '^kmalloc-128|^labthing' /proc/slabinfo
# columns: active_objs num_objs objsize objperslab pagesperslab ...
PREDICT FIRST: for 1,000,000 live 96-byte objects, how much memory does kmalloc-128 use versus
a named 96-byte cache? Compute it, then allocate them and check.
5. Failure mode
| Mistake | Symptom |
|---|---|
kmalloc for a hot object of an awkward size | Up to 2× memory waste, invisible until you look at slabinfo |
| No named cache for a heavily-allocated type | slabtop shows kmalloc-N and you cannot tell who is responsible |
Forgetting kmem_cache_destroy on module unload | "cache still has objects" and a leak |
| Freeing to the wrong cache | Corruption; CONFIG_SLUB_DEBUG catches it |
Using SLAB_TYPESAFE_BY_RCU without re-validating identity | The object is the right type but a different instance. A real and subtle bug class. |
Assuming ksize() bytes are yours | They are, but relying on it hides the real size |
Concept 3: Folios and the Page Cache
1. What problem it solves
The page cache is most of your RAM. It holds file contents so a second read costs microseconds
instead of milliseconds, and it absorbs writes so a write() can return before the disk has seen
anything.
Folios solve an internal problem: struct page was used to mean both "one 4 KB page" and "the
head of a multi-page compound allocation", and enormous amounts of code could not tell which it had.
Passing a tail page to a function expecting a head was a whole bug class. A struct folio is by
construction a head, so the bug is unrepresentable.
2. Where it exists in the kernel
rg -n "struct folio \{" -A 30 include/linux/mm_types.h
rg -n "filemap_read\b|filemap_get_pages" -A 30 mm/filemap.c | head -40
rg -n "struct address_space \{" -A 25 include/linux/fs.h
git log --oneline --grep="folio" -- mm/ | head -20
ls Documentation/mm/
3. The structure
struct file ──▶ struct inode ──▶ struct address_space
├── i_pages: an XArray
│ file offset (in pages) -> folio
├── a_ops: address_space_operations
│ read_folio, writepages,
│ dirty_folio, invalidate_folio...
└── host: back to the inode
struct folio
├── flags: uptodate | dirty | writeback | locked | lru | referenced
├── mapping ──▶ the address_space (or NULL/anon for anonymous)
├── index the offset within that mapping
├── _refcount who holds a reference
└── order a folio is 2^order pages, treated as ONE unit
The flags are a state machine, and reading them is how you reason about I/O:
| Flag | Means |
|---|---|
locked | Someone is operating on it. Others wait. |
uptodate | The contents match the backing store; safe to read |
dirty | Modified in memory, not yet written back |
writeback | Currently being written to the device |
referenced / active | Reclaim's aging information |
4. The two paths
READ WRITE
──── ─────
filemap_read() generic_perform_write()
└── look up the folio └── find or create the folio
├── present + uptodate └── copy from user
│ copy_to_user. MINOR. └── mark it DIRTY
│ A few microseconds. └── RETURN. The data is
└── absent NOT on disk.
├── allocate a folio
├── a_ops->read_folio() ...later, asynchronously:
│ submit I/O writeback (per-bdi threads)
├── SLEEP on the folio lock └── a_ops->writepages()
└── woken when uptodate └── clear dirty, set
copy_to_user. MAJOR. writeback, submit I/O
That "RETURN" on the write side is why fsync() exists and is slow, and why a filesystem's
crash-consistency design is complicated: the page cache decides when data reaches the disk, and
journaling exists to make "some of it did" a recoverable state.
Readahead is the other half of read performance: the kernel detects sequential access and fetches ahead, so most "reads" of a sequentially-read file are minor faults on pages already in flight.
rg -n "page_cache_ra_unbounded|ondemand_readahead" mm/readahead.c | head
cat /sys/block/*/queue/read_ahead_kb 2>/dev/null | head -3
grep -E 'Dirty|Writeback' /proc/meminfo
cat /proc/sys/vm/dirty_ratio /proc/sys/vm/dirty_background_ratio
5. Experiment
CLAIM. The page cache is visible, controllable, and dominant; and folio order is observable.
METHOD.
dd if=/dev/urandom of=/tmp/f bs=1M count=512 status=none
sync; sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
grep -E '^Cached' /proc/meminfo
sudo perf stat -e major-faults,minor-faults -- cat /tmp/f > /dev/null # cold
grep -E '^Cached' /proc/meminfo
sudo perf stat -e major-faults,minor-faults -- cat /tmp/f > /dev/null # warm
# Which pages of the file are resident, right now:
sudo bpftrace -e 'tracepoint:filemap:mm_filemap_add_to_page_cache { @[comm] = count(); }' &
cat /tmp/f > /dev/null; kill %1
# Watch the write side NOT reach the disk:
grep -E 'Dirty|Writeback' /proc/meminfo
dd if=/dev/zero of=/tmp/w bs=1M count=256 status=none
grep -E 'Dirty|Writeback' /proc/meminfo # look at Dirty immediately
sync
grep -E 'Dirty|Writeback' /proc/meminfo
PREDICT FIRST: the cold/warm ratio; and how large Dirty gets after writing 256 MB, and how long
before it returns to near zero on its own.
Then folio order:
# Large folios mean fewer, bigger units. Where supported, this shows up as
# higher-order allocations in the cache.
grep -E 'AnonHugePages|FilePmdMapped' /proc/meminfo
sudo bpftrace -e 'kretprobe:filemap_alloc_folio { @order = hist(retval ? 1 : 0); }' 2>/dev/null | head
6. Failure mode
| Mistake | Symptom |
|---|---|
| Benchmarking without dropping caches | You measured the page cache |
Assuming write() is durable | Data loss on power failure. The classic application bug. |
fsync() on every write | Correct and enormously slow; batching is the whole art |
| Passing a tail page where a head was expected | The bug class folios eliminate — and you can still write it with the old APIs |
New code using struct page where folio APIs exist | A review comment, and a conversion someone else has to write |
| Assuming one folio is one page | It is 2^order pages. folio_nr_pages(). |
| Reading a folio's contents without the lock or an uptodate check | Torn or absent data |
Validation / Self-check
- Why are two blocks called "buddies", and what makes the merge check O(1)?
- What are migratetypes for? Explain how a machine with 20 GB free fails a 2 MB contiguous allocation.
- What do the
min/low/highwatermarks control, and what does crossing each one trigger? - Why is the slab fast path lock-free? What makes that possible?
kmalloc(96)for a million objects: how much memory is wasted, and what would you do instead?- What does
SLAB_TYPESAFE_BY_RCUguarantee, and what must the reader still do? - What is a folio, and what class of bug did it make unrepresentable?
- Name five folio flags and say what each tells you about the I/O state.
- Trace a cold
read()and a warm one throughfilemap_read, naming where each sleeps. - Why does
write()return before the data is on disk, and what are the two consequences? - Which allocator is
kvmallocand when should you use it? What may you never do with the result? - Which is the better predictor of a high-order allocation succeeding:
MemFreeor/proc/buddyinfo? Why?
Next: Reclaim, cgroups, and OOM — what happens when there is not enough.