---
title: Physical Memory Management
url: https://doc.liz6.com/en/linux-kernel/02-memory-management/03-physical-memory-management
locale: en
area: linux-kernel
tags:
- linux-kernel
- memory-management
date: 2026-06-30
modified: 2026-07-16
description: 'Coverage: NUMA → Zone (DMA/DMA32/Normal/Movable) → Buddy Allocator → page allocator API → page struct → memory hotplug Kernel version: 2.6 ~ 6.x'
---

# Physical Memory Management

> Coverage: NUMA → Zone (DMA/DMA32/Normal/Movable) → Buddy Allocator → page allocator API → page struct → memory hotplug
> Kernel version: 2.6 ~ 6.x

## Overview

Virtual memory management answers "which virtual address maps to which physical page," while physical memory management answers a more fundamental question: **"Who does this 4KB physical page belong to, is it free or allocated, and from which NUMA node should it be taken?"**

The core of physical memory management is the **buddy allocator**, which organizes free pages into contiguous blocks of different orders, efficiently allocating and reclaiming them through splitting and merging. Above the buddy allocator lies slab/slub (small object allocators), and below it lies the NUMA-aware zone and node topology.

---

## NUMA Architecture

```
UMA (Uniform Memory Access):
  Consistent latency for all CPUs accessing all memory
  Typical: Single socket, desktop

NUMA (Non-Uniform Memory Access):
  Memory is divided into multiple nodes
  Each CPU has its own local memory (fast access)
  Accessing remote memory is slower (30%~40% slower)
  Typical: Multi-socket servers

View: numactl --hardware
      ls /sys/devices/system/node/
```

### pg_data_t: NUMA Node Descriptor

```c
// include/linux/mmzone.h
typedef struct pglist_data {
    struct zone     node_zones[MAX_NR_ZONES];
    struct zonelist node_zonelists[MAX_ZONELISTS];  // Allocation fallback order
    int             nr_zones;
    unsigned long   node_start_pfn;   // Starting physical page number
    unsigned long   node_present_pages;
    unsigned long   node_spanned_pages;
    int             node_id;
    struct page     *node_mem_map;    // Array of all page structs in this node

    // Reclaim
    struct lruvec   lruvec;          // LRU lists (anonymous/file)
    wait_queue_head_t kcompactd_wait;

    // Statistics
    struct per_cpu_nodestat *per_cpu_nodestats;
} pg_data_t;
```

---

## Zone: Physical Memory Partitioning

### Why Zones Are Needed

```
The fundamental reason for zones: Hardware limitations prevent certain physical memory from being used for specific purposes

16-bit ISA DMA:  Can only access physical addresses 0~16MB
32-bit PCI DMA:  Can only access 0~4GB (unless IOMMU is used)
32-bit kernel:   Can only directly map 896MB (highmem)
64-bit:          No highmem, but zone structure retains DMA/DMA32 limitations
```

### Zone Types

```c
// include/linux/mmzone.h
enum zone_type {
    ZONE_DMA,       // 0-16MB:    ISA DMA
    ZONE_DMA32,     // 0-4GB:     32-bit PCI DMA
    ZONE_NORMAL,    // Direct mapping (kernel can access directly, linear mapping)
    ZONE_HIGHMEM,   // 32-bit only: Dynamic mapping (no such zone in 64-bit)
    ZONE_MOVABLE,   // Movable pages (used for memory defragmentation/hotplug)
    ZONE_DEVICE,    // PMEM / GPU VRAM / CXL
    __MAX_NR_ZONES
};
```

### struct zone

```c
// include/linux/mmzone.h
struct zone {
    unsigned long   _watermark[NR_WMARK];  // low, min, high watermarks
    unsigned long   managed_pages;         // Number of pages managed by buddy
    unsigned long   spanned_pages;
    unsigned long   present_pages;

    // Per-CPU pagesets (cache hot single pages to avoid taking zone lock)
    struct per_cpu_pages  pcp;

    // Buddy free lists
    struct free_area    free_area[MAX_ORDER];  // order 0~10

    // LRU (anonymous/file)
    struct lruvec       lruvec;

    // Lock: protects free_area
    spinlock_t          lock;
};
```

### Watermarks and Memory Pressure

