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). Mechanism and code here are pinned to Linux 6.12 LTS (released 2024-11-17) and verified against the v6.12 source tree; runtime numbers are measured on a live machine and labelled as such — see the callout below.

Version pin and measurement provenance

Source citations are read from the v6.12 tag of torvalds/linux. Linux 6.12 (released 2024-11-17) is a maintained long-term-support branch, not mainline — mainline is on the 7.x series as of this writing (2026-09) — and it is pinned here precisely because it is the version most production distributions are actually running. Where a mechanism was introduced in a different release, that release is named and verified by an existence check (fetching the same path at successive tags and recording HTTP 200 versus 404), not asserted from memory.

Numbers labelled “on the measurement box” were sampled live on 2026-09-04 from a Fedora 44 workstation running kernel 7.1.8-200.fc44.x86_64: x86-64, 125 GiB RAM, a single NUMA node, 14 days 16 hours of uptime, compaction_proactiveness=20, THP enabled=[madvise], no hugetlbfs reservations. They exist to make the arithmetic concrete and to show the shape of real counters; where the running 7.1 kernel could differ from the 6.12 code being quoted, that is called out.


Mental Model — Two Scanners Meeting in the Middle

The defining image of compaction is a single memory zone scanned from both ends at once. The migrate scanner starts at the zone’s low PFN — page frame number, the index of a physical 4 KiB frame — 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 serve as destinations. Each batch of isolated movable pages is then migrated (via Page Migration) into the free pages the free scanner gathered. Because movable content keeps being copied toward the top while the bottom is emptied, the net effect is to push the in-use movable pages to one end of the zone and leave a large contiguous free run at the other.

Jonathan Corbet’s original write-up of Mel Gorman’s patch set describes exactly this shape (LWN, January 2010): “This code runs as two separate algorithms; the first of them starts at the bottom of the zone and builds a list of allocated pages which could be moved… Meanwhile, at the top of the zone, the other half of the algorithm is creating a list of free pages which could be used as the target of page migration. Eventually the two algorithms will meet somewhere toward the middle of the zone.”

Prose does not carry this well, so here it is as a filmstrip. Mermaid has no primitive for “an address range with two cursors converging over successive time steps”, so this one is an ASCII/box diagram in a fenced block — the fallback the vault’s conventions call for when mermaid genuinely cannot express the thing. Every other diagram in this note is mermaid or a table.

LEGEND   M = in-use MOVABLE page      U = in-use UNMOVABLE page (slab, page table)
         . = free page                [ ] = one pageblock (512 pages = 2 MiB on x86-64)

FRAME 0 - before compaction. ~40% of the zone is free, but no order-9 block exists.
         low PFN                                                        high PFN
         v                                                                     v
         [M.M.MM.M.U.M][.M.M.MM..M.M][MM..M.M.M..M][.M.MM..M.M.M][M..M.M.MM..M]
          ^migrate_pfn                                            free_pfn^
          cc->migrate_pfn = zone_start_pfn        cc->free_pfn = last pageblock start

FRAME 1 - one batch. The migrate scanner isolates up to COMPACT_CLUSTER_MAX = 32
          movable pages; the free scanner isolates free pages to receive them.
         [M.M.MM.M.U.M][.M.M.MM..M.M][MM..M.M.M..M][.M.MM..M.M.M][M..M.M.MM..M]
          ----> isolate 32 M                                 isolate 32 '.' <----
                     migrate_pages(): copy contents + repoint every PTE

FRAME 2 - after several batches. The bottom is draining; the top is filling.
         [............][.M.M.MM..M.M][MM..M.M.M..M][.MMMMMMMMMMM][MMMMMMMMMMMM]
          ^^^^^^^^^^^^ ^migrate_pfn                 free_pfn^
          this pageblock is now entirely free = one order-9 (2 MiB) block

FRAME 3 - the scanners meet. compact_scanners_met() is true; the pass ends.
         [............][............][MM..M.M.M..M][MMMMMMMMMMMM][MMMMMMMMMMMM]
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^both^
          two adjacent free pageblocks = one order-10 (4 MiB) block

FRAME 4 - the same zone, but with ONE unmovable page in the second pageblock.
         [............][.....U......][MM..M.M.M..M][MMMMMMMMMMMM][MMMMMMMMMMMM]
                            ^
                            this single U can never be migrated, so THIS pageblock
                            can never become an order-9 block, and the free run
                            stops at its boundary. One page costs 2 MiB of contiguity.

The two-scanner algorithm of mm/compaction.c, drawn as a filmstrip. 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 from the bottom into the top, draining the bottom into large aligned free runs. Frames 0–3 are the success case; frame 4 is the failure case. The insight to take: compaction does not create free memory — the total free-page count is identical in every frame — it only rearranges movable pages so that free pages it already had become physically contiguous and naturally aligned, which is the only property the buddy allocator needs. And frame 4 is the entire reason the buddy allocator bothers with migratetypes: as the 2010 LWN write-up puts it, “it only takes one non-movable page to ruin a contiguous segment of memory” (LWN 368869), so the real cost of a stray unmovable allocation is 512 pages of lost contiguity, not one.

Why the pageblock, and not the page, is the unit of everything

The termination test is a single line, and reading it explains why every structure in compaction — the skip hints, the migratetype, the fragmentation score — is denominated in pageblocks rather than pages:

/* mm/compaction.c:1452, v6.12 */
static inline bool compact_scanners_met(struct compact_control *cc)
{
	return (cc->free_pfn >> pageblock_order)
		<= (cc->migrate_pfn >> pageblock_order);
}

Both PFNs are right-shifted by pageblock_order, so the comparison happens at pageblock granularity: the pass finishes when the rising migrate scanner reaches the same pageblock as the falling free scanner, not when the raw PFNs cross. pageblock_order is chosen by configuration in include/linux/pageblock-flags.h, and the branch that applies on a distribution kernel is the first one, not the transparent-huge-page one:

/* include/linux/pageblock-flags.h, v6.12 — abridged */
#if defined(CONFIG_HUGETLB_PAGE)
# ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE
extern unsigned int pageblock_order;          /* runtime value, e.g. powerpc */
# else
#  define pageblock_order  MIN_T(unsigned int, HUGETLB_PAGE_ORDER, MAX_PAGE_ORDER)
# endif
#elif defined(CONFIG_TRANSPARENT_HUGEPAGE)
# define pageblock_order   MIN_T(unsigned int, HPAGE_PMD_ORDER, MAX_PAGE_ORDER)
#else
# define pageblock_order   MAX_PAGE_ORDER
#endif
#define pageblock_nr_pages (1UL << pageblock_order)

On x86-64 both huge-page branches evaluate to 9 — 512 pages, 2 MiB — because HUGETLB_PAGE_ORDER and HPAGE_PMD_ORDER are both the PMD order there, and MAX_PAGE_ORDER is 10 (include/linux/mmzone.h:30), so the MIN_T does not bite. The distinction matters on architectures where the default hugetlb size and the PMD size differ. Note also the CONFIG_HUGETLB_PAGE_SIZE_VARIABLE case: on some architectures pageblock_order is not a compile-time constant at all but a runtime variable, which is why kernel code always writes pageblock_order and never a literal 9.

A pageblock is therefore exactly the alignment unit compaction is trying to liberate, and it is simultaneously the unit that carries the migratetype (3 bits, PB_migrate..PB_migrate_end) and the compaction skip hint (PB_migrate_skip) in the zone’s pageblock bitmap.

Three scarcities that look alike

Before going further it is worth separating compaction from the two mechanisms it is most often confused with. “The high-order allocation failed” has three distinct root causes, and only one of them is compaction’s job.

flowchart TB
  subgraph THREE["Three problems that look identical from the caller"]
    direction TB
    P1["Not enough FREE pages<br/>at all"]
    P2["Enough free pages,<br/>none CONTIGUOUS"]
    P3["Contiguous pages exist,<br/>but in the WRONG<br/>migratetype pageblock"]
  end
  P1 --> S1["RECLAIM<br/>evict page cache,<br/>swap out anonymous<br/>(mm/vmscan.c)"]
  P2 --> S2["COMPACTION<br/>migrate movable pages<br/>(mm/compaction.c)"]
  P3 --> S3["FALLBACK and STEAL<br/>claim a block of another<br/>migratetype (mm/page_alloc.c)"]
  S1 --> W["High-order allocation<br/>now succeeds"]
  S2 --> W
  S3 --> W
  S3 -.->|"block not compatible enough<br/>to claim wholesale: take one<br/>page, leave the migratetype"| FRAG["FRAGMENTATION EVENT<br/>an unmovable page now sits<br/>inside a movable pageblock"]
  FRAG -.->|"creates future work for"| S2

The three distinct scarcities and their three distinct cures. What it shows: compaction is the answer to exactly one of the three ways a high-order allocation can fail — the middle one. Reclaim answers the first; the allocator’s own fallback machinery answers the third. The dotted path is the feedback loop that couples them: when the allocator cannot claim a whole compatible pageblock it takes a single page and leaves the block’s migratetype unchanged, permanently seeding a movable pageblock with an unmovable page — which is precisely the frame-4 situation above, and which manufactures work for compaction later. The insight to take: compaction, reclaim, and buddy fallback are one coupled system, so tuning any of them in isolation moves work between them rather than removing it. See Memory Reclaim Overview for the reclaim half and Page Order and Fragmentation for the fallback half.

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, and only from a block that is naturally aligned to 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 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.

This is not a hypothetical. Here is /proc/buddyinfo from the measurement box after two weeks of uptime, which is the canonical way to see external fragmentation:

$ cat /proc/buddyinfo
                    order:  0      1      2     3     4    5    6   7   8   9  10
Node 0, zone      DMA       0      0      0     0     0    0    0   0   1   1   2
Node 0, zone    DMA32    2220    820    490   214   106   92   53  41  14  39  87
Node 0, zone   Normal   99971 133229 184915 63149 12746 3952 2023 872 611 641 1740

Each column counts free blocks of that order, so zone Normal holds 99,971 lone free pages, 133,229 free pairs, and so on down to 1,740 free order-10 (4 MiB) blocks. Multiplying each count by 2^order and summing gives 4,449,074 free pages ≈ 17.0 GiB free in that zone, spread across 503,786 separate free blocks. But only 641 + 1740×2 = 4,121 order-9-sized slots are free, accounting for 2,109,952 pages. The other 2,339,122 free pages — 52.6% of all free memory in the zone — are stranded in blocks too small to satisfy a single huge-page request. That percentage is not an analogy: as shown later, it is literally the number the kernel computes as this zone’s fragmentation score. (This snapshot is used consistently throughout the note; the machine is live, so counts drift by a few percent between reads.)

Movable, reclaimable, unmovable

The key enabler is that a large class of pages are movable: their physical location is an implementation detail the kernel may change transparently by copying the contents to a new frame and fixing up every reference to the old frame. Anything reached through a level of indirection qualifies — user-space anonymous pages and page-cache pages are reached through page tables or through the page cache’s xarray, so relocating one means copying 4 KiB and rewriting a pointer. Anything reached by a raw kernel virtual address does not qualify, because there is no bounded set of pointers to rewrite.

The buddy allocator encodes this distinction directly in the pageblock bitmap, three bits per pageblock (include/linux/mmzone.h:48):

MigratetypeTypical contentsCompactable?Why
MIGRATE_MOVABLEanonymous pages, page cache, shmem, most GFP_HIGHUSER_MOVABLE allocationsyesreached via page tables / page cache; copy + repoint
MIGRATE_RECLAIMABLEdentry and inode caches, other shrinker-backed slabs (__GFP_RECLAIMABLE)no — but freeablecannot be moved, can be dropped entirely by a shrinker
MIGRATE_UNMOVABLEgeneral kernel slab, page tables, kmalloc, per-CPU datanoreached by raw kernel pointers; nothing to rewrite
MIGRATE_HIGHATOMICreserve for high-order atomic (GFP_ATOMIC) allocationsnoa reservation, deliberately held back
MIGRATE_CMApages inside a Contiguous Memory Allocator (CMA) region (CONFIG_CMA)yesonly movable allocations are ever placed here, by construction
MIGRATE_ISOLATEa range being taken offline or handed to CMA (CONFIG_MEMORY_ISOLATION)n/atemporarily non-allocatable

The pageblock migratetypes of Linux 6.12, and what each means for compaction. What it shows: the buddy allocator does not merely track free pages, it segregates them by how hard they are to get rid of. The insight to take: MIGRATE_UNMOVABLE is the adversary in this whole story. Compaction can do nothing about an unmovable page except route around it, and the segregation exists so that unmovable allocations cluster together in a few pageblocks rather than salting every pageblock in the zone with one.

