Memory Compaction

Memory compaction is the Linux kernel mechanism that defragments physical memory: it migrates movable pages around so that scattered free 4 KiB pages coalesce into physically-contiguous, naturally-aligned high-order blocks the buddy allocator can hand out. Its raison d’être is the external-fragmentation problem — a box can have gigabytes free yet fail a single order-9 (2 MiB) huge-page allocation because no 512 contiguous free pages exist. Compaction solves this by running two scanners across a memory zone — a migrate scanner sweeping upward from the bottom collecting movable pages, and a free scanner sweeping downward from the top collecting free pages as migration targets — relocating the movable pages into the free holes at the top so that the bottom of the zone empties into large free runs. It runs reactively (direct compaction, in the failing allocator’s own context), in the background (the per-node kcompactd thread), and preemptively (proactive compaction, governed by compaction_proactiveness). All facts here are pinned to Linux 6.12 LTS (released 2024-11-17), verified against the v6.12 source tree.


Mental Model — Two Scanners Meeting in the Middle

The defining image of compaction is a single zone scanned from both ends simultaneously. The migrate scanner starts at the zone’s low PFN (page frame number) and walks toward higher addresses, picking up pages that are movable — page-cache pages, anonymous pages on the LRU, and other relocatable folios. The free scanner starts at the zone’s high PFN and walks downward, picking up free pages to use as destinations. Each batch of isolated movable pages is migrated (via Page Migration) into the free pages the free scanner found. Because movable content keeps getting copied toward the top while the bottom is emptied, the net effect is to push all the in-use movable pages to one end of the zone and leave a large contiguous free run at the other end. The pass terminates when the two scanners cross — there is nothing left between them to compact.

flowchart LR
  subgraph ZONE["A memory zone (PFN low → high)"]
    direction LR
    M["migrate scanner<br/>starts at cc->migrate_pfn (bottom)<br/>moves UP, isolates MOVABLE pages"]
    GAP["...pages migrated from<br/>bottom into top holes..."]
    F["free scanner<br/>starts at cc->free_pfn (top)<br/>moves DOWN, isolates FREE pages"]
    M -->|"scan up"| GAP
    F -->|"scan down"| GAP
  end
  M -.->|"meet → compact_scanners_met()<br/>(free_pfn >> pageblock_order)<br/><= (migrate_pfn >> pageblock_order)"| F
  GAP --> RESULT["large contiguous<br/>free block at the top<br/>(now order-9 allocatable)"]

The two-scanner algorithm of mm/compaction.c. What it shows: the migrate scanner (cc->migrate_pfn) advances upward isolating movable pages while the free scanner (cc->free_pfn) advances downward isolating free pages; movable content is migrated into the free slots, draining the bottom of the zone. The insight: compaction does not create free memory — total free pages are unchanged — it merely rearranges movable pages so the free pages it already has become physically contiguous and aligned, which is the only thing the buddy allocator needs to satisfy a high-order request. The pass ends the instant the scanners meet, checked by compact_scanners_met(): (cc->free_pfn >> pageblock_order) <= (cc->migrate_pfn >> pageblock_order).


Why Compaction Exists — External Fragmentation

The buddy allocator satisfies a request for 2^order pages only from a free block of at least that order. Over time, as pages are allocated and freed in arbitrary patterns, the free pages that remain become externally fragmented: plenty of them exist, but they are scattered as isolated order-0 (single) pages interleaved with allocated ones. A request for an order-9 block (512 pages = 2 MiB, the THP size on x86-64) then fails despite abundant free memory, because no run of 512 aligned contiguous free pages survives. See Page Order and Fragmentation for the buddy allocator’s anti-fragmentation grouping (MIGRATE_MOVABLE / MIGRATE_UNMOVABLE / MIGRATE_RECLAIMABLE pageblocks), which is the prevention strategy; compaction is the cure applied once fragmentation has already happened.

