7. 进阶 - 物理内存管理

一、物理内存管理架构

物理内存通过Node -> Zone -> Page三级结构管理

1. Node (pglist_data)

关键字段:

c 复制代码
// include/linux/mmzone.h
typedef struct pglist_data {
    int node_id;                        // NUMA节点ID
    struct page *node_mem_map;          // 该节点所有page结构数组
    unsigned long node_start_pfn;       // 起始页帧号
    unsigned long node_present_pages;   // 实际可用物理页数
    unsigned long node_spanned_pages;   // 包含内存空洞的总页数
    
    // 内存区域管理
    struct zone node_zones[MAX_NR_ZONES]; 
    struct zonelist node_zonelists[MAX_ZONELISTS];
    
    // 内存回收机制
    wait_queue_head_t kswapd_wait;
    struct task_struct *kswapd;         // 内存回收守护进程
    int kswapd_order;                   // kswapd回收的页块大小
    enum zone_type kswapd_highest_zoneidx; // 最高回收区域索引
    
    // 统计信息
    unsigned long       totalreserve_pages;
    atomic_long_t       vm_stat[NR_VM_NODE_STAT_ITEMS];
} pg_data_t;

2. struct zone (内存域)

解决硬件限制,不是所有物理内存对所有内核组件都"平等可用"。关键成员

c 复制代码
struct zone {
    /* 基本属性 */
    const char      *name;             // 区域名称 ("DMA", "Normal"等)
    unsigned long   zone_start_pfn;    // 区域起始页帧号
    atomic_long_t   managed_pages;     // 被伙伴系统管理的页面数
    
    /* 水线管理 */
    unsigned long watermark_boost;     // 水线提升值
    unsigned long watermark[NR_WMARK]; // MIN, LOW, HIGH水线值
    
    /* 空闲内存管理 */
    struct free_area    free_area[MAX_ORDER]; // 伙伴系统核心结构
    unsigned long       percpu_drift_mark;   // per-CPU缓存水位
    
    /* 内存保留 */
    long lowmem_reserve[MAX_NR_ZONES]; // 为高优先级区域保留的内存
    
    /* 统计信息 */
    atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];
    atomic_long_t       vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
    
    /* 回收相关 */
    unsigned long       compact_cached_free_pfn;
    unsigned long       compact_cached_migrate_pfn[ASYNC_AND_SYNC];
};

主要域 (ZONE):

c 复制代码
enum zone_type {
#ifdef CONFIG_ZONE_DMA
    ZONE_DMA,     // 0-16MB。传统ISA设备DMA只能访问此范围。
#endif
#ifdef CONFIG_ZONE_DMA32
    ZONE_DMA32,    // (64位系统): 0-4GB。用于32位PCI设备DMA。
#endif
    ZONE_NORMAL,    // 直接映射到内核虚拟地址空间的部分。内核大部分动态分配和内核栈在此。驱动开发最常打交道的区域!
#ifdef CONFIG_HIGHMEM
    ZONE_HIGHMEM,    // (32位系统): 物理内存超过内核直接映射范围的部分。需动态映射(kmap/kunmap)才能访问。
#endif
    ZONE_MOVABLE,    // 可移动页的集合,用于反碎片和内存热插拔。
#ifdef CONFIG_ZONE_DEVICE
    ZONE_DEVICE,
#endif
    __MAX_NR_ZONES
};

3. struct page(页描述符)

内核为每个物理页帧维护一个struct page实例(存储在mem_map数组中),包含页的状态(空闲、使用中、脏、写回等)、引用计数、所属的zone、指向伙伴系统的链表指针、映射信息(谁在使用此页)等。 关键成员:

c 复制代码
// include/linux/mm_types.h

struct page {
    unsigned long flags;            // 页状态标志
    atomic_t _refcount;             // 引用计数
    
    union {
        struct {    // 页缓存和匿名页使用
            union {
                struct list_head lru;    // LRU链表
                struct dev_pagemap *pgmap; // 设备内存映射
            };
            struct address_space *mapping; // 文件映射
            pgoff_t index;               // 文件内偏移
        };
        struct {    // SLAB分配器使用
            struct kmem_cache *slab_cache; 
            void *freelist;     
        };
    };
    
    union {
        unsigned long private;      // 文件系统私有数据
        spinlock_t *ptl;            // 页表锁
    };
    