<svg viewBox="0 0 720 350" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Zone Watermark levels and memory reclaim intensity comparison chart">
  <defs><marker id="wm-arrow" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="350" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Watermarks: Three watermarks trigger different intensities of reclaim</text>
  <line x1="90" y1="250" x2="90" y2="56" stroke="#475569" stroke-width="1.8" marker-end="url(#wm-arrow)"/>
  <text x="90" y="46" text-anchor="middle" font-size="11" fill="#475569">pages</text>
  <text x="90" y="266" text-anchor="middle" font-size="11" fill="#64748b">0</text>
  <line x1="90" y1="96" x2="130" y2="96" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 3"/>
  <text x="80" y="100" text-anchor="end" font-size="12" font-weight="700" fill="#15803d">high</text>
  <rect x="140" y="76" width="500" height="40" rx="8" fill="#dcfce7" stroke="#4ade80"/>
  <text x="156" y="93" font-size="12" font-weight="700" fill="#166534">high watermark</text>
  <text x="156" y="109" font-size="11" fill="#15803d">kswapd wakes up, starts background reclaim</text>
  <line x1="90" y1="166" x2="130" y2="166" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 3"/>
  <text x="80" y="170" text-anchor="end" font-size="12" font-weight="700" fill="#c2410c">low</text>
  <rect x="140" y="146" width="500" height="40" rx="8" fill="#ffedd5" stroke="#f97316"/>
  <text x="156" y="163" font-size="12" font-weight="700" fill="#9a3412">low watermark</text>
  <text x="156" y="179" font-size="11" fill="#c2410c">Kernel direct reclaim (synchronous, blocking allocation)</text>
  <line x1="90" y1="236" x2="130" y2="236" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 3"/>
  <text x="80" y="240" text-anchor="end" font-size="12" font-weight="700" fill="#dc2626">min</text>
  <rect x="140" y="216" width="500" height="40" rx="8" fill="#ffffff" stroke="#ef4444" stroke-dasharray="5 4"/>
  <text x="156" y="233" font-size="12" font-weight="700" fill="#dc2626">min watermark</text>
  <text x="156" y="249" font-size="11" fill="#dc2626">Only emergency allocations are allowed to breach this line</text>
  <rect x="60" y="288" width="600" height="42" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="307" font-size="12.5" fill="#3730a3">Watermarks warn progressively from high to low: falling below high → kswapd background async reclaim; falling below low → kernel synchronous direct reclaim (blocking allocation);</text>
  <text x="76" y="323" font-size="12.5" fill="#3730a3">Falling below min → only emergency allocations can breach, approaching the memory exhaustion boundary.</text>
</svg>

```c
// Watermark check during allocation:
// zone_watermark_ok(zone, order, mark, classzone_idx, alloc_flags)
//   Calculates whether the zone's free pages satisfy the order allocation and do not fall below mark
//   If insufficient → triggers reclaim (kswapd or direct reclaim)
```

---

## Buddy Allocator

### Data Structures

```c
// mm/page_alloc.c
// Each zone has a free_area for each order
// free_area[order]: Linked list of free blocks of size 2^order pages

#define MAX_ORDER 11  // Typically: max 2^10 = 4MB contiguous (1024 × 4KB)

struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];  // Categorized by migration type
    unsigned long       nr_free;                   // Number of free blocks for this order
};

// Migration types (anti-fragmentation):
enum migratetype {
    MIGRATE_UNMOVABLE,  // Kernel data structures (cannot be moved)
    MIGRATE_MOVABLE,    // User pages (migratable → anti-fragmentation)
    MIGRATE_RECLAIMABLE,// Reclaimable (e.g., inode cache)
    MIGRATE_PCPTYPES,   // Per-CPU reserved
    MIGRATE_HIGHATOMIC, // High atomic allocation (emergency)
    MIGRATE_CMA,        // Contiguous Memory Allocator
    MIGRATE_ISOLATE,    // Isolated (memory hotplug / compaction)
};
```

### Core Algorithm

```c
// Allocation:
// alloc_pages(gfp_mask, order) → __alloc_pages()
//   → get_page_from_freelist()  // Try to allocate from a suitable zone
//     → rmqueue()                // Take from buddy free_area
//       → __rmqueue_smallest()   // Find the smallest free block >= order
//         → expand()             // If the found block is larger than needed → split
//              The lower half is put back into buddy free_area

// Example: Request order=1 (2 pages = 8KB)
//   free_area[2] has 16KB free → expand():
//     → Allocate the first 8KB (order 0~1) to the caller
//     → Put the remaining 8KB into free_area[1]

// Freeing:
// free_pages(addr, order) → __free_pages()
//   → __free_one_page()
//     → Check if the buddy (partner) is also free
//     → If buddy is free → merge into an order+1 block → recursive merge
//     → If buddy is not free → put into the corresponding order's free_area

// Buddy check:
//   buddy_pfn = page_pfn ^ (1 << order)  // XOR: PFN of the buddy
//   If buddy is also free and of the same order → merge
```

### GFP Flags