The key enabler is that a large class of pages are movable: their physical location is an implementation detail the kernel can change transparently by copying the contents to a new frame and fixing up every mapping that points at the old frame. The allocator segregates these into MIGRATE_MOVABLE pageblocks precisely so that compaction has a contiguous supply of relocatable pages to work with. Unmovable allocations — kernel slab objects, page tables, anything the kernel has handed out a raw physical address for — cannot be moved and act as immovable obstacles that limit how much a region can be defragmented.

Compaction was merged into mainline around Linux 2.6.35 (2010, Mel Gorman’s work) as the in-kernel successor to the older “lumpy reclaim” approach. As of 6.12 it is gated by CONFIG_COMPACTION, which is enabled in essentially all general-purpose distribution kernels.

Uncertain

Verify: the exact release (2.6.35) in which page compaction first merged. Reason: stated from general kernel history, not pinned to a primary changelog during this task. To resolve: check the merge commit / git log for mm/compaction.c introduction. This is a historical-provenance detail; the 6.12 behavior above is source-verified. uncertain


Mechanical Walk-through — How a Compaction Pass Runs

The core routine in mm/compaction.c is compact_zone(), which orchestrates one zone’s pass through a struct compact_control (referred to as cc throughout the code). It holds the two scanner cursors cc->migrate_pfn and cc->free_pfn, the lists of isolated migrate and free pages, the migration mode (async / sync-light / sync), and the target order.

The migrate scanner is isolate_migratepages() (and the per-block isolate_migratepages_block()). Starting from cc->migrate_pfn, it walks pageblock by pageblock looking for movable pages, isolating them from their LRU onto cc->migratepages. It isolates in batches: the code stops accumulating once cc->nr_migratepages >= COMPACT_CLUSTER_MAX (line 1252 / 1383 in compaction.c), so a single migration call moves at most COMPACT_CLUSTER_MAX pages before the scanner yields and the migration is performed. COMPACT_CLUSTER_MAX is defined in include/linux/swap.h as #define COMPACT_CLUSTER_MAX SWAP_CLUSTER_MAX, and SWAP_CLUSTER_MAX is 32UL — so compaction works in batches of 32 pages, which also bounds how long a lock is held before being dropped (the if (!(low_pfn % COMPACT_CLUSTER_MAX)) checks at lines 617/945 periodically release the per-zone lock and check for the need to reschedule).

The free scanner is isolate_freepages(). Starting from cc->free_pfn near the top of the zone, it walks downward (low_pfn = pageblock_end_pfn(cc->migrate_pfn) bounds it from below) isolating free pages from the buddy lists onto cc->freepages, to be used as migration destinations.

Migration is the call that actually moves data:

migrate_pages(&cc->migratepages, compaction_alloc, compaction_free,
              (unsigned long)cc, cc->mode, MR_COMPACTION, NULL);

compaction_alloc is a custom allocator callback that hands migrate_pages a destination from cc->freepages (the pages the free scanner gathered), and compaction_free returns unused ones. The actual page-copy-and-remap mechanism — unmapping every PTE, copying contents, installing migration entries, then remapping — lives in Page Migration and is not re-explained here; compaction is simply one of its consumers (alongside Memory Hotplug offlining and NUMA balancing).

Termination. After each migration batch the scanners are advanced and compact_scanners_met() (line 1453) is checked:

return (cc->free_pfn >> pageblock_order)
    <= (cc->migrate_pfn >> pageblock_order);

Both PFNs are right-shifted by pageblock_order so the comparison is at pageblock granularity — the pass finishes when the migrate scanner (rising) reaches the same pageblock as the free scanner (falling). On x86-64 with THP enabled, pageblock_order equals HPAGE_PMD_ORDER (9, i.e. 512 pages / 2 MiB), per include/linux/pageblock-flags.h: #define pageblock_order MIN_T(unsigned int, HPAGE_PMD_ORDER, MAX_PAGE_ORDER). A pageblock is therefore the natural alignment unit compaction is trying to free up.

Migration modes — async vs sync