    void *virtual;                  // 内核虚拟地址
    struct mem_cgroup *memcg_data;  // CGroup内存控制
};

页标志位

c 复制代码
// page-flags.h
enum pageflags {
    PG_locked,        // 页面被锁定
    PG_error,         // I/O错误发生
    PG_referenced,    // 页面被访问
    PG_uptodate,      // 页面数据有效
    PG_dirty,         // 页面被修改
    PG_lru,           // 在LRU链表中
    PG_active,        // 在活跃LRU链表中
    PG_slab,          // SLAB分配器页面
    PG_writeback,     // 页面正在回写
    // ...超过30种标志
};

二、物理内存分配流程

内核中常用的分配物理内存页面的接口函数是 alloc_pages(),用于分配一个或多个连续的物理页面,分配的页面个数只能是2的整数次幂。诸如 vmalloc、 get_user_pages、以及缺页中断中分配页面,都是通过该接口分配的物理页面。

c 复制代码
// include/linux/gfp.h
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
核心参数:
    gfp_mask:分配标志(Get Free Pages mask)
    order:分配页数的对数(实际分配页数 = 2^order)

返回值:
    成功:指向第一个page结构的指针    
    失败:NULL

1. GFP掩码详解

c 复制代码
// include/linux/gfp.h
// 1. 内存区域修饰符
__GFP_DMA (0x01u): 分配的内存页必须位于低 16MB 的内存中,通常用于旧的 ISA 设备。
__GFP_HIGHMEM (0x02u): 允许分配高内存(highmem)。高内存是指物理内存中超出内核直接映射范围的部分。
__GFP_DMA32 (0x04u): 分配的内存页必须位于低 4GB 的内存中,适用于一些只能访问 32 位地址空间的设备。
__GFP_MOVABLE (0x08u): 分配的内存页可以被移动(用于内存压缩等操作)。

// 2. 行为修饰符
__GFP_RECLAIMABLE (0x10u): 分配的内存页在需要时可以被回收。
__GFP_HIGH (0x20u): 允许分配高优先级的内存。
__GFP_IO (0x40u): 允许在分配过程中进行 I/O 操作。
__GFP_FS (0x80u): 允许在分配过程中调用文件系统操作。
__GFP_ZERO (0x100u): 分配后将内存页清零。
__GFP_ATOMIC (0x200u): 原子上下文分配,不允许睡眠。
__GFP_DIRECT_RECLAIM (0x400u): 允许直接回收内存。
__GFP_KSWAPD_RECLAIM (0x800u): 允许 kswapd(内核交换守护进程)回收内存。
__GFP_WRITE (0x1000u): 分配的内存页将用于写操作。
__GFP_NOWARN (0x2000u): 如果分配失败,不发出警告。
__GFP_RETRY_MAYFAIL (0x4000u): 如果分配失败,允许重试。
__GFP_NOFAIL (0x8000u): 不允许分配失败(可能会导致系统崩溃)。
__GFP_NORETRY (0x10000u): 如果分配失败,不重试。
__GFP_MEMALLOC (0x20000u): 允许分配内存用于内存分配(如 OOM 情况)。
__GFP_COMP (0x40000u): 分配的内存页将用于压缩。
__GFP_NOMEMALLOC (0x80000u): 不允许分配内存用于内存分配(与 __GFP_MEMALLOC 相反)。
__GFP_HARDWALL (0x100000u): 严格限制内存分配在当前节点。
__GFP_THISNODE (0x200000u): 仅在当前节点分配内存。
__GFP_ACCOUNT (0x400000u): 将分配的内存页计入内存使用统计。
__GFP_ZEROTAGS (0x800000u): 分配的内存页将清零所有标签。
__GFP_SKIP_KASAN_POISON (0x1000000u): 跳过 KASAN(Kernel Address Sanitizer)的内存毒化操作。
__GFP_CMA (0x2000000u): 分配的内存页将用于 CMA(Contiguous Memory Allocator)。
__GFP_NOLOCKDEP (0x4000000u): 禁用锁依赖检查。

内核常用预定义组合

