本页目录

物理内存管理

覆盖: NUMA → Zone (DMA/DMA32/Normal/Movable) → Buddy Allocator → page allocator API → page struct → memory hotplug 内核版本: 2.6 ~ 6.x

概述

虚拟内存管理回答了"哪个虚拟地址映射到哪个物理页",而物理内存管理回答的是更底层的问题:"这 4KB 物理页属于谁,空闲还是已分配,应该从哪个 NUMA 节点拿?"

物理内存管理的核心是 buddy allocator(伙伴分配器),它将空闲页面组织为不同阶(order)的连续块,通过分裂和合并高效分配和回收。buddy 之上是 slab/slub(小对象分配器),之下是 NUMA 感知的 zone 和 node 拓扑。


NUMA 架构

UMA (Uniform Memory Access):
  所有 CPU 访问所有内存的延迟一致
  典型: 单 socket, 台式机

NUMA (Non-Uniform Memory Access):
  内存被分成多个 node
  每个 CPU 有自己的 local memory (访问快)
  访问 remote memory (慢 30%~40%)
  典型: 多 socket 服务器

查看: numactl --hardware
      ls /sys/devices/system/node/

pg_data_t: NUMA 节点描述符

// include/linux/mmzone.h
typedef struct pglist_data {
    struct zone     node_zones[MAX_NR_ZONES];
    struct zonelist node_zonelists[MAX_ZONELISTS];  // 分配 fallback 顺序
    int             nr_zones;
    unsigned long   node_start_pfn;   // 起始物理页号
    unsigned long   node_present_pages;
    unsigned long   node_spanned_pages;
    int             node_id;
    struct page     *node_mem_map;    // 本 node 所有 page struct 的数组

    // 回收
    struct lruvec   lruvec;          // LRU 链表 (匿名/文件)
    wait_queue_head_t kcompactd_wait;

    // 统计
    struct per_cpu_nodestat *per_cpu_nodestats;
} pg_data_t;

Zone: 物理内存分区

为什么需要 Zone

Zone 存在的根本原因: 硬件限制导致某些物理内存不能用于特定目的

16-bit ISA DMA:  只能访问物理地址 0~16MB
32-bit PCI DMA:  只能访问 0~4GB (除非 IOMMU)
32-bit 内核:     只能直接映射 896MB (highmem)
64-bit:         没有 highmem,但 zone 结构保留了 DMA/DMA32 限制

Zone 类型

// include/linux/mmzone.h
enum zone_type {
    ZONE_DMA,       // 0-16MB:    ISA DMA
    ZONE_DMA32,     // 0-4GB:     32-bit PCI DMA
    ZONE_NORMAL,    // 直接映射    (内核可直接访问, 线性映射)
    ZONE_HIGHMEM,   // 32-bit only: 动态映射 (64-bit 无此 zone)
    ZONE_MOVABLE,   // 可移动页面  (用于内存碎片整理/热插拔)
    ZONE_DEVICE,    // PMEM / GPU VRAM / CXL
    __MAX_NR_ZONES
};

struct zone

// include/linux/mmzone.h
struct zone {
    unsigned long   _watermark[NR_WMARK];  // low, min, high watermarks
    unsigned long   managed_pages;         // buddy 管理的页面数
    unsigned long   spanned_pages;
    unsigned long   present_pages;

    // Per-CPU pagesets (缓存热的单个页面, 避免拿 zone lock)
    struct per_cpu_pages  pcp;

    // Buddy 空闲链表
    struct free_area    free_area[MAX_ORDER];  // order 0~10

    // LRU (匿名/文件)
    struct lruvec       lruvec;

    // 锁: 保护 free_area
    spinlock_t          lock;
};

Watermarks 与内存压力

Watermarks:三条水位线触发不同强度的回收 pages 0 high high 水位 kswapd 醒来,开始后台回收 low low 水位 内核直接回收(同步,阻塞分配) min min 水位 只有紧急分配才允许突破此线 水位从高到低层层示警:跌破 high → kswapd 后台异步回收;跌破 low → 内核同步直接回收(阻塞分配); 跌破 min → 只有紧急分配能突破,逼近内存耗尽边界。
// 分配时的 watermark 检查:
// zone_watermark_ok(zone, order, mark, classzone_idx, alloc_flags)
//   计算 zone 的空闲页是否满足 order 分配且不低于 mark
//   如果不够 → 触发回收 (kswapd 或 direct reclaim)

Buddy Allocator

数据结构

// mm/page_alloc.c
// 每个 zone 的每个 order 有一个 free_area
// free_area[order]: 大小为 2^order 页的空闲块链表

#define MAX_ORDER 11  // 通常: 最大 2^10 = 4MB 连续 (1024 × 4KB)

struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];  // 按迁移类型分类
    unsigned long       nr_free;                   // 该 order 的空闲块数
};

// 迁移类型 (反碎片):
enum migratetype {
    MIGRATE_UNMOVABLE,  // 内核数据结构 (不能移动)
    MIGRATE_MOVABLE,    // 用户页 (可迁移 → 反碎片)
    MIGRATE_RECLAIMABLE,// 可回收 (如 inode cache)
    MIGRATE_PCPTYPES,   // Per-CPU 保留
    MIGRATE_HIGHATOMIC, // 高原子分配 (紧急)
    MIGRATE_CMA,        // Contiguous Memory Allocator
    MIGRATE_ISOLATE,    // 隔离 (memory hotplug / compaction)
};

核心算法

// 分配:
// alloc_pages(gfp_mask, order) → __alloc_pages()
//   → get_page_from_freelist()  // 尝试从合适的 zone 分配
//     → rmqueue()                // 从 buddy free_area 取
//       → __rmqueue_smallest()   // 找 >= order 的最小空闲块
//         → expand()             // 如果找到的比需要的大 → 分裂
//              下半部分放回 buddy free_area