cc->mode is one of three migration modes that trade latency for thoroughness:

  • MIGRATE_ASYNC — never blocks. It skips pages whose locks it cannot grab immediately and bails out of pageblocks it cannot quickly process. This is what a latency-sensitive direct-compaction attempt uses first.
  • MIGRATE_SYNC_LIGHT — may block on most operations but avoids waiting on writeback of dirty pages.
  • MIGRATE_SYNC — will wait on everything, including page writeback. Most thorough, highest latency.

Direct compaction typically tries async first and escalates to sync only if the cheaper attempt fails, which is why a stressed huge-page allocation can produce a visible latency spike.


Direct Compaction — Reactive, in the Allocator’s Context

When the buddy allocator’s fast path fails a high-order allocation, the slow path may invoke direct compaction synchronously in the failing task’s own context — analogous to Direct Reclaim but for fragmentation rather than for free-page shortage. The entry point is compact_zone_order()try_to_compact_pages(). This is exactly the path the THP allocator takes when its defrag policy says to compact-on-fault.

The capture mechanism

A subtlety: a naive direct-compaction pass might free a perfect high-order block, only to have another CPU steal it from the buddy free lists before the original caller can grab it. The kernel closes this race with capture. In compact_zone_order():

struct capture_control capc = { .cc = cc, .page = NULL };
...
WRITE_ONCE(current->capture_control, &capc);

A struct capture_control is attached to the current task. When compaction frees a block of the requested order, the buddy free path checks for a pending capture_control matching that order and, instead of putting the page on the free list, stuffs it directly into capc->page. Back in compact_zone_order() the logic is if (*capture) ret = COMPACT_SUCCESS; — even if compaction would otherwise report incomplete, a successful capture is reported as success because the caller already holds the page it wanted. This guarantees the task that did the work of compacting is the one that gets the resulting block.


kcompactd — The Per-Node Background Thread

Doing all compaction synchronously in the allocation path would stall whoever happens to trigger it. So the kernel also runs kcompactd, one kernel thread per NUMA node (kcompactd0, kcompactd1, …), defined by the kcompactd() function in compaction.c. Its main loop is:

while (!kthread_should_stop()) {
    ...
    wait_event_freezable_timeout(pgdat->kcompactd_wait,
        kcompactd_work_requested(pgdat), timeout);
    ...
    kcompactd_do_work(pgdat);
}

It sleeps on a wait queue until woken, then calls kcompactd_do_work(), which compacts the node’s zones up to pgdat->kcompactd_max_order / pgdat->kcompactd_highest_zoneidx. It is woken by wakeup_kcompactd(), called when a high-order allocation would benefit — most importantly when kswapd finishes a reclaim pass: rather than have the allocating task block on direct compaction, the kernel hands the deferred work to kcompactd so the next allocation finds the contiguous block already prepared. This is the “defer” half of the THP defrag policy.


Proactive Compaction — compaction_proactiveness

Reactive compaction is, by definition, late: it only acts once an allocation has already failed (or once kswapd ran). For workloads that allocate huge pages in bursts — virtual machines being booted, databases reserving large regions — that lateness shows up as multi-millisecond allocation latency. Proactive compaction (merged in Linux 5.9, 2020; see the LWN proposal) makes kcompactd compact before the demand arrives, keeping fragmentation low so that high-order allocations stay cheap.

The single tunable is /proc/sys/vm/compaction_proactiveness, an integer in 0–100, default 20 (per the vm sysctl doc and sysctl_compaction_proactiveness = 20 at line 1923 of compaction.c). 0 disables proactive compaction entirely; writing a non-zero value triggers an immediate proactive pass. The doc notes that “values above 80 make fragmentation levels more stable but increase compaction frequency.”

Uncertain

Verify: the original LWN write-up describes the interface as /sys/kernel/mm/compaction/proactiveness, but the merged-and-current interface (per the v6.12 vm sysctl doc) is the sysctl /proc/sys/vm/compaction_proactiveness. The sysctl path is the one to use on 6.12; the /sys/kernel/mm/... path from the proposal may not exist. Reason: proposal text predates the merged form. To resolve: check ls /proc/sys/vm/compaction_proactiveness on a 6.12 box. uncertain

