On this page

Page Reclaim and Swap

Coverage: LRU (anonymous/file) → kswapd + direct reclaim → swap mechanism → reverse mapping (rmap) → page cache reclaim → compaction Kernel versions: 2.6 ~ 6.x, with emphasis on multi-gen LRU (6.1+)

Overview

When physical memory is insufficient, the kernel must reclaim some pages. Reclaimable objects fall into two categories:

  • File-backed pages: Clean pages can be dropped directly (as they have a disk copy), while dirty pages must be written back.
  • Anonymous pages: Must be written to swap before they can be reclaimed.

The core of the reclaim strategy is LRU (Least Recently Used)—swapping out cold pages while retaining hot ones. However, determining "hotness" and "coldness" is extremely difficult within a working set of hundreds of gigabytes. Linux has evolved from traditional doubly-linked list LRU to the Multi-Gen LRU (MGLRU) introduced in version 6.1, which significantly improves handling of large working sets and scanning storms.


LRU Lists

Traditional Dual LRU (2.6 ~ 5.x)

// mm/vmscan.c
// Each memcg and each node has a set of LRUs:

enum lru_list {
    LRU_INACTIVE_ANON,  // Cold anonymous pages (not accessed for a long time)
    LRU_ACTIVE_ANON,    // Hot anonymous pages
    LRU_INACTIVE_FILE,  // Cold file pages
    LRU_ACTIVE_FILE,    // Hot file pages
    LRU_UNEVICTABLE,    // Unreclaimable (mlock, ramfs)
    NR_LRU_LISTS
};

// Pages move between active and inactive lists:
//   First mapping → inactive
//   Accessed again → promoted to active
//   Not accessed for a long time → demoted to inactive
//   Pages in inactive list → reclaim candidates

Reclaim Priority

// Reclaim order (file pages take precedence over anonymous pages):
//   1. Clean file pages (drop, fastest)
//   2. Dirty file pages (writeback)
//   3. Anonymous pages (swap out)
//
// Why do file pages take precedence?
//   Clean file pages: Dropped directly, 0 IO
//   Anonymous pages:  Must be written to swap → IO overhead
//   Dirty file pages: Must be written back → IO overhead
//
// /proc/sys/vm/swappiness controls this priority:
//   0:   Prefer reclaiming file pages; only swap if absolutely necessary
//   100: Treat file and anonymous pages equally
//   Default is 60

Multi-Gen LRU (6.1+)

Problems with traditional LRU:
  - Only two generations (active/inactive) → coarse granularity
  - Hot/cold judgment relies on the referenced bit + periodic scanning
  - Scanning cost is extremely high under large working sets ("scanning storm")

MGLRU Solution:
  Pages are grouped by generation, with each generation having its own LRU.
    New generation = Active (recently accessed)
    Old generation = Cold (reclaim candidates)
    
  Reclaim starts from the oldest generation → naturally excludes hot pages
  No need to scan all pages to determine hotness/coldness → scanning volume is significantly reduced
  
  Implementation: mm/vmscan.c (MGLRU build) + /sys/kernel/mm/lru_gen
  
  Both Google and Meta have reported significant CPU savings
  (Scanning overhead reduced by 50%~90%)

kswapd and Direct Reclaim

kswapd: Background Reclaim

// mm/vmscan.c
// Each NUMA node has its own kswapd kernel thread

// Wake-up conditions:
//   Free pages < high watermark → kswapd wakes up
//   kswapd continues reclaiming until free pages > high watermark

static int kswapd(void *p) {
    while (!kthread_should_stop()) {
        // 1. Check watermark: if > high → sleep
        // 2. Call balance_pgdat()
        //    ├─ Scan LRU lists
        //    ├─ shrink_node() → shrink_lruvec()
        //    │   ├─ shrink_inactive_list()  → reclaim cold pages
        //    │   └─ shrink_active_list()    → demote hot pages
        //    └─ May trigger compaction (if fragmentation is severe)

        // 3. If enough pages were reclaimed → sleep
        //    If not enough reclaimed → watermark remains low → increase scanning intensity
    }
}

Direct Reclaim: Synchronous Reclaim

// When kswapd cannot reclaim fast enough (memory allocation is too fast),
// the allocator reclaims itself:
__alloc_pages_slowpath()__alloc_pages_direct_reclaim()__perform_reclaim()try_to_free_pages()do_try_to_free_pages()shrink_zones()shrink_node()shrink_lruvec()
          // Same reclaim logic as kswapd, but executed synchronously
          // The allocator blocks here until pages become available

Direct reclaim causes sudden spikes in allocation latency—which is why everyone pays attention to "memory pressure" metrics.


Swap

Swap Partition/File

# Configuration
swapon /dev/sda2           # Swap partition
swapon /swapfile            # Swap file (Btrfs swap supported in 5.5+)