// 举例: 请求 order=1 (2 页 = 8KB)
//   free_area[2] 有 16KB 空闲 → expand():
//     → 分配 order 0~1 前 8KB 给调用者
//     → 剩余 8KB 放入 free_area[1]

// 释放:
// free_pages(addr, order) → __free_pages()
//   → __free_one_page()
//     → 检查 buddy (伙伴) 是否也空闲
//     → 如果 buddy 空闲 → 合并为 order+1 的块 → 递归合并
//     → 如果 buddy 不空闲 → 放入对应 order 的 free_area

// Buddy 检查:
//   buddy_pfn = page_pfn ^ (1 << order)  // XOR: buddy 的 PFN
//   如果 buddy 也是空闲的且同 order → 合并

GFP Flags

// include/linux/gfp_types.h
// 分配行为的控制:

// Zone modifiers:
#define __GFP_DMA       // 从 ZONE_DMA 分配
#define __GFP_DMA32     // 从 ZONE_DMA32 分配
#define __GFP_HIGHMEM   // 允许从 HIGHMEM 分配 (32-bit only)
#define __GFP_MOVABLE   // 分配可迁移的类型

// Watermark modifiers:
#define __GFP_ATOMIC    // 高优先级,可用 emergency reserves
#define __GFP_HIGH      // 允许用紧急保留内存
#define __GFP_MEMALLOC  // 允许 ALLOC_NO_WATERMARKS (极紧急)

// Reclaim modifiers:
#define __GFP_IO        // 允许 IO (写回 swap)
#define __GFP_FS        // 允许文件系统操作 (清 inode cache)
#define __GFP_DIRECT_RECLAIM  // 允许同步回收
#define __GFP_RECLAIM   // __GFP_DIRECT_RECLAIM 的新名
#define __GFP_NORETRY   // 不重试

// 常用组合:
#define GFP_KERNEL      (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
                        // 最常用: 允许睡眠/回收/IO
#define GFP_ATOMIC      (__GFP_HIGH)        
                        // 不可睡眠: 用于中断/atomic 上下文
#define GFP_USER        (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
                        // 用户空间分配
#define GFP_NOWAIT      (0)                 
                        // 不等待: 分配失败立即返回 NULL

Per-CPU Pages (PCP)

// mm/page_alloc.c
// 每个 CPU 有 per-CPU 的 1-page 缓存 (struct per_cpu_pages)
// 
// 为什么? 
//   分配单个页面 (order 0) 是最常见的操作
//   如果每次都要拿 zone->lock (全局锁) → 锁竞争严重
//   PCP 作为 L1 cache: 本地预取一 batch 的页面
//     → alloc: PCP 有 → 直接拿 (无锁)
//              PCP 空 → 从 buddy 拿一 batch (拿一次 zone lock)
//     → free:  PCP 未满 → 放回 PCP (无锁)
//              PCP 满 → 放回 buddy (拿一次 zone lock)
//
// 设计原理: 批量操作分摊锁开销

struct page

// include/linux/mm_types.h
// 每个物理页都有一个 struct page (4KB page 管理 64 bytes → ~1.5% 开销)

struct page {
    unsigned long flags;       // 页标志 (PG_locked, PG_dirty, PG_uptodate, ...)

    union {
        // 在 buddy allocator 中: 链表节点
        struct list_head lru;

        // 在 slab 中: slab 信息
        struct slab *slab_cache;

        // 作为 compound page 的 head: tail page 的数量
        unsigned long compound_head;
    };

    union {
        atomic_t _mapcount;    // 被多少个 PTE 映射
        atomic_t _refcount;    // 引用计数 (get_page/put_page)
    };

    // 指向 address_space (如果此页是 page cache 的一部分)
    struct address_space *mapping;
    pgoff_t index;             // 在 mapping 中的 offset

    // 私有数据 (根据用途解释):
    //   匿名页: anon_vma
    //   slab:   slab 信息
    //   buffer: buffer_head
    void *private;
};

struct page 是一个超载的结构体——根据页面用途(buddy 空闲、slab、匿名内存、page cache、THP compound),同一个字段有不同的解释。内核使用 flags 中的 bit 来区分。


Memory Hotplug

// mm/memory_hotplug.c
// 物理添加/移除内存条:
//   online: add_memory() → __add_memory() → 初始化 page struct → 加入 buddy
//   offline: remove_memory() → __remove_memory() → 迁移已用页面 → 退出 buddy

// DIMM (NVDIMM/CXL):
//   可以做成 ZONE_DEVICE (不被 buddy 管理)
//   或者做成普通内存 (ZONE_NORMAL),由 buddy 分配

调试

# Buddy allocator 状态
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 信息
cat /proc/zoneinfo
# 含: per-zone watermarks, free pages, protection

# NUMA 统计
numastat  # per-node allocation/miss 统计

# 内存碎片指数
cat /sys/kernel/debug/extfrag/unusable_index
# 1.000 = 完全碎片化,无法分配连续页面
# 0.000 = 无碎片

# 触发的 reclaim 统计
cat /proc/vmstat | grep -E 'pgsteal|pgscan|compact'

参考与延伸

  • 内核文档⁠: 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" 系列
  • 源码文件⁠:
    • mm/page_alloc.c — buddy allocator (~7000行)
    • include/linux/mmzone.h — zone, pg_data_t, free_area
    • include/linux/mm_types.h — struct page
    • include/linux/gfp_types.h — GFP flags

关键词: NUMA, Zone, Buddy Allocator, GFP_KERNEL, GFP_ATOMIC, struct page, watermark, kswapd, memory hotplug, compaction