The fragmentation score

kcompactd decides when to act using a per-node fragmentation score. fragmentation_score_zone() computes a per-zone external-fragmentation measure relative to the huge-page order; fragmentation_score_zone_weighted() weights it by the zone’s share of node memory; and fragmentation_score_node() sums the weighted zone scores into a single 0–100 node score. The score is checked on an interval defined as #define HPAGE_FRAG_CHECK_INTERVAL_MSEC (500) (line 33) — every 500 ms.

The thresholds are derived directly from the tunable (in compaction.c):

wmark_low = max(100U - sysctl_compaction_proactiveness, 5U);
wmark_high = min(wmark_low + 10, 100U);

With the default proactiveness of 20: wmark_low = max(80, 5) = 80 and wmark_high = min(90, 100) = 90. Compaction starts when the fragmentation score rises above the high watermark and continues until it falls below the low watermark — a hysteresis band that prevents the daemon from oscillating on and off. Raising compaction_proactiveness lowers both watermarks (e.g. proactiveness 50 gives wmark_low = 50), making the daemon intervene at lower fragmentation and thus more often. A proactive pass uses compact_node(pgdat, proactive=true) with order = -1 (no specific target order — just reduce fragmentation generally).


The compact_memory sysctl — Manual, Whole-System Compaction

For one-shot manual defragmentation there is /proc/sys/vm/compact_memory (available under CONFIG_COMPACTION). Writing 1 to it triggers synchronous compaction of all zones on all nodes. The handler sysctl_compaction_handler() calls compact_nodes(), which does lru_add_drain_all() (to flush per-CPU LRU pagevecs so pages are actually visible to the migrate scanner) and then compact_node(NODE_DATA(nid), false) for every online node:

echo 1 | sudo tee /proc/sys/vm/compact_memory   # compact everything, now

This is a blunt instrument — it does a full sync compaction pass and can take noticeable time on a large, fragmented machine — but it is the standard way to manually prepare for a large huge-page reservation or to test compaction behavior.

A related knob, /proc/sys/vm/extfrag_threshold (default 500, range 0–1000), governs whether the kernel chooses compaction or direct reclaim for a high-order allocation: the kernel consults a per-zone fragmentation index and “skips compaction if the fragmentation index is ≤ threshold,” falling back to reclaim instead (a low index means the failure is due to too little free memory rather than fragmentation, so reclaim is the right tool). And /proc/sys/vm/compact_unevictable_allowed (default 1, but 0 on CONFIG_PREEMPT_RT) controls whether compaction may migrate mlocked / unevictable pages; allowing it improves contiguity at the cost of minor page-fault stalls for the affected mappings.


Interaction with THP and Huge-Page Allocation

Compaction’s single most important consumer is huge-page allocation. The THP defrag policy (/sys/kernel/mm/transparent_hugepage/defrag) decides how hard a faulting THP allocation tries to compact (verified against huge_memory.c at the v6.12 tag):

  • always — the faulting task does direct compaction (and reclaim) and stalls until a huge page is obtained or the attempt definitively fails.
  • defer — never stall the fault; instead wake kswapd + kcompactd to prepare a huge page in the background and fall back to small pages for now.
  • defer+madvise — direct compaction for regions explicitly marked madvise(MADV_HUGEPAGE); background (defer) behavior for everything else.
  • madvise — direct compaction only for MADV_HUGEPAGE regions; this is the compiled-in default (the kernel sets TRANSPARENT_HUGEPAGE_DEFRAG_REQ_MADV_FLAG, which defrag_show() renders as [madvise]).
  • never — no compaction on THP allocation at all (though madvise(MADV_COLLAPSE) can still force a huge page).

This is why defrag=always can cause latency spikes (every THP fault may trigger a full sync compaction) while defrag=madvise confines that cost to applications that explicitly asked for huge pages. khugepaged, the daemon that retroactively collapses small pages into THPs, similarly leans on compaction to find contiguous targets.