The practical stakes are easy to underestimate. Through 2009, “not-top-of-the-line wireless network adapters which require contiguous memory chunks to operate” were failing on ordinary desktops because high-order allocations could not be satisfied on a fragmented system, producing allocation-failure log spam, failed applications, and unwanted OOM kills (LWN, “High-order GFP_ATOMIC allocation trouble”). Such a driver allocates from interrupt context and cannot block, so it cannot compact and cannot reclaim — it can only hope the free lists already hold what it needs. MIGRATE_HIGHATOMIC exists as a standing reserve for exactly that case, and it is the reason the migratetype table above has a row whose whole purpose is to be unavailable.

This split has a long pre-history. Mel Gorman’s “page clustering” patch set — version 27 by the time LWN covered it in 2006 — introduced exactly these three categories, with the argument that “one stubborn page is all it takes to keep an entire large block of memory from being consolidated.” The same article covers lumpy reclaim, the mechanism compaction eventually replaced: lumpy reclaim picked an LRU victim and then tried to free its physical neighbours as well, distorting the LRU order to manufacture contiguity. Compaction is strictly better because it does not have to destroy anything — the pages it touches survive at a different address.

stateDiagram-v2
  direction LR
  [*] --> Movable: pageblock initialised<br/>(most of the zone starts here)
  Movable --> Unmovable: steal_suitable_fallback()<br/>kernel alloc claims the<br/>whole block
  Unmovable --> Movable: block becomes free and<br/>is claimed by a movable alloc
  Movable --> Reclaimable: claimed for shrinker-backed<br/>slab (__GFP_RECLAIMABLE)
  Reclaimable --> Movable: freed and reclaimed
  Movable --> Polluted: fallback took ONE page<br/>but left the migratetype
  Polluted --> Movable: the intruding page<br/>is eventually freed
  Polluted --> Polluted: more unmovable pages land here
  note right of Polluted
    Still labelled MIGRATE_MOVABLE,
    but contains an unmovable page.
    Compaction will scan it, migrate
    what it can, and still fail to
    produce an aligned free block.
    This is the expensive state.
  end note
  Movable --> Highatomic: reserved for high-order<br/>GFP_ATOMIC
  Highatomic --> Movable: reserve drained/released

Pageblock migratetype transitions and the one state that hurts. What it shows: a pageblock’s migratetype is not fixed at boot — the allocator re-labels blocks as demand shifts, and each re-labelling is cheap. The insight to take: the damaging transition is the unlabelled one on the right. When the allocator cannot claim a whole block it will still satisfy the request from a single page of a movable block and leave the label alone, producing a block that advertises itself as movable but is not fully compactable. Compaction pays full scanning cost on such blocks and gets nothing back, which is the mechanism behind a rising compact_fail count on a long-uptime machine.

Making driver pages movable

There is a third option besides “movable because it is user memory” and “unmovable because the kernel points at it directly”: a subsystem can opt in to migration by publishing callbacks. This began as special-case code for balloon-driver pages in 2012 and was generalised in 2015 (LWN, “Making kernel pages movable”). In 6.12 the interface is struct movable_operations (include/linux/migrate.h:53):

struct movable_operations {
	bool (*isolate_page)(struct page *, isolate_mode_t);
	int  (*migrate_page)(struct page *dst, struct page *src,
			     enum migrate_mode);
	void (*putback_page)(struct page *);
};

A subsystem calls __SetPageMovable(page, mops), which — and this is a nice piece of kernel frugality — stores the operations pointer in page->mapping with the low PAGE_MAPPING_MOVABLE bit set, so no new field in struct page was needed:

/* mm/compaction.c:131, v6.12 */
void __SetPageMovable(struct page *page, const struct movable_operations *mops)
{
	VM_BUG_ON_PAGE(!PageLocked(page), page);
	VM_BUG_ON_PAGE((unsigned long)mops & PAGE_MAPPING_MOVABLE, page);
	page->mapping = (void *)((unsigned long)mops | PAGE_MAPPING_MOVABLE);
}

migrate_page() returning -EAGAIN is read by the migration core as a temporary failure to be retried; any other negative value is permanent and the page is put back. zsmalloc (the backing store for zram and zswap) and the virtio balloon driver are the principal in-tree users. Memory allocated from slab caches remains immobile regardless — there is no way to rewrite the arbitrary kernel pointers into a slab object — which is why kernel slab growth is the most durable source of fragmentation on a long-lived machine.

A Short History — From Lumpy Reclaim to Proactive Compaction

Compaction did not arrive fully formed, and the sequence explains the shape of the code. Every date below is pinned by an existence check against the tag named.

timeline
  title Fixing external fragmentation in Linux, verified by tag
  2004 : "Active memory defragmentation" RFC (Tosatti)<br/>relocate pages around a free block<br/>LWN 105021 - never merged
  2005-2006 : Mel Gorman's page clustering, v27<br/>MOVABLE / RECLAIMABLE / UNMOVABLE<br/>pageblocks - LWN 211505<br/>lumpy reclaim as the stopgap
  2010 : mm/compaction.c merged in v2.6.35<br/>404 at v2.6.34, 200 at v2.6.35<br/>two scanners + direct compaction<br/>LWN 368869
  2016 : kcompactd merged in v4.6<br/>absent at v4.5<br/>background compaction leaves<br/>the allocation path
  2017 : LSFMM debates proactive compaction<br/>LWN 717656 - "start as simple<br/>as possible", one on/off-ish knob
  2020 : proactive compaction merged in v5.9<br/>sysctl_compaction_proactiveness<br/>absent at v5.8 - LWN 817905
  2024 : LSFMM+BPF - how do we even<br/>MEASURE fragmentation?<br/>LWN 974943 - no consensus

The evolution of contiguity recovery in Linux. What it shows: four merged milestones (2.6.35, 4.6, 5.9) and the two decades of rejected attempts around them. The insight to take: each step moved work further away from the allocating task. In 2.6.35 the allocator itself compacted and stalled; in 4.6 the work could be handed to a per-node daemon; in 5.9 the daemon started working before anyone asked. That trajectory is the answer to “why are there three entry points” — they are three generations of the same idea, all still present because each covers a case the next one does not.

The existence checks behind those dates, run against raw.githubusercontent.com on 2026-09-04:

PathTagHTTPConclusion
mm/compaction.cv2.6.34404file does not exist
mm/compaction.cv2.6.35200compaction merged in 2.6.35 (2010)
mm/compaction.c grep kcompactdv4.50 hitsdaemon does not exist
mm/compaction.c grep kcompactdv4.649 hitskcompactd merged in 4.6 (2016)
mm/compaction.c grep sysctl_compaction_proactivenessv5.80 hitstunable does not exist
mm/compaction.c grep sysctl_compaction_proactivenessv5.93 hitsproactive compaction merged in 5.9 (2020)

Dating kernel features by fetching the same path at successive tags. What it shows: three merge windows established without trusting any secondary source or any recollection. The insight to take: an HTTP 404 at one tag and a 200 at the next is a primary-source fact about when a file appeared; this technique also resolves the “which release added the flag” questions that changelogs and blog posts routinely get wrong. It replaces the uncertainty flag this note previously carried about the 2.6.35 date — the claim was correct, and is now verified rather than remembered.

The 2010 write-up also records an interface that no longer exists: the original patch triggered compaction by “writing a node number to /proc/sys/vm/compact_node” (LWN 368869). On 6.12 that file is gone; the equivalents are the system-wide /proc/sys/vm/compact_memory and the per-node /sys/devices/system/node/nodeN/compact (registered by compaction_register_node(), mm/compaction.c:3006). Both were confirmed present on the measurement box; compact_node was confirmed absent.

Mechanical Walk-through — How a Compaction Pass Runs

Every compaction entry point — direct, daemon, proactive, manual — funnels into one function, compact_zone() (mm/compaction.c:2506), which compacts exactly one zone. Everything it needs travels in a struct compact_control, universally called cc in this file: the two scanner cursors cc->migrate_pfn and cc->free_pfn, the isolated-page lists cc->migratepages and cc->freepages[], the migration mode, the target order, and a pile of booleans (direct_compaction, proactive_compaction, whole_zone, ignore_skip_hint) that encode which caller is asking.

flowchart TD
  A["compact_zone(cc, capc)"] --> B{"is_via_compact_memory<br/>(cc->order)?<br/>i.e. order == -1"}
  B -->|"no — a real order<br/>was requested"| C["compaction_suit_allocation_order()<br/>watermark already OK? -> COMPACT_SUCCESS<br/>zone unsuitable? -> COMPACT_SKIPPED"]
  B -->|"yes — compact<br/>unconditionally"| D
  C -->|"COMPACT_CONTINUE"| D["compaction_restarting()?<br/>-> __reset_isolation_suitable()<br/>wipe every PB_migrate_skip bit"]
  D --> E["Place the scanners.<br/>whole_zone: migrate_pfn = zone_start_pfn,<br/>free_pfn = last pageblock start.<br/>otherwise: resume from the zone's<br/>cached PFNs from the previous pass"]
  E --> F["lru_add_drain()<br/>flush this CPU's LRU batches so<br/>fresh pages are visible to the scanner"]
  F --> G{"compact_finished(cc)<br/>== COMPACT_CONTINUE?"}
  G -->|no| Z["return the compact_result:<br/>SUCCESS / COMPLETE /<br/>PARTIAL_SKIPPED / CONTENDED"]
  G -->|yes| H["isolate_migratepages(cc)<br/>walk UP; fill cc->migratepages<br/>with up to COMPACT_CLUSTER_MAX = 32 pages"]
  H --> I["migrate_pages(&cc->migratepages,<br/>compaction_alloc, compaction_free, cc,<br/>cc->mode, MR_COMPACTION, &nr_succeeded)"]
  I --> J["compaction_alloc() pulls a destination<br/>from cc->freepages[]; if empty it calls<br/>isolate_freepages(), which walks DOWN"]
  J --> K{"migration<br/>errors?"}
  K -->|"-ENOMEM and scanners<br/>have not met"| Y["COMPACT_CONTENDED"]
  K -->|"ok / partial"| L["update cached PFNs and<br/>PB_migrate_skip hints"]
  L --> G

The compact_zone() control flow in Linux 6.12. What it shows: the whole algorithm is a single loop whose exit condition is compact_finished() and whose body is “isolate a batch, migrate the batch”. The insight to take: the free scanner is not a peer of the migrate scanner in the code — it is called lazily from the allocation callback, compaction_alloc(), only when the destination list runs dry. That is why compact_free_scanned in /proc/vmstat is typically far larger than compact_migrate_scanned: the free scanner must sift a lot of already-allocated memory to find each free page, and it re-enters on demand rather than running in lockstep.

On the measurement box that ratio is stark: compact_free_scanned 5,468,096,527 against compact_migrate_scanned 2,195,523,291 — the free scanner has examined 2.49 pages for every one the migrate scanner examined, over the same 14 days.

The migrate scanner is isolate_migratepages() (mm/compaction.c:2078) and its per-block worker isolate_migratepages_block(). It iterates whole pageblocks — for (; block_end_pfn <= cc->free_pfn; ... block_end_pfn += pageblock_nr_pages), an explicit refusal to cross the free scanner — and within each block isolates movable pages from their LRU onto cc->migratepages. It stops accumulating once cc->nr_migratepages >= COMPACT_CLUSTER_MAX, and that constant is defined by aliasing the reclaim batch size (include/linux/swap.h:225):

#define SWAP_CLUSTER_MAX    32UL
#define COMPACT_CLUSTER_MAX SWAP_CLUSTER_MAX

So compaction works in batches of 32 pages. The same constant bounds lock hold time: isolate_migratepages_block() unlocks the LRU vector and checks for a fatal signal every COMPACT_CLUSTER_MAX pages (if (!(low_pfn % COMPACT_CLUSTER_MAX)), mm/compaction.c:945), and the outer pageblock loop reschedules every COMPACT_CLUSTER_MAX * pageblock_nr_pages = 32 × 512 = 16,384 pages (64 MiB) of skipped-over zone. Without that second check, scanning a 64 GiB zone of unsuitable pageblocks would be an unpreemptible hold.

Not every pageblock is even eligible as a source. suitable_migration_source() (mm/compaction.c:1393) refuses a block outright when the request is async direct compaction and the block’s migratetype does not match what the caller wants:

if ((cc->mode != MIGRATE_ASYNC) || !cc->direct_compaction)
	return true;                      /* daemon and sync passes take anything */
block_mt = get_pageblock_migratetype(page);
if (cc->migratetype == MIGRATE_MOVABLE)
	return is_migrate_movable(block_mt);
else
	return block_mt == cc->migratetype;