# Priority (multiple swap devices)
swapon -p 10 /dev/nvme0n1p2  # NVMe swap (fast)
swapon -p 1  /dev/sda2        # SATA swap (slow)

Swap Out (Anonymous Pages → Swap)

// mm/vmscan.c
// When reclaiming anonymous pages in shrink_page_list():

if (PageAnon(page) && !PageSwapCache(page)) {
    // 1. Allocate swap slot
    add_to_swap(page)get_swap_page()       // Find free slot in swap bitmap
add_to_swap_cache()   // Add page to swap cache (address_space)
    
    // 2. Write to swap
    //   If CONFIG_THP_SWAP=y:
    //     THP is split into 4KB pages and swapped individually (avoiding 2MB contiguous IO)
    //
    // 3. Mark PTE as swap entry
    //    PTE = swp_entry_t (type + offset)
    //    Hardware sees Present=0 → page fault → do_swap_page()
}

// Swap cache:
//   Cache used during the transition of a page between swap and memory
//   If there is a race between swap out and swap in → swap cache provides consistency

Swap In (Page Fault → Swap)

// mm/memory.c
// do_swap_page(): Process accesses an address that has been swapped out

do_swap_page()
  ├─ lookup_swap_cache()     // Check swap cache (might already be in memory)
  │   └─ If present: Direct mapping (no IO needed!)
  │
  └─ swapin_readahead()      // Pre-read from swap device
      └─ read_swap_cache_async()swap_readpage()  // Submit IO
          → Page enters swap cache
          → Map PTE
          → User process continues execution

Reverse Mapping (rmap)

// mm/rmap.c
// Reverse mapping answers: "Given a physical page, which processes' virtual addresses map to it?"

// Anonymous pages:
//   Traverse all mappings via anon_vma + VMA interval tree
//   page->mapping → anon_vma → anon_vma_chain → VMA

// File pages:
//   Look up via address_space + page->index
//   page->mapping → address_space → i_mmap (interval tree)

// rmap_walk(page, func):
//   Traverse all PTE mappings for the page
//   Call func for each mapping → e.g., try_to_unmap
//     → Mark PTE as swap entry or clear it
//     → Flush TLB

// try_to_unmap() is a prerequisite for swap:
//   All PTE references must be found and unmapped before the page can be reclaimed

Compaction

// mm/compaction.c
// Problem: The buddy allocator requires contiguous pages (e.g., order 4 = 16 pages = 64KB)
//           But memory is fragmented → cannot allocate contiguous blocks

// Compaction: "Move" used pages to free up contiguous free areas
//   1. Scan: Find free pages (target) and used pages (source)
//   2. Migrate: Copy used page content to free page
//   3. Update page tables: Modify all PTEs to point to the new location
//   4. Repeat: Until enough contiguous space is freed

// Trigger methods:
//   1. kcompactd: Background thread (similar to kswapd)
//   2. Direct compaction: Allocator actively compacts in the slowpath

// Check compaction status:
cat /proc/vmstat | grep compact

Debugging and Observation

# Memory pressure overview
cat /proc/meminfo
# SwapTotal/SwapFree: Swap usage
# Active/Inactive(anon/file): Pages in LRU
# Dirty/Writeback: Pages pending writeback

# Reclaim statistics
cat /proc/vmstat | grep -E 'pgsteal|pgscan|pswpin|pswpout'
# pgsteal_*: How many pages were reclaimed
# pgscan_*:  How many pages were scanned
#   scan >> steal → Low LRU scanning efficiency (improved by MGLRU)

# Check if MGLRU is enabled
ls /sys/kernel/mm/lru_gen 2>/dev/null && echo "MGLRU enabled"

# kswapd behavior
ps aux | grep kswapd
cat /proc/<kswapd_pid>/status

# Compaction
cat /proc/vmstat | grep compact
cat /sys/kernel/debug/extfrag/extfrag_index

# Trigger manual reclaim (for testing, use with caution in production)
echo 3 > /proc/sys/vm/drop_caches  # Clear page cache

References and Further Reading

  • Kernel Documentation: Documentation/admin-guide/sysctl/vm.rst, Documentation/mm/multigen_lru.rst
  • LWN:
    • "Multi-generational LRU" (lwn.net/Articles/856932/)
    • "The case for MGLRU" (lwn.net/Articles/916777/)
    • "Swap and the page cache" (lwn.net/Articles/358953/)
  • Source Files:
    • mm/vmscan.c — kswapd + LRU scanning + direct reclaim
    • mm/swap_state.c — Swap cache
    • mm/swapfile.c — Swap partition/file management
    • mm/rmap.c — Reverse mapping
    • mm/compaction.c — Memory compaction
    • mm/page_io.c — Swap IO

Keywords: LRU, MGLRU, kswapd, direct reclaim, swap, swappiness, rmap, try_to_unmap, compaction, page cache reclaim