For the deliberately-reserved variety — hugetlbfs gigantic pages — compaction matters even more, because order-18 (1 GiB) blocks essentially never exist on a fragmented system without it.


Observability — /proc/vmstat Counters

Compaction exposes its activity through /proc/vmstat (definitions per the THP admin guide and vm_event_item):

  • compact_stall — incremented each time a process stalls to run direct compaction so that a huge page becomes free for it. A high and rising count means workloads are paying the direct-compaction latency tax; consider defrag=defer or proactive compaction.
  • compact_success — incremented when compaction freed a huge page for use.
  • compact_fail — incremented when compaction ran but failed to produce the block.
  • compact_daemon_wake — number of times kcompactd was woken.
  • compact_migrate_scanned / compact_free_scanned — total pages examined by the migrate and free scanners respectively (cost metrics — large values relative to work done indicate the scanners are grinding through hard-to-compact regions).
  • compact_isolated — pages isolated for migration.
grep -E 'compact_' /proc/vmstat
# compact_migrate_scanned 1048576
# compact_free_scanned   2097152
# compact_isolated         32768
# compact_stall              412   # direct-compaction stalls — latency events
# compact_fail               118
# compact_success            294
# compact_daemon_wake       1551

A useful diagnostic ratio is compact_fail / (compact_success + compact_fail): a high failure rate means the zone is full of unmovable obstructions that no amount of scanning can defragment — the cure is then better anti-fragmentation grouping (Page Order and Fragmentation) or simply more RAM, not more compaction.


Failure Modes and Common Misunderstandings

“Compaction frees memory.” It does not. Compaction is zero-sum on free-page count — it only rearranges. If you are short on free pages, you need reclaim, not compaction. The two cooperate: reclaim creates free pages, compaction makes them contiguous.

Unmovable pages cap effectiveness. Kernel slab objects, page tables, and pages with elevated reference counts (e.g. those pinned by get_user_pages for DMA or RDMA) cannot be migrated. A pageblock containing even one such page can never be fully freed, which is why long-term pinning is so corrosive to high-order allocation. The MIGRATE_UNMOVABLE pageblock segregation exists to confine the damage.

Async compaction giving up early. MIGRATE_ASYNC mode deliberately skips anything it cannot process without blocking, and uses per-zone skip hints (PB_migrate_skip pageblock bits) to avoid re-scanning pageblocks that recently failed. This is why a direct THP allocation under defrag=madvise may fall back to small pages even though a slow sync pass would have succeeded — the kernel chose latency over the page.

Lock contention and the COMPACT_CLUSTER_MAX cadence. Both scanners hold the zone lock / LRU lock while isolating, and release every COMPACT_CLUSTER_MAX (32) pages. On large NUMA boxes with many concurrent allocators, this lock can become a bottleneck, visible as time spent in isolate_migratepages_block in a profile.

Proactive compaction interfering with workloads. Setting compaction_proactiveness high (>80) on a latency-sensitive box can cause background kcompactd to consume CPU and migrate pages (each migration is a TLB shootdown and a copy) at inopportune moments. The proactive-compaction patch includes back-off logic to yield under reclaim pressure, but the tunable is still a CPU-vs-fragmentation trade.


  • Anti-fragmentation grouping (Page Order and Fragmentation) — prevents fragmentation by segregating movable / unmovable / reclaimable allocations into separate pageblocks. Prevention; compaction is cure. They are complementary, not alternatives.
  • CMA (Contiguous Memory Allocator) — reserves a region up front from which only movable allocations are served, guaranteeing large contiguous blocks can be reclaimed on demand (used by device drivers needing big DMA buffers). An alternative when you need a guarantee rather than best-effort.
  • hugetlbfs pre-reservation (hugetlbfs and Reserved Huge Pages) — reserve huge pages at boot when memory is pristine and unfragmented, sidestepping the need to compact later. The deterministic alternative to relying on compaction at runtime.
  • More RAM / vm.min_free_kbytes — raising the free-memory floor keeps more headroom for the buddy allocator and reduces how often compaction is needed.

See Also