The logic is subtle and worth stating plainly: an async, direct compactor pulling pages for an unmovable allocation will only drain MIGRATE_UNMOVABLE blocks, because emptying a movable block to hand it to a kernel allocation would itself be a fragmentation event. Sync passes and kcompactd ignore the restriction, because by then the system is desperate enough that the trade is worth making.

The free scanner is isolate_freepages() (mm/compaction.c:1711). Starting from cc->free_pfn near the top of the zone it walks downward, bounded from below by pageblock_end_pfn(cc->migrate_pfn), isolating free pages off the buddy lists onto cc->freepages[] — an array indexed by order, not a flat list, so a large free block can be donated whole rather than split. Its own filter is suitable_migration_target() (mm/compaction.c:1413), which rejects a free block that is already at least as large as the target order (buddy_order_unsafe(page) >= order). Consuming an existing order-9 block to build an order-9 block would be pure loss.

There is also a fast path. fast_find_migrateblock() (mm/compaction.c:1956) and fast_isolate_freepages() (mm/compaction.c:1538) search the buddy free lists directly for promising pageblocks rather than scanning linearly, using cc->search_order and a failure-adaptive limit:

static inline unsigned int freelist_scan_limit(struct compact_control *cc)
{
	unsigned short shift = BITS_PER_LONG - 1;
	return (COMPACT_CLUSTER_MAX >> min(shift, cc->fast_search_fail)) + 1;
}

Every consecutive failure of the fast search halves how many free-list entries the next attempt will examine — 33, 17, 9, 5, 3, 2, then 1 from the seventh failure onward — so the optimisation degrades to a single probe on a zone where it is not paying, rather than burning CPU indefinitely.

Skip hints — how compaction remembers what did not work

Re-scanning a pageblock that yielded nothing last time is the single largest avoidable cost in compaction, so the kernel records failures in the pageblock bitmap. PB_migrate_skip (include/linux/pageblock-flags.h) is one bit per pageblock, set by update_pageblock_skip() when a block produced no isolatable pages, and consulted by isolation_suitable():

static inline bool isolation_suitable(struct compact_control *cc,
				      struct page *page)
{
	if (cc->ignore_skip_hint)
		return true;
	return !get_pageblock_skip(page);
}

Alongside the bits, each zone caches where the scanners stopped: zone->compact_cached_free_pfn and zone->compact_cached_migrate_pfn[2]two migrate cursors, indexed by sync, because an async pass gives up on pageblocks a sync pass would have handled, and the two must not poison each other’s resume point.

StateWhere it livesSet byCleared by
PB_migrate_skip bitpageblock bitmap, 1 bit/2 MiBupdate_pageblock_skip() after a fruitless block__reset_isolation_suitable()
compact_cached_migrate_pfn[0]struct zoneend of an async passreset_cached_positions()
compact_cached_migrate_pfn[1]struct zoneend of a sync passreset_cached_positions()
compact_cached_free_pfnstruct zoneend of any passreset_cached_positions()
compact_blockskip_flushstruct zone__compact_finished() when a direct pass completes a whole zoneconsumed by kswapd before it sleeps

Compaction’s persistent memory between passes. What it shows: compaction is not stateless — it deliberately carries scars from previous attempts so that repeated failures get cheaper rather than costing full price every time. The insight to take: this is also why compaction behaviour is history-dependent and hard to benchmark. A zone whose skip bits are all set will report a very fast, very useless compaction pass. The bits are wiped by reset_isolation_suitable(), which — see mm/vmscan.c:7093kswapd calls just before going to sleep, on the reasoning that reclaim has just freed memory so previously hopeless blocks deserve another look.

compaction_restarting() adds a second reset trigger: when a zone has hit the maximum deferral shift and burned through the full deferral countdown, compact_zone() wipes the skip bits before trying again — a periodic amnesty so a zone cannot be permanently written off.

Migration itself is one call:

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

compaction_alloc() is the destination allocator: it hands migrate_pages a page from cc->freepages[], calling isolate_freepages() to refill when the list is empty. compaction_free() returns unused destinations. The page-copy-and-remap mechanism itself — unmapping every PTE, copying contents, installing migration entries, remapping, waking anyone who faulted on the migration entry — lives in Page Migration and is deliberately not re-explained here; compaction is one of its consumers alongside Memory Hotplug offlining, NUMA balancing, and CMA. The MR_COMPACTION reason code is how you tell them apart in the mm_migrate_pages tracepoint.

Sync versus Async — Who Blocks, and Who Calls Which

This is the single most consequential axis in compaction, because it decides whether a compaction attempt is a sub-millisecond latency blip or a multi-second stall. It is also routinely misdescribed, including in the earlier version of this note, so it is worth getting exactly right from the source.

There are two orthogonal knobs, not one.

The first is cc->mode, a enum migrate_mode passed straight through to migrate_pages() (include/linux/migrate_mode.h, whose comments are the authoritative definition):

/*
 * MIGRATE_ASYNC means never block
 * MIGRATE_SYNC_LIGHT in the current implementation means to allow blocking
 *	on most operations but not ->writepage as the potential stall time
 *	is too significant
 * MIGRATE_SYNC will block when migrating pages
 */
enum migrate_mode {
	MIGRATE_ASYNC,
	MIGRATE_SYNC_LIGHT,
	MIGRATE_SYNC,
};

The second is enum compact_priority (include/linux/compaction.h), which is not the mode. It is the direct compactor’s escalation ladder, and it controls how much of the zone gets looked at:

enum compact_priority {
	COMPACT_PRIO_SYNC_FULL,
	MIN_COMPACT_PRIORITY = COMPACT_PRIO_SYNC_FULL,
	COMPACT_PRIO_SYNC_LIGHT,
	MIN_COMPACT_COSTLY_PRIORITY = COMPACT_PRIO_SYNC_LIGHT,
	DEF_COMPACT_PRIORITY = COMPACT_PRIO_SYNC_LIGHT,
	COMPACT_PRIO_ASYNC,
	INIT_COMPACT_PRIORITY = COMPACT_PRIO_ASYNC
};

Lower numeric value means higher priority, deliberately mirroring reclaim priority. compact_zone_order() (mm/compaction.c:2743) translates priority into the cc fields, and here is the line that surprises people:

.mode = (prio == COMPACT_PRIO_ASYNC) ? MIGRATE_ASYNC : MIGRATE_SYNC_LIGHT,
.whole_zone           = (prio == MIN_COMPACT_PRIORITY),
.ignore_skip_hint     = (prio == MIN_COMPACT_PRIORITY),
.ignore_block_suitable = (prio == MIN_COMPACT_PRIORITY),

Direct compaction never uses MIGRATE_SYNC. The highest direct priority, COMPACT_PRIO_SYNC_FULL, still runs in MIGRATE_SYNC_LIGHT mode; what “full” buys is thoroughness of scanning, not willingness to block on I/O — it makes the pass cover the whole zone, ignore every skip hint, and ignore the block-suitability filter. The only code path in the whole file that sets MIGRATE_SYNC is compact_node(pgdat, proactive=false), reached exclusively from /proc/sys/vm/compact_memory and the per-node sysfs compact file.

That distinction is load-bearing, and the reason is history. Writing to a slow USB stick used to lock up desktops for minutes: a THP page fault would enter synchronous compaction, hit a page under writeback to the slow device, and sleep on that I/O — repeatedly, since building one huge page can mean migrating hundreds of ordinary ones (LWN, “Huge pages, slow drives, and long delays”, 2011). MIGRATE_SYNC_LIGHT is the settlement: block on locks, never block on ->writepage. The isolate_mode computed in isolate_migratepages() encodes it directly —

const isolate_mode_t isolate_mode =
	(sysctl_compact_unevictable_allowed ? ISOLATE_UNEVICTABLE : 0) |
	(cc->mode != MIGRATE_SYNC ? ISOLATE_ASYNC_MIGRATE : 0);

— so ISOLATE_ASYNC_MIGRATE (skip anything that would require waiting) is cleared for full MIGRATE_SYNC only. Everything except a manual compact_memory write refuses to wait on dirty-page writeback.

CallerEntry pointcc->modeorderignore_skip_hintwhole_zoneCan block on writeback?
Allocator slow path, first trycompact_zone_order() at COMPACT_PRIO_ASYNCMIGRATE_ASYNCrequestednonono — never blocks at all
Allocator slow path, escalatedcompact_zone_order() at COMPACT_PRIO_SYNC_LIGHTMIGRATE_SYNC_LIGHTrequestednonono
Allocator slow path, top priority (non-costly only)compact_zone_order() at COMPACT_PRIO_SYNC_FULLMIGRATE_SYNC_LIGHTrequestedyesyesno
kcompactd, woken for an orderkcompactd_do_work()MIGRATE_SYNC_LIGHTkcompactd_max_ordernonono
kcompactd, proactive passcompact_node(pgdat, true)MIGRATE_SYNC_LIGHT−1yesyesno
echo 1 > /proc/sys/vm/compact_memorycompact_nodes()compact_node(pgdat, false)MIGRATE_SYNC−1yesyesYES
echo 1 > /sys/devices/system/node/nodeN/compactcompact_store()compact_node(pgdat, false)MIGRATE_SYNC−1yesyesYES

Every compaction caller in Linux 6.12, and exactly how hard each one tries. What it shows: the migration mode is decided by who is calling, not by how desperate the system is. The insight to take: if you are chasing a multi-second stall attributed to compaction, only the bottom two rows can block on disk I/O — and both are triggered by a human or a script writing to a sysctl. A stall in the allocator path is a stall on locks, on cond_resched(), and on the sheer volume of page copying, never on writeback. Conversely, echo 1 > /proc/sys/vm/compact_memory on a busy machine with a slow backing device is genuinely dangerous in a way that the automatic paths are not.

An order of −1 in that table is not a typo. is_via_compact_memory(cc->order) tests order == -1, and a cc carrying it means “no specific target — just reduce fragmentation generally”, which makes __compact_finished() return COMPACT_CONTINUE unconditionally and run until the scanners meet. Every whole-zone caller uses it.

Async mode has one further self-limiting behaviour that explains a common observation. In isolate_migratepages_block():

if (cc->direct_compaction && (cc->mode == MIGRATE_ASYNC)) {
	skip_on_failure = true;
	next_skip_pfn = block_end_pfn(low_pfn, cc->order);
}

An async direct compactor that fails to isolate one page abandons the entire cc->order-aligned sub-block and jumps to the next one, on the reasoning that a single stuck page has already made this candidate block useless. Sync passes instead set cc->finish_pageblock and rescan the remainder of the block specifically so it can be marked skip and not revisited. Same failure, opposite response: async optimises for exiting quickly, sync optimises for not being asked again.

Direct Compaction — Reactive, in the Allocator’s Context

When the buddy allocator’s fast path fails a high-order allocation, __alloc_pages_slowpath() may invoke direct compaction synchronously in the failing task’s own context — the fragmentation analogue of Direct Reclaim. The chain is __alloc_pages_direct_compact()try_to_compact_pages()compact_zone_order()compact_zone().

The first thing to understand is when the slow path reaches for compaction, because it is not always, and it is not always after reclaim (mm/page_alloc.c:4299):