c 复制代码
#define GFP_ATOMIC (__GFP_HIGH | __GFP_ATOMIC | __GFP_KSWAPD_RECLAIM) // 用于原子上下文,不允许睡眠,允许 kswapd 回收内存
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS) // 用于普通内核上下文,允许直接回收内存、I/O 操作和文件系统调用
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT) // 与 GFP_KERNEL 类似,但分配的内存页计入内存使用统计
#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM) // 与 GFP_KERNEL 类似,但不允许直接回收内存,仅允许 kswapd 回收
#define GFP_NOIO (__GFP_RECLAIM) // 与 GFP_KERNEL 类似,但不允许在分配过程中进行 I/O 操作
#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO) // 与 GFP_KERNEL 类似,但不允许调用文件系统操作
#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL) // 用于用户空间内存分配,严格限制在当前节点
#define GFP_DMA __GFP_DMA // 分配位于低 16MB 内存中的页,适用于旧的 ISA 设备
#define GFP_DMA32 __GFP_DMA32 // 分配位于低 4GB 内存中的页,适用于只能访问 32 位地址空间的设备
#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM) // 与 GFP_USER 类似,但允许分配高内存
#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE | __GFP_SKIP_KASAN_POISON) // 与 GFP_HIGHUSER 类似,但分配的内存页可以被移动,并跳过 KASAN 的内存毒化操作
#define GFP_TRANSHUGE_LIGHT ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | __GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM) // 用于透明大页分配,轻量级版本,不允许直接回收内存
#define GFP_TRANSHUGE (GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM) // 用于透明大页分配,完整版本,允许直接回收内存

/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE | __GFP_MOVABLE) // 用于提取与内存迁移相关的标志

2. 物理内存分配流程(以alloc_pages为例)

2.1 代码调用流程

c 复制代码
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
    static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
        static inline struct page *__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
            static inline struct page *__alloc_pages(gfp_t gfp_mask, unsigned int order, int preferred_nid)
                struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid, nodemask_t *nodemask)
                    static struct page *get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags, const struct alloc_context *ac)
                    static inline struct page *__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,struct alloc_context *ac)
                        static inline struct page *rmqueue(struct zone *preferred_zone, struct zone *zone, unsigned int order, gfp_t gfp_flags, unsigned int alloc_flags, int migratetype)

2.2 快速路径分配 (get_page_from_freelist)

c 复制代码
// mm/page_alloc.c
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
                        const struct alloc_context *ac)
{
    for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, ac->nodemask) {
        // 1. 检查水线
        mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
        if (!zone_watermark_fast(zone, order, mark, 
                   ac_classzone_idx(ac), alloc_flags))
            continue;
        
        // 2. 尝试分配
        page = rmqueue(ac->preferred_zoneref->zone, zone, order, 
                      gfp_mask, alloc_flags, ac->migratetype);
        if (page) {
            // 3. 页面初始化
            prep_new_page(page, order, gfp_mask, alloc_flags);
            return page;
        }
    }
    return NULL;
}

2.3 慢速路径分配 (__alloc_pages_slowpath)

c 复制代码
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
                        struct alloc_context *ac)
{
retry:
    // 1. 唤醒kswapd
    if (alloc_flags & ALLOC_KSWAPD)
        wake_all_kswapds(order, gfp_mask, ac);
    
    // 2. 直接内存回收
    page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac, &did_reclaim);
    if (page)
        goto got_pg;
    
    // 3. 内存压缩
    page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                    compact_priority, &compact_result);
    if (page)
        goto got_pg;
    
    // 4. OOM终结进程
    page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
    if (page)
        goto got_pg;
    
    // 5. 最终重试
    page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    if (page)
        goto got_pg;
    
fail:
    return NULL;
}

2.4 伙伴系统核心操作 (rmqueue)

c 复制代码
static struct page *rmqueue(struct zone *preferred_zone,
            struct zone *zone, unsigned int order,
            gfp_t gfp_flags, unsigned int alloc_flags,
            int migratetype)
{
    // 尝试从指定迁移类型分配
    page = __rmqueue(zone, order, migratetype, alloc_flags);
    if (!page) {
        // 尝试回退迁移类型
        page = __rmqueue_fallback(zone, order, migratetype);
    }
    return page;
}

3. 物理内存分配流程图

三、开发技巧

1. 上下文与GFP标志选择

中断处理函数中的安全分配

c 复制代码
irqreturn_t interrupt_handler(int irq, void *dev_id)
{
    struct page *page = alloc_pages(GFP_ATOMIC, 0);
    if (!page) {
        // 使用预分配缓冲
        page = this_cpu_read(emergency_page);
    }
    ...
}

从连续内存区域分配