```c
// include/linux/gfp_types.h
// Control of allocation behavior:

// Zone modifiers:
#define __GFP_DMA       // Allocate from ZONE_DMA
#define __GFP_DMA32     // Allocate from ZONE_DMA32
#define __GFP_HIGHMEM   // Allow allocation from HIGHMEM (32-bit only)
#define __GFP_MOVABLE   // Allocate movable type

// Watermark modifiers:
#define __GFP_ATOMIC    // High priority, can use emergency reserves
#define __GFP_HIGH      // Allow use of emergency reserved memory
#define __GFP_MEMALLOC  // Allow ALLOC_NO_WATERMARKS (extremely urgent)

// Reclaim modifiers:
#define __GFP_IO        // Allow IO (write back swap)
#define __GFP_FS        // Allow filesystem operations (clear inode cache)
#define __GFP_DIRECT_RECLAIM  // Allow synchronous reclaim
#define __GFP_RECLAIM   // New name for __GFP_DIRECT_RECLAIM
#define __GFP_NORETRY   // Do not retry

// Common combinations:
#define GFP_KERNEL      (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
                        // Most common: allows sleeping/reclaim/IO
#define GFP_ATOMIC      (__GFP_HIGH)        
                        // Cannot sleep: used in interrupt/atomic context
#define GFP_USER        (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
                        // User space allocation
#define GFP_NOWAIT      (0)                 
                        // Do not wait: return NULL immediately if allocation fails
```

### Per-CPU Pages (PCP)

```c
// mm/page_alloc.c
// Each CPU has a per-CPU 1-page cache (struct per_cpu_pages)
// 
// Why? 
//   Allocating a single page (order 0) is the most common operation
//   If every time you have to take zone->lock (global lock) → severe lock contention
//   PCP acts as L1 cache: locally prefetch a batch of pages
//     → alloc: PCP has it → take directly (lock-free)
//              PCP empty → take a batch from buddy (take zone lock once)
//     → free:  PCP not full → put back into PCP (lock-free)
//              PCP full → put back into buddy (take zone lock once)
//
// Design principle: Batch operations amortize lock overhead
```

---

## struct page

```c
// include/linux/mm_types.h
// Each physical page has a struct page (4KB page manages 64 bytes → ~1.5% overhead)

struct page {
    unsigned long flags;       // Page flags (PG_locked, PG_dirty, PG_uptodate, ...)

    union {
        // In buddy allocator: linked list node
        struct list_head lru;

        // In slab: slab information
        struct slab *slab_cache;

        // As compound page head: number of tail pages
        unsigned long compound_head;
    };

    union {
        atomic_t _mapcount;    // Number of PTEs mapping this page
        atomic_t _refcount;    // Reference count (get_page/put_page)
    };

    // Pointer to address_space (if this page is part of page cache)
    struct address_space *mapping;
    pgoff_t index;             // Offset in the mapping

    // Private data (interpreted based on usage):
    //   Anonymous page: anon_vma
    //   Slab:   slab information
    //   Buffer: buffer_head
    void *private;
};
```

`struct page` is an overloaded structure—the same field has different interpretations depending on the page's usage (buddy free, slab, anonymous memory, page cache, THP compound). The kernel uses bits in `flags` to distinguish them.

---

## Memory Hotplug

```c
// mm/memory_hotplug.c
// Physically adding/removing memory sticks:
//   online: add_memory() → __add_memory() → initialize page struct → add to buddy
//   offline: remove_memory() → __remove_memory() → migrate used pages → remove from buddy

// DIMM (NVDIMM/CXL):
//   Can be made into ZONE_DEVICE (not managed by buddy)
//   Or made into normal memory (ZONE_NORMAL), allocated by buddy
```

---

## Debugging

```bash
# Buddy allocator status
cat /proc/buddyinfo
# Node 0, zone Normal: 11 4 2 1 0 0 ...
#   order 0: 11 blocks (44KB free as 4KB pages)
#   order 1: 4 blocks  (32KB free as 8KB chunks)

# Zone information
cat /proc/zoneinfo
# Contains: per-zone watermarks, free pages, protection

# NUMA statistics
numastat  # per-node allocation/miss statistics

# Memory fragmentation index
cat /sys/kernel/debug/extfrag/unusable_index
# 1.000 = Fully fragmented, unable to allocate contiguous pages
# 0.000 = No fragmentation

# Triggered reclaim statistics
cat /proc/vmstat | grep -E 'pgsteal|pgscan|compact'
```

---

## References and Further Reading

- **Kernel Documentation**: `Documentation/mm/page_alloc.rst`, `Documentation/admin-guide/sysctl/vm.rst`
- **LWN**: 
  - "The design of the buddy allocator" (lwn.net/Articles/259832/)
  - "Memory management APIs" series
- **Source Files**:
  - `mm/page_alloc.c` — buddy allocator (~7000 lines)
  - `include/linux/mmzone.h` — zone, pg_data_t, free_area
  - `include/linux/mm_types.h` — struct page
  - `include/linux/gfp_types.h` — GFP flags

---

*Keywords: NUMA, Zone, Buddy Allocator, GFP_KERNEL, GFP_ATOMIC, struct page, watermark, kswapd, memory hotplug, compaction*