if (can_direct_reclaim && can_compact &&
	(costly_order ||
	   (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
	&& !gfp_pfmemalloc_allowed(gfp_mask)) {
	page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
					    INIT_COMPACT_PRIORITY, &compact_result);

costly_order means order > PAGE_ALLOC_COSTLY_ORDER, and that constant is 3 (include/linux/mmzone.h:46) — so “costly” begins at order 4, 16 pages, 64 KiB. Two classes of request get compaction before reclaim: costly ones (where base pages probably exist and only contiguity is missing), and any non-movable high-order request (where compacting first avoids permanently polluting a movable pageblock). Everything else falls through to reclaim first and only reaches __alloc_pages_direct_compact() again at mm/page_alloc.c:4386, after __alloc_pages_direct_reclaim().

flowchart TD
  A["__alloc_pages_slowpath()<br/>fast path already failed"] --> B["wake_all_kswapds()"]
  B --> C{"costly_order (order > 3)<br/>OR non-movable high order?"}
  C -->|yes| D["__alloc_pages_direct_compact()<br/>INIT_COMPACT_PRIORITY = ASYNC"]
  C -->|no| G
  D --> E{"got a page?"}
  E -->|yes| OK["got_pg"]
  E -->|no| F{"costly AND __GFP_NORETRY?<br/>(this is the THP fault case)"}
  F -->|"result was SKIPPED<br/>or DEFERRED"| NP["nopage — fall back<br/>to base pages immediately"]
  F -->|otherwise| G["retry: label"]
  G --> H["__alloc_pages_direct_reclaim()<br/>evict page cache / swap"]
  H --> I["__alloc_pages_direct_compact()<br/>at the CURRENT compact_priority"]
  I --> J{"got a page?"}
  J -->|yes| OK
  J -->|no| K["should_compact_retry()"]
  K -->|"result == SUCCESS:<br/>retry up to MAX_COMPACT_RETRIES = 16<br/>(divided by 4 = 4 for costly orders)"| G
  K -->|"result == failure:<br/>compact_priority-- and retry,<br/>floored at MIN_COMPACT_COSTLY_PRIORITY<br/>for costly orders"| G
  K -->|"no retries left"| OOM["__alloc_pages_may_oom()<br/>or return NULL"]

The allocator slow path’s use of compaction, from mm/page_alloc.c at v6.12. What it shows: compaction appears twice in the slow path — once speculatively before reclaim for costly and non-movable orders, and once after reclaim inside the retry loop — and the retry loop is where the priority ratchet lives. The insight to take: the left-hand nopage exit is the THP fault path. A transparent-huge-page fault allocation carries __GFP_NORETRY, so when async compaction returns COMPACT_SKIPPED or COMPACT_DEFERRED the kernel gives up instantly and hands the task a 4 KiB page. That is not a bug; it is the deliberate outcome of the 2011 desktop-stall debate, and it is why thp_fault_fallback can be large on a fragmented machine without any latency complaint at all.

The priority ratchet, and where it stops

should_compact_retry() (mm/page_alloc.c:3727) drives escalation. Two numbers govern it:

#define MAX_COMPACT_RETRIES 16
...
if (compact_result == COMPACT_SUCCESS) {
	if (order > PAGE_ALLOC_COSTLY_ORDER)
		max_retries /= 4;                 /* costly: 4 retries, not 16 */
	if (++(*compaction_retries) <= max_retries) { ret = true; goto out; }
}
...
min_priority = (order > PAGE_ALLOC_COSTLY_ORDER) ?
		MIN_COMPACT_COSTLY_PRIORITY : MIN_COMPACT_PRIORITY;
if (*compact_priority > min_priority) {
	(*compact_priority)--;
	*compaction_retries = 0;
	ret = true;
}

COMPACT_SUCCESS followed by an allocation failure means compaction produced a block and someone else took it — a race, so retry at the same priority. An actual compaction failure instead raises the priority by one step and resets the retry counter.

Costly orders are floored at MIN_COMPACT_COSTLY_PRIORITY, which is COMPACT_PRIO_SYNC_LIGHT. An order-9 THP allocation is costly, so it can reach COMPACT_PRIO_SYNC_LIGHT and stop there — it never gets COMPACT_PRIO_SYNC_FULL, and therefore never gets the whole-zone, ignore-all-skip-hints treatment. Only non-costly orders (order 1–3) can escalate that far, on the grounds that for them the alternative to success is the OOM killer, whereas a failed huge-page allocation just becomes 512 small pages.

OrderExampleCostly?Retries at COMPACT_SUCCESSPriority floor reachable
1–38 KiB–32 KiB kernel structures, fork() stacksno16COMPACT_PRIO_SYNC_FULL (whole zone, ignore skip hints)
4–864 KiB–1 MiB, jumbo network buffers, some slabsyes4COMPACT_PRIO_SYNC_LIGHT
92 MiB THP / hugetlbfs huge pageyes4COMPACT_PRIO_SYNC_LIGHT
104 MiB — MAX_PAGE_ORDER on x86-64yes4COMPACT_PRIO_SYNC_LIGHT
181 GiB gigantic pagen/a — not served by the buddy allocator at all; alloc_contig_range() / boot reservation

How hard the allocator will push compaction, by order. What it shows: PAGE_ALLOC_COSTLY_ORDER = 3 is the dividing line, and everything a huge-page workload cares about is on the low-effort side of it. The insight to take: the kernel deliberately tries less hard for the allocations people most want to succeed, because those are the ones with a cheap fallback. If you need order-9 blocks reliably, the answer is never “make direct compaction try harder” — there is no knob for that — it is proactive compaction, hugetlbfs pre-reservation, or madvise(MADV_COLLAPSE).

The cost is charged to PSI and to delay accounting

Direct compaction is wrapped in two accounting brackets (mm/page_alloc.c:3679):

psi_memstall_enter(&pflags);
delayacct_compact_start();
noreclaim_flag = memalloc_noreclaim_save();
 
*compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac, prio, &page);
 
memalloc_noreclaim_restore(noreclaim_flag);
psi_memstall_leave(&pflags);
delayacct_compact_end();

Time spent in direct compaction therefore counts as memory pressure stall in PSI — it shows up in /proc/pressure/memory exactly like reclaim stall does, indistinguishable there from swapping. It is also recorded separately by the delay-accounting subsystem (CONFIG_TASK_DELAY_ACCT), which is the only interface that reports per-task compaction delay in nanoseconds. That separation matters: PSI tells you that memory work is stalling the workload; delayacct is what tells you the stall is compaction rather than reclaim. Michal Hocko made exactly this argument at the 2024 LSFMM+BPF fragmentation session — that PSI “is measuring the amount of work that is needed to successfully allocate memory” and is the practical fragmentation metric, even if only “a ballpark measure” (LWN, “Measuring memory fragmentation”).

Note that the proactive path is deliberately not bracketed by psi_memstall_enter() — only kcompactd_do_work(), the woken-for-an-order path, is (mm/compaction.c:3185). Background defragmentation nobody asked for is not counted as pressure on anybody.

The capture mechanism

A naive direct-compaction pass might free a perfect high-order block only to have another CPU take it off the buddy free list before the original caller can look. The kernel closes that race with capture. In compact_zone_order():

struct capture_control capc = { .cc = &cc, .page = NULL };
barrier();
WRITE_ONCE(current->capture_control, &capc);
 
ret = compact_zone(&cc, &capc);
 
WRITE_ONCE(current->capture_control, NULL);
*capture = READ_ONCE(capc.page);
if (*capture)
	ret = COMPACT_SUCCESS;

The struct capture_control is hung off current. When the buddy free path is about to release a block of the requested order and finds a matching pending capture_control, it stuffs the page into capc->page instead of publishing it on the free list. The barrier() and the WRITE_ONCE/READ_ONCE ordering are not decoration: the comments state that without them an interrupt arriving mid-setup could free and capture a page into a half-initialised struct, or a page could be captured after the pointer was cleared and then leak.

The final if (*capture) ret = COMPACT_SUCCESS; is what makes the /proc/vmstat accounting honest — a pass that technically ended COMPACT_PARTIAL_SKIPPED but nonetheless handed the caller its page is recorded as a success, and compact_zone() also breaks out of its main loop the moment capc->page is non-NULL. Capture guarantees that the task which did the work is the task that gets the block.

Deciding to Start: compaction_suitable and the Watermark Gate

Compaction is not free, and running it when it cannot possibly help is worse than not running it at all. Two gates stand in front of every pass.

The outer gate is compaction_suit_allocation_order(), called from compact_zone() and from kcompactd_do_work(). It first asks whether the allocation would already succeed — if zone_watermark_ok() passes at the requested order, there is nothing to do and the answer is COMPACT_SUCCESS without a single page being touched. Otherwise it defers to compaction_suitable().

The inner gate, __compaction_suitable() (mm/compaction.c:2380), asks a question that catches people out: does the zone have enough free order-0 pages for compaction to have somewhere to put things?

watermark = (order > PAGE_ALLOC_COSTLY_ORDER) ?
			low_wmark_pages(zone) : min_wmark_pages(zone);
watermark += compact_gap(order);
return __zone_watermark_ok(zone, 0, watermark, highest_zoneidx,
			   ALLOC_CMA, wmark_target);

Read the arguments carefully. The order passed to __zone_watermark_ok() is 0, not order — this is not asking “can I allocate a huge page”, it is asking “are there enough loose single pages that the free scanner will find migration targets”. The threshold is the zone’s low watermark for costly orders and the stricter-to-clear min watermark for cheap ones, plus a headroom term:

static inline unsigned long compact_gap(unsigned int order)
{
	return 2UL << order;
}

The comment above it walks the reasoning: the free scanner may hold up to 1 << order pages on its list and then need to split an (order - 1) free page, at which point a gap of 1 << order is not enough, “so it’s safer to require twice that amount”. For an order-9 THP allocation compact_gap(9) is 1,024 pages = 4 MiB of slack demanded above the low watermark.

Making that concrete with the measurement box’s Normal zone (/proc/zoneinfo, and vm.min_free_kbytes = 67584):

QuantityPagesBytesMeaning
min watermark129,309505 MiBbelow this, only PF_MEMALLOC allocations proceed
low watermark161,633631 MiBkswapd wakes here
high watermark193,957758 MiBkswapd stops here
boost112,640440 MiBtemporary watermark boost from a recent fragmentation event
compact_gap(9)1,0244 MiBextra headroom compaction demands for an order-9 request
gate for an order-9 request162,657635 MiBlow + compact_gap(9); free pages must exceed this
actual free~4,449,000~17.0 GiBcomfortably above the gate — compaction is permitted

The watermark arithmetic that decides whether compaction may run at all, computed from live /proc/zoneinfo values on the measurement box. What it shows: the gate is a plain order-0 free-page floor, not a contiguity test. The insight to take: on a machine that is genuinely short of memory, compaction refuses to run and returns COMPACT_SKIPPED, which is the correct answer — with no spare pages there is nowhere to migrate anything to, and the right remedy is reclaim. This is also why compact_stall is not incremented for a COMPACT_SKIPPED result: nothing stalled, because nothing ran. Note the non-zero boost field: watermark boosting is the allocator’s response to a recent fragmentation event, temporarily raising the bar so kswapd frees more and gives compaction more room.

For costly orders there is a second test, and this is where vm.extfrag_threshold lives:

if (order > PAGE_ALLOC_COSTLY_ORDER) {
	int fragindex = fragmentation_index(zone, order);
	if (fragindex >= 0 && fragindex <= sysctl_extfrag_threshold) {
		suitable = false;
		compact_result = COMPACT_NOT_SUITABLE_ZONE;
	}
}

fragmentation_index() (mm/vmstat.c:1109) answers “if this allocation fails, is it because of fragmentation or because of a shortage?”:

if (!info->free_blocks_total)      return 0;
if (info->free_blocks_suitable)    return -1000;
return 1000 - div_u64((1000 + div_u64(info->free_pages * 1000ULL, requested)),
                      info->free_blocks_total);

Walking it symbol by symbol: requested is 1 << order; free_pages is every free page in the zone; free_blocks_total is the number of free blocks at any order; free_blocks_suitable counts blocks big enough for the request, expressed in units of 2^order. The middle term free_pages / requested is “how many allocations of this size the free memory could satisfy if it were perfectly contiguous”, and dividing that by the number of free blocks gives the average blocks-per-satisfiable-allocation. Subtracting from 1000 inverts the sense:

  • −1000 — a suitable block already exists; the request would succeed if watermarks allow. (The sysctl documentation renders this as “−1” because the debugfs file prints it in fixed point as -1.000.)
  • towards 0 — the zone has few free blocks relative to the free page count, i.e. the free memory is already well clumped and the failure is a shortage. Reclaim is the right tool.
  • towards 1000 — many small free blocks, plenty of free pages, no big ones: a fragmentation failure. Compaction is the right tool.

vm.extfrag_threshold defaults to 500 — the exact midpoint — and the kernel “will not compact memory in a zone if the fragmentation index is extfrag_threshold” (per Documentation/admin-guide/sysctl/vm.rst at v6.12). The per-order values are readable at /sys/kernel/debug/extfrag/extfrag_index, root-only. Note the guard order > PAGE_ALLOC_COSTLY_ORDER: non-costly orders skip this test entirely, because as the source comment says, “the alternative to a successful reclaim/compaction is OOM” and stability outranks the heuristic.

Deciding to Stop: compact_finished and the Result Codes

compact_zone()’s loop runs while compact_finished(cc) == COMPACT_CONTINUE. __compact_finished() (mm/compaction.c:2271) is where all four callers’ different notions of “done” are reconciled.

flowchart TD
  A["__compact_finished(cc)"] --> B{"compact_scanners_met(cc)?<br/>free_pfn >> pageblock_order<br/>&lt;= migrate_pfn >> pageblock_order"}
  B -->|yes| C["reset_cached_positions(zone)<br/>if direct: set compact_blockskip_flush<br/>so kswapd wipes the skip bits"]
  C --> D{"cc->whole_zone?"}
  D -->|yes| E["COMPACT_COMPLETE<br/>scanned everything, still failed"]
  D -->|no| F["COMPACT_PARTIAL_SKIPPED<br/>resumed mid-zone, still failed"]
  B -->|no| G{"cc->proactive_compaction?"}
  G -->|yes| H{"kswapd running<br/>on this node?"}
  H -->|yes| I["COMPACT_PARTIAL_SKIPPED<br/>back off, do not fight reclaim"]
  H -->|no| J{"fragmentation_score_zone()<br/>&gt; wmark_low?"}
  J -->|yes| K["COMPACT_CONTINUE"]
  J -->|no| L["COMPACT_SUCCESS<br/>score is back under control"]
  G -->|no| M{"is_via_compact_memory(order)<br/>i.e. order == -1?"}
  M -->|yes| N["COMPACT_CONTINUE<br/>manual pass: run to completion"]
  M -->|no| O{"pageblock_aligned<br/>(cc->migrate_pfn)?"}
  O -->|no| P["COMPACT_CONTINUE<br/>always finish the current pageblock"]
  O -->|yes| Q["scan orders cc->order .. MAX:<br/>free block of the right migratetype?<br/>or MOVABLE fallback onto CMA?<br/>or find_suitable_fallback() would steal?"]
  Q -->|"any hit"| R["COMPACT_SUCCESS"]
  Q -->|"nothing"| S["COMPACT_NO_SUITABLE_PAGE<br/>-> mapped to COMPACT_CONTINUE<br/>by compact_finished()"]

How a compaction pass decides it is finished, mm/compaction.c v6.12. What it shows: three completely different termination rules coexist in one function, selected by which flag the caller set. The insight to take: the direct-compaction branch (bottom right) does not wait for a perfect block — find_suitable_fallback() returning a hit means “an allocation of this order would now succeed by stealing from another migratetype”, and that counts as success. Compaction’s contract is “make the allocation succeed”, not “produce a pristine aligned block”. Also note the pageblock_aligned() guard: the pass will always finish the pageblock it is standing in, because leaving a half-processed block behind guarantees a fallback event later.

The result codes are worth having as a reference table, because they are what the tracepoints print and what should_compact_retry() branches on:

enum compact_resultMeaningWho returns itEffect on the caller
COMPACT_SKIPPEDcompaction did not start — not enough free order-0 pages, or reclaim was the better toolcompaction_suit_allocation_order()no compact_stall counted; THP faults with __GFP_NORETRY give up immediately
COMPACT_DEFERREDskipped because this zone recently failed at this ordertry_to_compact_pages() via compaction_deferred()same as above; the deferral countdown ticks
COMPACT_NOT_SUITABLE_ZONEfragmentation index ≤ extfrag_threshold — the failure is a shortage, not fragmentationcompaction_suitable() (tracepoint only)folded into COMPACT_SKIPPED
COMPACT_CONTINUEkeep goingcompact_finished()loop again
COMPACT_NO_SUITABLE_PAGEthis iteration produced nothing (internal, tracepoint only)__compact_finished()rewritten to COMPACT_CONTINUE
COMPACT_PARTIAL_SKIPPEDscanners met, but the pass had resumed from cached PFNs so part of the zone was never seen__compact_finished()defer_compaction(); retry at higher priority
COMPACT_COMPLETEthe entire zone was scanned and it still did not work__compact_finished()defer_compaction(); this zone is genuinely hopeless for now
COMPACT_CONTENDEDaborted on lock contention or a fatal signal__compact_finished(), compact_zone() on -ENOMEMstop trying further zones
COMPACT_SUCCESSan allocation of the requested order should now succeed__compact_finished(), or capturecompaction_defer_reset(); caller retries the allocation

The nine compact_result values of include/linux/compaction.h. What it shows: “compaction failed” is not one outcome but at least five, and the allocator treats them very differently. The insight to take: COMPACT_SKIPPED and COMPACT_DEFERRED are the quiet failures — they cost almost nothing and are invisible in compact_stall, compact_success, and compact_fail. A machine can be failing every huge-page allocation while all three /proc/vmstat counters sit still, because compaction is being skipped before it ever starts. If the counters look suspiciously calm on a fragmented box, that is the explanation, and the mm_compaction_try_to_compact_pages and mm_compaction_suitable tracepoints are the only way to see it.

Compaction Deferral — Not Trying Again Too Soon

If a zone has just been scanned end to end and produced nothing, scanning it again a microsecond later is pure waste. Compaction therefore rate-limits itself per zone, per order, with an exponential backoff (mm/compaction.c:151):

/* Do not skip compaction more than 64 times */
#define COMPACT_MAX_DEFER_SHIFT 6
 
static void defer_compaction(struct zone *zone, int order)
{
	zone->compact_considered = 0;
	zone->compact_defer_shift++;
	if (order < zone->compact_order_failed)
		zone->compact_order_failed = order;
	if (zone->compact_defer_shift > COMPACT_MAX_DEFER_SHIFT)
		zone->compact_defer_shift = COMPACT_MAX_DEFER_SHIFT;
}
 
static bool compaction_deferred(struct zone *zone, int order)
{
	unsigned long defer_limit = 1UL << zone->compact_defer_shift;
	if (order < zone->compact_order_failed)
		return false;
	if (++zone->compact_considered >= defer_limit) {
		zone->compact_considered = defer_limit;
		return false;
	}
	return true;
}

Three fields in struct zone carry the state. compact_defer_shift is the backoff exponent: after each failure the next 1 << compact_defer_shift requests are skipped outright, doubling each time up to 1 << 6 = 64 consecutive skips. compact_considered is the countdown. compact_order_failed records the lowest order that has failed, and the early return if (order < zone->compact_order_failed) return false; means a request for a smaller block is never deferred by a larger block’s failure — failing to build a 2 MiB block says nothing about whether a 32 KiB block is achievable.

stateDiagram-v2
  direction TB
  [*] --> Armed
  Armed --> Armed: compaction succeeded<br/>compaction_defer_reset<br/>shift = 0, considered = 0
  Armed --> Deferring: COMPACT_COMPLETE or<br/>COMPACT_PARTIAL_SKIPPED<br/>defer_compaction<br/>shift++, considered = 0
  Deferring --> Deferring: request arrives, considered++<br/>still under the limit of<br/>1 shifted left by shift<br/>return COMPACT_DEFERRED, skip
  Deferring --> Trying: considered reaches the limit<br/>let exactly one attempt through
  Trying --> Armed: the attempt succeeds<br/>compaction_defer_reset with<br/>alloc_success = true
  Trying --> Deferring: the attempt fails again<br/>shift++ capped at<br/>COMPACT_MAX_DEFER_SHIFT = 6<br/>i.e. up to 64 skips
  Deferring --> Amnesty: shift is 6 AND considered is 64<br/>compaction_restarting is true
  Amnesty --> Trying: __reset_isolation_suitable<br/>wipes every PB_migrate_skip bit<br/>then compact the whole zone again
  note right of Armed
    Armed = shift 0, considered 0.
    Every request is allowed through.
  end note

Per-zone, per-order compaction deferral in Linux 6.12. What it shows: a zone that keeps failing is progressively taken out of service, 1 skip, then 2, 4, 8, 16, 32, up to 64 consecutive skipped requests. The insight to take: the Amnesty transition is what keeps this from being a permanent write-off. At maximum deferral, the next attempt through does not merely retry — compaction_restarting() causes every skip hint in the zone to be erased first, so the retry sees the zone with fresh eyes. Deferral makes repeated failure cheap; amnesty makes it recoverable. Both try_to_compact_pages() and kcompactd_do_work() consult compaction_deferred(), so the daemon backs off on exactly the same schedule as the allocator; the one caller that ignores it entirely is the manual compact_memory path, which sets order = -1 and never enters this machinery.

One asymmetry is easy to miss. try_to_compact_pages() calls defer_compaction() only when prio != COMPACT_PRIO_ASYNC — a failed async attempt does not arm the backoff, because async gives up so readily that its failure carries little information. And compaction_defer_reset() is called with alloc_success = false from try_to_compact_pages() on COMPACT_SUCCESS (which only raises compact_order_failed) but with alloc_success = true from __alloc_pages_direct_compact() once a page is genuinely in hand (which zeroes the shift). The optimistic reset is deliberately weaker than the confirmed one.

kcompactd — The Per-Node Background Thread

Doing all compaction synchronously in the allocation path stalls whoever happens to trigger it, so since Linux 4.6 the kernel also runs kcompactd: one kernel thread per NUMA node, named kcompactd0, kcompactd1, … and pinned to that node’s CPU mask (set_cpus_allowed_ptr(tsk, cpumask_of_node(pgdat->node_id))). On the single-node measurement box there is exactly one, PID 221, which has accumulated 2 minutes 37 seconds of CPU time over 14 days 16 hours — about 0.012% of one core.

Its main loop (mm/compaction.c:3154) has two arms:

while (!kthread_should_stop()) {
	if (!sysctl_compaction_proactiveness)
		timeout = MAX_SCHEDULE_TIMEOUT;     /* proactive off: sleep forever */
 
	if (wait_event_freezable_timeout(pgdat->kcompactd_wait,
			kcompactd_work_requested(pgdat), timeout) &&
			!pgdat->proactive_compact_trigger) {
		psi_memstall_enter(&pflags);
		kcompactd_do_work(pgdat);           /* ARM 1: someone asked */
		psi_memstall_leave(&pflags);
		timeout = default_timeout;
		continue;
	}
 
	timeout = default_timeout;              /* ARM 2: periodic tick */
	if (should_proactive_compact_node(pgdat)) {
		prev_score = fragmentation_score_node(pgdat);
		compact_node(pgdat, true);
		score = fragmentation_score_node(pgdat);
		if (unlikely(score >= prev_score))
			timeout = default_timeout << COMPACT_MAX_DEFER_SHIFT;
	}
}

default_timeout is msecs_to_jiffies(HPAGE_FRAG_CHECK_INTERVAL_MSEC) and HPAGE_FRAG_CHECK_INTERVAL_MSEC is 500 — so with proactive compaction enabled the thread wakes at least twice a second to re-evaluate fragmentation. With compaction_proactiveness = 0 the timeout becomes MAX_SCHEDULE_TIMEOUT and the thread only ever wakes on an explicit request, costing nothing. The no-progress backoff at the bottom is the third of the three back-off conditions from the original design: if a proactive round did not lower the score, the next tick is pushed out by 500 ms << 6 = 32 seconds.

Who wakes it, and why the handoff exists

wakeup_kcompactd(pgdat, order, highest_zoneidx) has exactly three callers in 6.12, all of them in mm/vmscan.c — that is, all of them in reclaim:

sequenceDiagram
  participant T as Allocating task
  participant PA as __alloc_pages_slowpath()
  participant KS as kswapd (per node)
  participant KC as kcompactd (per node)
  participant Z as zone free lists

  T->>PA: order-9 allocation fails fast path
  PA->>KS: wake_all_kswapds(order, ...)
  Note over PA,KS: wakeup_kswapd() itself short-circuits:<br/>if the node is already balanced, it skips<br/>kswapd entirely and calls wakeup_kcompactd()<br/>directly (mm/vmscan.c:7286) - "plenty of free<br/>memory, but it's too fragmented"
  KS->>Z: balance_pgdat(): reclaim until watermarks met
  KS->>KC: wakeup_kcompactd(pgdat, pageblock_order, ...)<br/>(mm/vmscan.c:7036, after undoing watermark boost)
  KS->>KS: reset_isolation_suitable(pgdat)<br/>wipe every PB_migrate_skip bit
  KS->>KC: wakeup_kcompactd(pgdat, alloc_order, ...)<br/>(mm/vmscan.c:7099, just before sleeping)
  KC->>KC: kcompactd_node_suitable()? else go back to sleep
  KC->>Z: kcompactd_do_work(): MIGRATE_SYNC_LIGHT, per zone
  Note over KC,Z: count_compact_event(KCOMPACTD_WAKE)<br/>wrapped in psi_memstall_enter/leave
  PA->>Z: retry allocation - block may now be there

The kswapd-to-kcompactd handoff. What it shows: compaction’s background arm is driven entirely by reclaim, and the wake-up happens at three distinct moments — when a node is already balanced so reclaim would be pointless, after reclaim has finished undoing a watermark boost, and immediately before kswapd goes back to sleep. The insight to take: the ordering is the whole point. Reclaim runs first and creates free pages; compaction runs second and makes them contiguous. That is why reset_isolation_suitable() sits between the two — reclaim has just changed the facts on the ground, so every “this pageblock was hopeless” hint recorded earlier is now stale and must be discarded before compaction looks again.

Note also the short-circuit inside wakeup_kswapd() itself (mm/vmscan.c:7286): if the node is already balanced and not watermark-boosted, kswapd is not woken and wakeup_kcompactd() is called instead, with the comment “There may be plenty of free memory available, but it’s too fragmented for high-order allocations.” This is the cheapest possible response to a fragmentation-only failure, and it is gated on !(gfp_flags & __GFP_DIRECT_RECLAIM) — i.e. it is the path taken by allocations that cannot block, such as GFP_ATOMIC in an interrupt handler, which have no option but to hope the daemon fixes things before the next attempt.

kcompactd_do_work() is careful in three ways the direct path is not. It consults compaction_deferred() per zone and skips deferred ones. It calls drain_all_pages(zone) after a COMPACT_COMPLETE or COMPACT_PARTIAL_SKIPPED result, because freed pages stranded on per-CPU page lists cannot merge in the buddy allocator and would make the next compact_finished() check lie. And it clears pgdat->kcompactd_max_order only if the order it just worked on was at least as large as the pending request, so a higher-order request arriving mid-pass is not silently dropped.

Proactive Compaction — compaction_proactiveness

Reactive compaction is, by construction, late: it acts only after an allocation has already failed, or after kswapd has just finished a reclaim pass. For workloads that allocate huge pages in bursts — virtual machines booting, JVMs sizing a heap, databases reserving large regions — that lateness shows up directly as allocation latency. Proactive compaction, merged in Linux 5.9 (2020, Nitin Gupta; verified absent at v5.8 and present at v5.9), makes kcompactd compact before the demand arrives.

The measured case for it is unusually concrete. On a 1 TB, two-node x86-64 machine running 5.6.0-rc4, deliberately fragmented so that no huge page was directly allocatable, a test driver allocating as many huge pages as it could saw a 95th-percentile allocation latency of 33,799 µs on the stock kernel and 429 µs with the patch at proactiveness = 20 — a factor of 78. A Java workload allocating a 700 GB heap on the same fragmented system took ~27 minutes unpatched and roughly 4 minutes patched (Gupta, LWN, April 2020). Both kernels allocated about 98% of free memory as huge pages; what changed was not success rate but when the work happened. The cost is visible too: the same write-up records that “a kcompactd thread takes 100% of one of the CPU cores while it is active.”

The design is deliberately minimal, and that is a decision with a paper trail. At the 2017 LSFMM session on this topic, Michal Hocko proposed a watermark-plus-period configuration; Vlastimil Babka objected that administrators think in terms of “transparent huge page allocation rates or network throughput” and cannot be expected to translate that into free-page counts; Johannes Weiner concluded there was “value in an on/off switch” but that “any more tuning than that should be avoided” (LWN, “Proactive compaction”). The merged interface is exactly one integer.

Uncertain — resolved 2026-09-04

The 2020 LWN proposal describes the interface as /sys/kernel/mm/compaction/proactiveness. That path does not exist on a modern kernel: it was checked on the measurement box and /sys/kernel/mm/ contains cma damon hugepages ksm lru_gen mempolicy numa page_idle swap transparent_hugepage and no compaction directory. The merged form, present since v5.9 and confirmed live, is the sysctl /proc/sys/vm/compaction_proactiveness. The article text describes the pre-merge patch, not the shipped ABI. This callout is retained rather than deleted because the discrepancy is real and will trip up anyone reading the design write-up.

The fragmentation score, computed on a live machine

kcompactd decides when to act from a per-node fragmentation score in the range 0–100. Three functions build it (mm/compaction.c:22002245), and they are short enough to walk completely.

First, per zone — this is just extfrag_for_order() from mm/vmstat.c:1137 evaluated at the huge-page order:

extfrag(zone, order) = (free_pages − (free_blocks_suitable << order)) × 100 / free_pages

Symbol by symbol: free_pages is every free page in the zone; free_blocks_suitable is the number of free blocks of order ≥ order, counted in units of 2^order (an order-10 block counts as two order-9 slots); free_blocks_suitable << order is therefore the number of free pages that live in blocks big enough for the request. Subtracting and scaling gives the percentage of the zone’s free memory that is unusable for an allocation of this order. A score of 0 means every free page is in a big enough block; 100 means none of them are.

Second, weighted by the zone’s share of the node:

score = zone->present_pages * fragmentation_score_zone(zone);
return div64_ul(score, zone->zone_pgdat->node_present_pages + 1);

The comment states the purpose: “The scaling factor ensures that proactive compaction focuses on larger zones like ZONE_NORMAL, rather than smaller, specialized zones like ZONE_DMA32.” A badly fragmented 16 MiB ZONE_DMA is not worth waking a thread for. Third, fragmentation_score_node() sums the weighted zone scores.

Here is that arithmetic carried out on the measurement box, from the /proc/buddyinfo snapshot above and present from /proc/zoneinfo:

Zonefree pagesfree blocksorder-9 slots freeextfrag(zone, 9)presentweighted contribution
DMA2,8164593,9983998 × 9 / 33368658 = 0
DMA32133,4524,17621318473,587473587 × 18 / 33368658 = 0
Normal4,449,074503,7864,1215232,891,07232891072 × 52 / 33368658 = 51
node 0 total33,368,657score = 51

The node fragmentation score, recomputed by hand from live /proc data. What it shows: more than half the free memory on this machine is unusable for a 2 MiB allocation, yet the two small zones contribute literally nothing to the score because integer division by the node’s total page count flattens them to zero. The insight to take: with compaction_proactiveness = 20 the high watermark is 90, and 51 is comfortably below it — which is exactly why kcompactd0 on this box has burned only 2m37s in two weeks. The score is not a health metric to be minimised; a long-running general-purpose machine sitting at ~50 is normal and the kernel is right to leave it alone.

The hysteresis band

The thresholds come straight from the tunable (mm/compaction.c:2247):

static unsigned int fragmentation_score_wmark(bool low)
{
	unsigned int wmark_low;
	/* Cap the low watermark to avoid excessive compaction
	 * activity in case a user sets the proactiveness tunable
	 * close to 100 (maximum). */
	wmark_low = max(100U - sysctl_compaction_proactiveness, 5U);
	return low ? wmark_low : min(wmark_low + 10, 100U);
}

should_proactive_compact_node() starts a pass when the node score exceeds wmark_high; __compact_finished() returns COMPACT_SUCCESS and stops the pass once the zone score falls below wmark_low. The 10-point gap between them is the hysteresis that stops the daemon oscillating.

compaction_proactivenesswmark_lowwmark_highBehaviour
0disabled; kcompactd sleeps on MAX_SCHEDULE_TIMEOUT and never ticks
20 (default)8090intervenes only when ≥90% of free memory is unusable for huge pages
505060intervenes at the level the measurement box sits at today
802030aggressive; keeps fragmentation genuinely low, costs continuous CPU
95515the max(…, 5U) cap bites — 100−95 = 5, and it would be lower without it
100515identical to 95 because of the cap; “be careful when setting it to extreme values like 100”

How the single tunable maps to the two thresholds. What it shows: proactiveness is an inverted dial — raising it lowers the fragmentation the kernel will tolerate. The insight to take: the useful range is narrower than 0–100 suggests. Values above 95 do nothing further because of the max(…, 5U) floor, and the LWN evaluation found that “tests with higher proactiveness values did not show any further speedups or slowdowns” past 20 on that workload. If the default is not enough for you, the interesting experiment is 40–60, not 100.

The proactive pass itself is compact_node(pgdat, proactive=true) with order = -1, mode = MIGRATE_SYNC_LIGHT, ignore_skip_hint = true, whole_zone = true. It is subject to three back-off conditions, all present in the 6.12 source and all named in the original design (LWN 817905):

  1. kswapd is running on this nodeshould_proactive_compact_node() refuses to start, and __compact_finished() returns COMPACT_PARTIAL_SKIPPED mid-pass if kswapd wakes. Reclaim outranks defragmentation.
  2. Lock contention — contention on the per-node LRU lock or the per-zone lock aborts the pass through the ordinary cc->contended path, so background work never wins a lock fight against a latency-sensitive caller.
  3. No progress — if the node score did not fall after a round, the next tick is deferred to 500 ms << 6 = 32 s. This is the case where unmovable pages are scattered everywhere and, as the write-up puts it, “compaction cannot do much apart from warming the CPU.”

Writing a non-zero value to the sysctl triggers an immediate pass via compaction_proactiveness_sysctl_handler(), which sets pgdat->proactive_compact_trigger and wakes each node’s daemon — so sysctl -w vm.compaction_proactiveness=20 on a box that is already at 20 is a way to force one proactive round without the blunt compact_memory hammer.

Manual Triggers and the Complete Set of Knobs

For one-shot defragmentation there is /proc/sys/vm/compact_memory. Its handler is strict about its input:

static int sysctl_compaction_handler(...)
{
	ret = proc_dointvec(table, write, buffer, length, ppos);
	if (ret) return ret;
	if (sysctl_compact_memory != 1) return -EINVAL;   /* only "1" is accepted */
	if (write) ret = compact_nodes();
	return ret;
}

compact_nodes() calls lru_add_drain_all() first — an IPI to every CPU to flush its per-CPU LRU batches, so that recently-touched pages are actually visible to the migrate scanner — and then compact_node(NODE_DATA(nid), false) for every online node. Recall from the caller table that proactive = false means MIGRATE_SYNC, the only mode in the kernel that will block waiting for page writeback:

echo 1 | sudo tee /proc/sys/vm/compact_memory     # every zone, every node, fully synchronous
echo 1 | sudo tee /sys/devices/system/node/node0/compact   # one node only

The per-node form goes through compact_store() and is otherwise identical. Both are blunt instruments: a full whole-zone MIGRATE_SYNC pass on a large, fragmented, actively-writing machine can take a long time and will stall on I/O. The legitimate uses are preparing for a large hugetlbfs reservation, and reproducing compaction behaviour deliberately while testing.

KnobTypeDefaultWhat it controls
/proc/sys/vm/compact_memorywrite-only, 1run a full MIGRATE_SYNC, whole-zone, all-nodes pass now; returns -EINVAL for any value but 1
/sys/devices/system/node/nodeN/compactwrite-onlythe same, restricted to one NUMA node (CONFIG_SYSFS && CONFIG_NUMA)
/proc/sys/vm/compaction_proactivenessint 0–10020background aggressiveness; wmark_low = max(100−v, 5), wmark_high = wmark_low+10; writing non-zero also triggers one pass immediately
/proc/sys/vm/extfrag_thresholdint 0–1000500for costly orders only: skip compaction when fragmentation_index ≤ threshold, i.e. when the failure looks like a shortage rather than fragmentation
/proc/sys/vm/compact_unevictable_allowedbool1 (0 on CONFIG_PREEMPT_RT)whether the migrate scanner may isolate mlocked / unevictable pages (ISOLATE_UNEVICTABLE)
/sys/kernel/mm/transparent_hugepage/defragenum[madvise]how hard a THP fault compacts — see below
/proc/sys/vm/min_free_kbytesintsized from RAMraises every zone watermark, which raises the compaction_suitable() gate and gives the free scanner more to work with
/sys/kernel/debug/extfrag/extfrag_indexread-only, rootper-zone, per-order fragmentation index; the direct view of what extfrag_threshold is compared against
/sys/kernel/debug/extfrag/unusable_indexread-only, rootper-zone, per-order fraction of free memory unusable at that order — the same quantity as the fragmentation score

Every user-visible control over compaction on Linux 6.12. What it shows: there are exactly two “do it now” triggers, one background dial, and three policy thresholds — and no knob at all for how hard direct compaction tries. The insight to take: the absence is deliberate. Direct compaction’s effort is fixed by compact_priority escalation in the allocator, and the supported way to reduce direct-compaction latency is to shift work to the background with compaction_proactiveness, not to make the foreground path try less. compact_unevictable_allowed = 0 is the one setting worth changing on a latency-critical box: it stops compaction from touching mlocked memory, at the cost of some achievable contiguity, and it is already the default under PREEMPT_RT for exactly that reason.

Interaction with THP and Huge-Page Allocation

Compaction’s dominant consumer is huge-page allocation. The full THP story lives in Transparent Huge Pages and is not repeated here; what matters for compaction is the single translation step where the defrag policy becomes GFP flags, because those flags are what steer the allocator slow path shown earlier. That translation is vma_thp_gfp_mask() (mm/huge_memory.c:1231), and it is worth reading as a table.

defrag settingInternal flagGFP mask returned by vma_thp_gfp_mask()Compaction behaviour on a THP fault
alwaysDEFRAG_DIRECT_FLAGGFP_TRANSHUGE (plus __GFP_NORETRY if the VMA is not MADV_HUGEPAGE)direct compaction and reclaim; the faulting task stalls
deferDEFRAG_KSWAPD_FLAGGFP_TRANSHUGE_LIGHT | __GFP_KSWAPD_RECLAIMnever stall; wake kswapd + kcompactd and use base pages now
defer+madviseDEFRAG_KSWAPD_OR_MADV_FLAGGFP_TRANSHUGE_LIGHT | (madvised ? __GFP_DIRECT_RECLAIM : __GFP_KSWAPD_RECLAIM)stall only for MADV_HUGEPAGE regions; background for everything else
madvise (compiled-in default)DEFRAG_REQ_MADV_FLAGGFP_TRANSHUGE_LIGHT | (madvised ? __GFP_DIRECT_RECLAIM : 0)direct compaction only for MADV_HUGEPAGE; others get no compaction at all
nevernone setGFP_TRANSHUGE_LIGHTno compaction on fault; madvise(MADV_COLLAPSE) can still force one

How the THP defrag policy becomes an allocation flag, verified against mm/huge_memory.c at v6.12. What it shows: defrag does not talk to compaction directly — it sets __GFP_DIRECT_RECLAIM or __GFP_KSWAPD_RECLAIM, and the allocator slow path does the rest. __GFP_DIRECT_RECLAIM is what makes can_direct_reclaim true and therefore lets __alloc_pages_direct_compact() run at all. The insight to take: madvise is the compiled-in default (transparent_hugepage_flags is initialised with DEFRAG_REQ_MADV_FLAG set, mm/huge_memory.c:68), and it means a plain unmarked mapping does zero compaction work on fault — it takes a huge page only if one happens to be sitting on the free list. That is why thp_fault_fallback being non-zero is normal and by itself not a problem. Only always and defer+madvise-on-madvised-VMAs put compaction in the fault path, which is exactly where the latency complaints come from.

Two further consumers are worth naming. khugepaged retroactively collapses runs of base pages into huge pages and needs the same contiguous targets, so it leans on compaction indirectly through its own allocations. And gigantic pages — order-18, 1 GiB — are not served by the buddy allocator at all: MAX_PAGE_ORDER is 10 on x86-64, so alloc_gigantic_folio() (mm/hugetlb.c:1518) goes to CMA (cma_alloc_folio()) or to alloc_contig_range() under CONFIG_CONTIG_ALLOC, and the boot-time reservation path uses memblock_alloc_try_nid_raw() before the buddy allocator is even running. alloc_contig_range() internally uses the same isolate-and-migrate machinery as compaction, but with a fixed target range rather than an opportunistic scan — see hugetlbfs and Reserved Huge Pages.

Observability — Counters, PSI, and Tracepoints

/proc/vmstat, walked concretely

Seven counters are incremented from the compaction code (mm/compaction.c and mm/page_alloc.c). Here they are as sampled from the measurement box, after 14 days 16 hours of ordinary desktop and build workload:

$ grep -E '^compact_' /proc/vmstat
compact_migrate_scanned        2195523291
compact_free_scanned           5468096527
compact_isolated                129327110
compact_stall                        9759
compact_fail                         3090
compact_success                      6669
compact_daemon_wake                126147
compact_daemon_migrate_scanned 2185274650
compact_daemon_free_scanned    5457245271
CounterIncremented atWhat it means
compact_stallmm/page_alloc.c:3696, after try_to_compact_pages() returns anything but COMPACT_SKIPPEDa task actually ran direct compaction and waited for it
compact_successmm/page_alloc.c:3711that stall ended with a page in hand
compact_failmm/page_alloc.c:3719that stall ended with nothing
compact_migrate_scanned / compact_free_scannedmm/compaction.c:2733 at the end of every compact_zone()total pages examined by each scanner — the cost metric
compact_isolatedthree sites in the isolation pathspages actually pulled off the LRU or the buddy lists
compact_daemon_wakemm/compaction.c:3066 in kcompactd_do_work()how often kcompactd was woken for a specific order (the proactive tick does not count)
compact_daemon_migrate_scanned / compact_daemon_free_scannedmm/compaction.c:3107, and from the proactive path at :2913the kcompactd share of the two scan totals

Now read them, because the arithmetic says things prose cannot:

  • compact_stall == compact_success + compact_fail exactly: 6,669 + 3,090 = 9,759. This is not a coincidence, it is guaranteed by __alloc_pages_direct_compact(), which increments COMPACTSTALL and then takes exactly one of the two other branches. If you ever see the identity violated, you are looking at a torn read across the per-CPU vm event counters, not a bug.
  • Direct-compaction success rate: 6,669 / 9,759 = 68.3%. The complementary failure rate, compact_fail / (compact_success + compact_fail) = 31.7%, is the headline health number. Rising toward 100% means the zone is full of unmovable obstructions that no amount of scanning will fix.
  • compact_daemon_migrate_scanned / compact_migrate_scanned = 2,185,274,650 / 2,195,523,291 = 99.53%. Essentially all compaction work on this machine is done by kcompactd; direct compaction accounts for under half a percent of pages scanned. This is the single most useful ratio in the set, because it tells you whether compaction cost is being paid in the background (fine) or in application context (a latency problem).
  • Isolation yield. compact_isolated is incremented from both scanners (mm/compaction.c:707 in isolate_freepages_block(), :1337 in isolate_migratepages_block(), :1643 in fast_isolate_freepages()), so it must be compared against the sum of the two scan totals: 129,327,110 isolated out of 7,663,619,818 scanned = 1.7%, or about 59 pages examined per page isolated. That is the concrete price of scanning, and it is why the two *_scanned counters are the cost side of the ledger while compact_isolated is the work side.
  • compact_daemon_wake = 126,147 over 14.7 days ≈ 8,580 wake-ups per day ≈ one every 10 seconds, against 2m37s of kcompactd0 CPU time — so the average woken pass is roughly 1.2 ms of CPU. Background compaction is cheap per event; it is only expensive when a proactive pass runs to completion on a large zone.
flowchart LR
  subgraph DIRECT["Direct compaction — in the task's context"]
    D1["try_to_compact_pages()"] --> D2{"COMPACT_SKIPPED<br/>or DEFERRED?"}
    D2 -->|yes| D3["no counter moves at all<br/>INVISIBLE in vmstat"]
    D2 -->|no| D4["compact_stall++"]
    D4 --> D5{"page obtained?"}
    D5 -->|yes| D6["compact_success++"]
    D5 -->|no| D7["compact_fail++"]
  end
  subgraph DAEMON["kcompactd — background"]
    K1["kcompactd_do_work()"] --> K2["compact_daemon_wake++"]
    K2 --> K3["compact_daemon_migrate_scanned +=<br/>compact_daemon_free_scanned +="]
    K4["compact_node(proactive)"] --> K3
  end
  D1 --> S["compact_migrate_scanned +=<br/>compact_free_scanned +=<br/>compact_isolated +=<br/>(every compact_zone() call,<br/>whoever made it)"]
  K1 --> S
  K4 --> S
  S --> R["compact_daemon_* / compact_* =<br/>fraction of the cost paid<br/>OFF the application's critical path"]

Which code path moves which /proc/vmstat counter. What it shows: the totals are shared between direct and daemon compaction, while compact_stall/success/fail are direct-only and compact_daemon_* are daemon-only. The insight to take: the compact_daemon_migrate_scanned / compact_migrate_scanned ratio is a derived metric the kernel does not expose but that you should compute — it separates “compaction is busy” from “compaction is hurting my application”. And note the top-left dead end: a COMPACT_SKIPPED or COMPACT_DEFERRED result moves nothing, so a machine failing every huge-page allocation can show flat compaction counters. Flat counters are not proof that compaction is healthy.

PSI and delay accounting

Direct compaction is bracketed by psi_memstall_enter()/psi_memstall_leave(), so it contributes to /proc/pressure/memory — but so does reclaim, and PSI cannot distinguish them. Delay accounting can: delayacct_compact_start()/delayacct_compact_end() wrap the same region, and with CONFIG_TASK_DELAY_ACCT and kernel.task_delayacct=1 the per-task compaction delay is readable through the taskstats netlink interface (the in-tree tools/accounting/getdelays utility prints it). This is the only per-task attribution of compaction cost the kernel offers.

The kcompactd on-demand path is also PSI-bracketed; the proactive path deliberately is not. Background defragmentation that nobody requested is not charged as pressure against anybody.

Tracepoints

When the counters are not enough — in particular when compaction is being skipped and therefore invisible — include/trace/events/compaction.h defines fifteen tracepoints:

# what is the allocator asking for, and at what priority?
trace-cmd record -e compaction:mm_compaction_try_to_compact_pages \
                 -e compaction:mm_compaction_suitable \
                 -e compaction:mm_compaction_finished \
                 -e compaction:mm_compaction_deferred \
                 -e compaction:mm_compaction_defer_compaction
 
# how much did each pass actually move?
trace-cmd record -e compaction:mm_compaction_begin \
                 -e compaction:mm_compaction_end \
                 -e compaction:mm_compaction_migratepages

mm_compaction_suitable and mm_compaction_finished both print a decoded compact_result, which is the direct answer to “why did nothing happen”. mm_compaction_begin/end carry the four PFN cursors, so the pair tells you how much of the zone a pass covered. mm_compaction_deferred firing repeatedly is the signature of a zone stuck in the backoff state machine. mm_compaction_kcompactd_sleep and the two kcompactd_wake_template events (mm_compaction_wakeup_kcompactd, mm_compaction_kcompactd_wake) trace the daemon’s duty cycle.

Finally, /sys/kernel/debug/extfrag/unusable_index gives the fragmentation score per zone per order directly, without the hand arithmetic done earlier — worth using when the question is “at which order does this machine fall off a cliff”, since compaction only ever optimises for one order at a time while a real workload spans several.

Failure Modes and Common Misunderstandings

“Compaction frees memory.” It does not. Compaction is zero-sum on free-page count — every frame of the filmstrip at the top has the same number of free pages. If you are short of free pages you need reclaim, not compaction, and the kernel agrees: compaction_suitable() will refuse to run at all below the watermark gate. The two cooperate — reclaim creates free pages, compaction makes them contiguous — and the wakeup_kcompactd() calls in mm/vmscan.c are that cooperation written down.

Unmovable pages cap effectiveness, and one is enough. Kernel slab objects, page tables, and pages with elevated reference counts — those pinned by get_user_pages for DMA or RDMA, for instance — cannot be migrated. A pageblock containing even one such page can never be fully freed, which is frame 4 of the filmstrip and which “only takes one non-movable page to ruin a contiguous segment of memory” (LWN 368869). Long-term pinning is therefore corrosive out of all proportion to the memory it holds: pinning a single page in each of a thousand pageblocks costs 2 GiB of achievable contiguity while accounting for 4 MiB of RSS.

Async compaction giving up early is a feature, not a bug. MIGRATE_ASYNC skips anything it cannot process without blocking, refuses source pageblocks of the wrong migratetype, and abandons an order-aligned sub-block on the first isolation failure (skip_on_failure). A THP fault under the default defrag=madvise on an unmarked VMA does not even reach compaction — vma_thp_gfp_mask() returns a mask without __GFP_DIRECT_RECLAIM. The kernel chose latency over the huge page, deliberately.

Flat compaction counters do not mean compaction is healthy. COMPACT_SKIPPED and COMPACT_DEFERRED move no /proc/vmstat counter at all. A zone deep in the deferral state machine, or one failing the compaction_suitable() watermark gate, will fail every huge-page allocation while compact_stall, compact_success, and compact_fail all sit still. The mm_compaction_suitable and mm_compaction_deferred tracepoints are the only way to see this.

Lock contention and the COMPACT_CLUSTER_MAX cadence. Both scanners hold the zone lock or the LRU lock while isolating, releasing every 32 pages. On a large NUMA box with many concurrent allocators this becomes a bottleneck, visible as time in isolate_migratepages_block in a profile, and as a rising COMPACT_CONTENDED rate in the mm_compaction_finished tracepoint. too_many_isolated() is the other side of the same problem: when parallel reclaimers and compactors have already isolated too much of the LRU, an async compactor returns -EAGAIN immediately and a sync one calls reclaim_throttle(pgdat, VMSCAN_THROTTLE_ISOLATED).

Every migration is a TLB shootdown. The cost of compaction is not only the scanning and the memcpy. Moving a mapped page means unmapping it from every address space that has it, which means an IPI to every CPU with that mapping cached. On a large machine with widely shared page cache this is the dominant cost, and it is charged to whoever is running on those CPUs, not to the compactor. This is what “compaction has a non-trivial system-wide impact as pages belonging to different processes are moved around, which could also lead to latency spikes in unsuspecting applications” means in the sysctl documentation.

echo 1 > /proc/sys/vm/compact_memory is the only path that can block on disk. It is the sole caller that sets MIGRATE_SYNC. Running it on a busy machine with a slow backing device reproduces exactly the 2011 desktop-freeze pathology, which the automatic paths were specifically redesigned to avoid. It is a diagnostic and preparation tool, not something to put in a cron job.

High compaction_proactiveness is a CPU-versus-fragmentation trade, and the CPU is real. The original evaluation measured kcompactd at 100% of one core while a proactive pass was active. The no-progress back-off (defer to 32 s) limits the damage when compaction cannot help, but on a latency-sensitive box the migrations themselves — copies and shootdowns — land at times nobody chose.

A diagnostic table

SymptomLikely causeHow to confirmWhat to do
compact_stall climbing fast, application latency spikesdirect compaction in the fault pathcompact_daemon_migrate_scanned / compact_migrate_scanned well below 1; /proc/pressure/memory some risingdefrag=madvise or defer+madvise; raise compaction_proactiveness to move the work to kcompactd
compact_fail / (success + fail) near 1unmovable pages salted through the zone/proc/pagetypeinfo shows Unmovable blocks everywhere; compact_isolated low relative to scannednothing tunable will fix this — reduce slab/page-table pressure, avoid long-term pinning, or add RAM
Huge-page allocations failing but all compaction counters flatCOMPACT_SKIPPED or COMPACT_DEFERRED — compaction never ranmm_compaction_suitable and mm_compaction_deferred tracepointsif SKIPPED: free pages are below the compaction_suitable() gate, so reclaim (raise min_free_kbytes). If DEFERRED: the zone is in backoff after real failures
kcompactd0 pegged at 100% of a corea proactive pass running to completion on a large zonecompact_daemon_* counters climbing steeply; node score above wmark_highlower compaction_proactiveness, or accept it — the no-progress back-off will defer by 32 s if it is achieving nothing
Multi-second system-wide stall during a manual compactionMIGRATE_SYNC blocking on writebackit was a write to compact_memory or a node’s compact filedo not run those on a busy machine with slow storage
Swap storms on one NUMA node when a THP-heavy job starts__GFP_THISNODE plus compaction on a constrained nodeper-node pgscan/pgsteal concentrated on one nodeensure the kernel has the mm/mempolicy.c local-first-then-remote logic; reconsider MADV_HUGEPAGE on the mapping
thp_fault_fallback large but no latency complaintdefrag=madvise on unmarked VMAs — compaction never attemptedcat /sys/kernel/mm/transparent_hugepage/defrag shows [madvise]; compact_stall flatnothing — this is the intended default behaviour

Compaction symptoms and their causes. What it shows: the same three /proc/vmstat counters point at completely different problems depending on their ratios, and two of the seven rows are diagnosed by what the counters do not say. The insight to take: the first question in any compaction investigation is “is this cost landing in the background or in application context”, answered by the compact_daemon_* ratio, and the second is “did compaction even run”, answered only by tracepoints. Reaching for compaction_proactiveness before answering both is guesswork.

Production Notes

The 2011 desktop-freeze incident is the origin of MIGRATE_SYNC_LIGHT. Writing a large file to a slow USB device would fill memory with dirty pages; a browser’s THP fault would then enter synchronous compaction, encounter a page under writeback to that device, and sleep on the I/O — repeatedly, since building one huge page can require migrating hundreds of ordinary ones. Users saw multi-minute desktop lockups. Mel Gorman’s fix was a one-liner declining synchronous compaction for THP allocations, and it was contested: Andrew Morton objected that “some people would prefer to get lots of huge pages for their 1000-hour compute job, and waiting a bit to get those pages is acceptable” (LWN, November 2011). The eventual settlement is the whole defrag policy menu plus the MIGRATE_SYNC_LIGHT mode — never wait on ->writepage, wait on everything else.

The 2018–2019 THP/NUMA swap storms. Since a 2015 change, madvise(MADV_HUGEPAGE) regions attempted huge-page allocation exclusively on the local NUMA node, and the kernel tried so hard that it would drive aggressive reclaim and compaction on that node while other nodes had free memory — in effect turning MADV_HUGEPAGE into a node binding, with severe swap storms as the result. Andrea Arcangeli’s loosening patch was applied in November 2018, reverted in December after objections that some workloads genuinely want the binding, and then re-landed after distributions independently reverted the revert (LWN, “Dueling memory-management performance regressions”). The v6.12 code is the compromise, and it is worth reading because the comment states the policy outright (mm/mempolicy.c:2249):

/*
 * First, try to allocate THP only on local node, but
 * don't reclaim unnecessarily, just compact.
 */
page = __alloc_pages_node_noprof(nid,
	gfp | __GFP_THISNODE | __GFP_NORETRY, order);
if (page || !(gfp & __GFP_DIRECT_RECLAIM))
	return page;
/*
 * If hugepage allocations are configured to always
 * synchronous compact or the vma has been madvised
 * to prefer hugepage backing, retry allowing remote
 * memory with both reclaim and compact as well.
 */

Local-node first with __GFP_NORETRY so it compacts but does not thrash; then, only for callers that asked for direct reclaim, a second attempt with remote nodes allowed. The lesson generalises: compaction is dangerous precisely where it is combined with a hard placement constraint, because the constraint removes the cheap fallback that normally bounds its cost.

Compaction bugs are notoriously hard to reproduce. At the 2014 LSFMM summit, Vlastimil Babka led a session on compaction overhead in which the conclusion was essentially an admission: “the memory management developers don’t really have a good understanding of why compaction problems are happening”, because the pathologies “arise out of specific workloads that exercise the system in certain ways” and there is “no easy way to abstract the problematic access patterns out of the workloads into separate test programs” (LWN, “Memory compaction issues”). The proposed remedy — a counter incremented whenever the kernel notices it has spent significant time in compaction — is essentially what compact_migrate_scanned and compact_free_scanned provide today, and it is why they are worth graphing over time rather than reading once.

The 2020 proactive-compaction numbers remain the best published quantification of the latency this all exists to remove: 95th-percentile huge-page allocation latency of 33,799 µs falling to 429 µs, and a 700 GB Java heap allocation falling from ~27 minutes to ~4, on a deliberately fragmented 1 TB two-node machine (LWN 817905). Vlastimil Babka’s review comment on that series is the operational advice worth keeping: a one-shot fragmentation event followed by a burst of allocations is a smoke test, and what actually matters is “behavior under more complex workloads, where we should also check the vmstat compact_daemon* stats and possibly also kcompactd kthreads CPU utilizations” — which is precisely the ratio computed in the observability section above.

Measuring fragmentation remains unsolved. At the 2024 LSFMM+BPF summit the memory-management developers could not agree on a metric. Yu Zhao argued that fragmentation “is a two-dimensional problem that cannot be described by a single number”; Michal Hocko argued that PSI already answers the practically useful question by measuring “the amount of work that is needed to successfully allocate memory”, while conceding it is “a ballpark measure”; the session ended without consensus (LWN, “Measuring memory fragmentation”). The takeaway for an operator is that no single number is going to tell you whether a machine is “too fragmented” — the workable approach is the combination this note uses: the node fragmentation score for the state, compact_daemon_* versus compact_* for where the cost lands, compact_fail/(success+fail) for whether the work is paying, and PSI for whether any of it is reaching the application.

The direction of travel inverts the premise. At the same 2024 summit, Yu Zhao argued that the kernel’s current economy — “4KB pages are cheap, and huge pages are expensive” — is backwards, and that the cornerstone of his THP allocation optimisation proposal is “making huge pages cheap, and 4KB pages expensive”, on the grounds that “some CPU vendor is planning to drop 4KB pages entirely within the next decade” (LWN, “Allocator optimizations for transparent huge pages”). Under that model compaction stops being an occasional repair and becomes a permanent background service. The other pressure is multi-size THP: compaction was designed around a single huge-page order — that assumption is literally the constant COMPACTION_HPAGE_ORDER, which the fragmentation score is computed against — while an mTHP workload wants several orders at once. Nothing in the 6.12 mechanism described here has been removed, but that single-order assumption is the part most likely to change.

Uncertain

Verify: how compaction policy has changed in the 7.x series, particularly whether COMPACTION_HPAGE_ORDER remains a single order under multi-size THP. Reason: this note is pinned to 6.12 LTS and the mTHP work post-dates it; the measurement box runs 7.1.8 but its counters were read, not its source. To resolve: diff mm/compaction.c between v6.12 and the current 7.x tag. Note also that lore.kernel.org was unreachable during this research — it returns an Anubis proof-of-work bot challenge to curl rather than the archive — so the patch-series discussion for anything post-6.12 could not be read directly. uncertain

MechanismGuaranteeCostUse when
Anti-fragmentation grouping (Page Order and Fragmentation)none — best-effort preventionfree; it is just allocator bookkeepingalways on; it is what makes compaction tractable at all
Compaction (this note)none — best-effort cureCPU, page copies, TLB shootdowns, possible latencycontiguity is wanted but a base-page fallback exists
Proactive compactionnone, but shifts cost off the critical pathcontinuous background CPUhuge-page allocation latency matters and the box has CPU headroom
hugetlbfs pre-reservation (hugetlbfs and Reserved Huge Pages)hard — reserved at boot from pristine memorythe memory is unavailable for anything else, forevera known, fixed huge-page requirement (databases, VMs)
CMA — Contiguous Memory Allocatorhard for movable-only regionsa reserved region that only accepts movable allocations, and historically a source of its own pathologies — the kernel tending to avoid the CMA area and entering reclaim earlier than it should (LWN, LSFMM 2016)a device driver needs large DMA buffers on demand
alloc_contig_range()best-effort over a specific rangesame as compaction plus isolation of the rangegigantic pages, CMA, memory offlining
vmalloc() (vmalloc and Virtually Contiguous Memory)hard for virtual contiguity onlyTLB pressure; useless to devicesthe caller needs a big buffer but not physical contiguity
More RAM / higher min_free_kbytesnonememorythe real problem is headroom, and it often is

The full menu for getting contiguous memory, ordered from weakest to strongest guarantee. What it shows: compaction sits in the middle — better than hoping, weaker than reserving. The insight to take: the first question to ask is not “how do I make compaction work better” but “does this caller actually need a guarantee”. If it does, compaction is the wrong tool at any tuning, because it is best-effort by construction; reserve instead. If it does not, the fallback path already exists and the cost of compaction is optional. vmalloc() is the frequently-forgotten answer: a great many kernel callers that reach for a high-order allocation only need virtual contiguity and can be rewritten to not create the problem at all — which is, per the 2006 LWN survey, exactly how the kernel has coped with fragmentation for most of its history.

See Also

  • Page Migration — the page-copy-and-remap primitive compaction calls via migrate_pages(). Compaction supplies the source list and the destination allocator (compaction_alloc/compaction_free); everything about unmapping, migration entries, and remapping lives there. MR_COMPACTION is how compaction’s migrations are distinguished from hotplug’s, NUMA balancing’s, and CMA’s in the tracepoints.
  • Page Order and Fragmentation — the buddy allocator’s pageblock-based anti-fragmentation grouping, find_suitable_fallback(), and watermark boosting. Prevention to compaction’s cure; the two are complementary, not alternatives.
  • The Buddy Allocator — the high-order allocator whose failures trigger compaction, whose free lists the free scanner raids, and whose free path implements capture.
  • Memory Zones and Nodes — compaction operates per-zone (compact_zone()); kcompactd and the fragmentation score are per-node; the weighting in fragmentation_score_zone_weighted() only makes sense once you know why ZONE_DMA32 exists.
  • Transparent Huge Pages — the principal consumer. The defrag policy, vma_thp_gfp_mask(), and the thp_fault_* counters are covered there in depth; this note covers only the point where policy becomes GFP flags.
  • khugepaged and THP Collapse — the retroactive path to huge pages, which needs the same contiguity and can be forced by madvise(MADV_COLLAPSE) when compaction on fault is disabled.
  • hugetlbfs and Reserved Huge Pages — the pre-reservation alternative, and the home of alloc_gigantic_folio() and alloc_contig_range() for orders the buddy allocator cannot serve.
  • Memory Reclaim Overview — the free-page-shortage counterpart. Read it for the other half of the coupled system: reclaim creates the free pages compaction rearranges, and the wakeup_kcompactd() calls that drive background compaction all live in mm/vmscan.c.
  • Direct Reclaim / kswapd and Background Reclaim — the direct/background pair that compaction’s direct/kcompactd pair mirrors exactly, down to the deferral and the PSI accounting.
  • Memory Hotplug — another migrate_pages() consumer; offlining is conceptually “compaction that must empty a specific region entirely, or fail”.
  • Pressure Stall Information — where direct-compaction stall time actually surfaces, and the metric the memory-management developers currently recommend for fragmentation cost.
  • Shrinkers and Slab ReclaimMIGRATE_RECLAIMABLE pageblocks hold shrinker-backed slabs, the one class of unmovable memory that can at least be freed on demand.
  • get_user_pages and Page Pinning — long-term pinning is the most durable way user space can defeat compaction.
  • vmalloc and Virtually Contiguous Memory — the escape hatch for callers that need a large buffer but not physical contiguity, and therefore never create work for compaction.
  • MOC: Linux Memory Management MOC (§15 Advanced and Cross-Cutting MM Facilities).