---
title: Page Reclaim and Swap
url: https://doc.liz6.com/en/linux-kernel/02-memory-management/05-page-reclaim-and-swap
locale: en
area: linux-kernel
tags:
- linux-kernel
- memory-management
date: 2026-06-30
modified: 2026-07-16
description: '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+)'
---

# 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)

```c
// 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

```c
// 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

```c
// 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

```c
// 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

```bash
# 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)

```c
// 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)

```c
// 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)

```c
// 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

```c
// 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

```bash
# 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*