c 复制代码
struct page *cma_alloc(struct cma *cma, size_t count, unsigned int align)
{
    // 1. 尝试直接分配
    page = alloc_contig_range(pfn, pfn + count, MIGRATE_CMA);
    
    // 2. 失败时迁移页面
    if (!page) {
        ret = migrate_pages(&cma->migrate_pages, alloc_contig_range, 
                           NULL, 0, MIGRATE_SYNC);
        page = alloc_contig_range(pfn, pfn + count, MIGRATE_CMA);
    }
    return page;
}

2. 大页分配

分配2MB大页 (order=9)

c 复制代码
#define HPAGE_ORDER 9
struct page *hpage = alloc_pages(GFP_TRANSHUGE, HPAGE_ORDER); //GFP_TRANSHUGE:用于透明大页分配,完整版本,允许直接回收内存
if (!hpage) {
    // 回退方案:尝试分配多个小页
    hpage = alloc_pages(GFP_KERNEL, 0);
    // ... 组合多个小页
}

3. 错误处理

c 复制代码
struct page *alloc_buffer_pages(int order)
{
    struct page *page;
    int retries = 0;
    
retry:
    page = alloc_pages(GFP_KERNEL, order);
    if (unlikely(!page)) {
        if (order > 0) {
            // 尝试降低分配阶
            order--;
            goto retry;
        }
        
        // 尝试紧急保留内存
        page = alloc_pages(GFP_HIGHUSER | __GFP_NOFAIL, order);
    }
    
    if (!page && retries++ < 3) {
        // 主动回收内存后重试
        sync_filesystems(0);
        try_to_free_pages(&zone, order, GFP_KERNEL);
        goto retry;
    }
    
    return page;
}

四、调试技巧

1. /proc接口分析

查看伙伴系统状态

c 复制代码
cat /proc/buddyinfo 
Node 0, zone      DMA      1    1    1    0    2    1    1    0    1    1    3 
Node 0, zone    DMA32    314  273  452  240  187  124   77   55   35   20   16 

解释:每列表示不同order(0-10)的空闲块数量
order0: 1个4KB块, order1: 1个8KB块, ... order10: 3个4MB块

查看区域统计

c 复制代码
cat /proc/zoneinfo
cat /proc/zoneinfo | grep -A10 "Node 0, zone      DMA"
Node 0, zone      DMA
  per-node stats
      nr_inactive_anon 76226
      nr_active_anon 244
      nr_inactive_file 67980
      nr_active_file 18533
      nr_unevictable 8171
      nr_slab_reclaimable 15033
      nr_slab_unreclaimable 14169
      nr_isolated_anon 0
      nr_isolated_file 0

2. 动态追踪

跟踪alloc_pages调用

c 复制代码
echo 'p:alloc alloc_pages order=%dx gfp_flags=+0(%di):x32' > /sys/kernel/debug/tracing/kprobe_events

触发跟踪

c 复制代码
echo 1 > /sys/kernel/debug/tracing/events/kprobes/alloc/enable

查看结果

c 复制代码
cat /sys/kernel/debug/tracing/trace
相关推荐
kfepiza39 分钟前
`/etc/samba/smb.conf`笔记250721
linux·网络协议
beyoundout1 小时前
LVS(Linux virtual server)-实现四层负载均衡
linux·服务器·lvs
Spike()1 小时前
LVS工作模式和算法的总结
linux·服务器·lvs
禁默1 小时前
《命令行参数与环境变量:从使用到原理的全方位解析》
linux
老马啸西风2 小时前
windows wsl ubuntu 如何安装 maven
linux·运维·windows·ubuntu·docker·k8s·maven
东木君_2 小时前
【Linux驱动-快速回顾】简单了解一下PinCtrl子系统:设备树如何被接解析与匹配
linux·运维·服务器
CZIDC2 小时前
博客摘录「 华为云平台-FusionSphere OpenStack 8.2.1 系统加固」2025年7月15日
linux·服务器·笔记·华为云·openstack
Linux运维技术栈3 小时前
企业运维实战:Jenkins 依赖 JDK21 与应用需 JDK1.8 共存方案(含流水线配置)
java·linux·服务器
niewzh3 小时前
最新版vscode 连接ubuntu 18.04 保姆级教程
linux·运维·ubuntu
cpsvps3 小时前
美国VPS服务器Linux内核参数调优的实践与验证
linux·运维·服务器