Linux内存管理深度解析:从Level 5页表到缺页异常的完整旅程

一、引言

Linux内存管理是操作系统最复杂的子系统之一。从 malloc(1) 这个看似简单的调用,到内核实际分配物理页,中间经历了虚拟地址→VMA→页表遍历→缺页异常→伙伴分配→页回收的漫长旅程。

本文将带你深入这一过程:从48位虚拟地址的页表层级,到buddy/slab/slub分配器源码,再到mmap和缺页异常的完整处理链。

二、虚拟地址空间

2.1 48位分三层解读

// 48位虚拟地址解码 (Level 5 Paging):
// bits 48-63: 符号扩展(用户态全0/内核态全1)
// bits 39-47: PGD索引(P4D in 5-level)  — 512 entries × 512GB
// bits 30-38: PUD索引 — 512 entries × 1GB
// bits 21-29: PMD索引 — 512 entries × 2MB
// bits 12-20: PTE索引 — 512 entries × 4KB
// bits  0-11: 页内偏移   — 4096 bytes

#define PGDIR_SHIFT   39
#define PUD_SHIFT     30
#define PMD_SHIFT     21
#define PAGE_SHIFT    12

// 页表项(PTE)结构 (x86_64):
// bits  0:    Present
// bit   1:    Read/Write
// bit   2:    User/Supervisor
// bit   5:    Accessed
// bit   6:    Dirty
// bits 12-51: Physical Page Frame Number (PFN)
// bit   63:   No Execute (NX)

typedef struct {
    unsigned long pte;
} pte_t;

static inline unsigned long pte_pfn(pte_t pte) {
    return (pte.pte & PHYSICAL_MASK) >> PAGE_SHIFT;
}

static inline phys_addr_t pte_phys(pte_t pte) {
    return (phys_addr_t)pte_pfn(pte) << PAGE_SHIFT;
}

2.2 内核页表遍历实现

// arch/x86/mm/fault.c  — 5级页表walk核心
static int __walk_page_table(pgd_t *pgd, unsigned long addr,
                               struct page_walk *walk) {
    p4d_t *p4d;
    pud_t *pud;
    pmd_t *pmd;
    pte_t *pte;
    
    // Level 1: PGD (Page Global Directory)
    pgd = pgd_offset_k(addr);
    if (pgd_none(*pgd) || pgd_bad(*pgd))
        return -EFAULT;
    
    // Level 2: P4D (仅5级页表使用,4级时fold为pgd)
    p4d = p4d_offset(pgd, addr);
    if (p4d_none(*p4d))
        return -EFAULT;
    
    // Level 3: PUD (Page Upper Directory)
    pud = pud_offset(p4d, addr);
    if (pud_none(*pud))
        return -EFAULT;
    
    // 2MB大页检测
    if (pud_large(*pud) && walk->allow_large) {
        phys_addr_t phys = (pud_pfn(*pud) << PAGE_SHIFT) +
                           (addr & ~PUD_MASK);
        walk->phys = phys;
        return 0;
    }
    
    // Level 4: PMD
    pmd = pmd_offset(pud, addr);
    if (pmd_none(*pmd))
        return -EFAULT;
    
    if (pmd_large(*pmd) && walk->allow_large) {
        walk->phys = (pmd_pfn(*pmd) << PAGE_SHIFT) + (addr & ~PMD_MASK);
        return 0;
    }
    
    // Level 5: PTE
    pte = pte_offset_map(pmd, addr);
    if (pte_none(*pte))
        return -EFAULT;
    
    walk->phys = (pte_pfn(*pte) << PAGE_SHIFT) + (addr & ~PAGE_MASK);
    walk->pte = *pte;
    pte_unmap(pte);
    
    return 0;
}

三、malloc→brk→缺页异常 全链路

// glibc malloc内部:
// 小分配(<128KB): 使用arena缓存的chunk
// 大分配(>128KB): 直接mmap
// Arena不够: brk()扩展堆 → 缺页异常 → 内核分配物理页

// Step 1: brk() 扩展进程堆
SYSCALL_DEFINE1(brk, unsigned long, brk) {
    struct mm_struct *mm = current->mm;
    unsigned long newbrk, oldbrk = mm->brk;
    
    newbrk = PAGE_ALIGN(brk);
    if (newbrk < oldbrk)
        goto out;
    
    // 扩展VMA (Virtual Memory Area)
    if (do_brk_flags(oldbrk, newbrk - oldbrk, 0, &populate) != newbrk - oldbrk)
        goto out;
    
    // 可选:主动填充物理页(populate)
    if (populate)
        mm_populate(oldbrk, newbrk - oldbrk);
    
    mm->brk = newbrk;
out:
    return newbrk;
}

// Step 2: 访问新VMA中的地址 → CPU触发Page Fault
// 硬件自动报错: 错误码的低位指示原因
// bit 0 = 0 (not-present)  → page不在内存
// bit 1 = 0 (read)          → 读操作触发的缺页
// bit 2 = 0 (kernel mode)   → 用户态缺页

// Step 3: 缺页异常处理 (arch/x86/mm/fault.c)
static void do_user_addr_fault(struct pt_regs *regs,
                                 unsigned long error_code,
                                 unsigned long address) {
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma;
    vm_fault_t fault;
    
    // 3.1 找到包含该地址的VMA
    vma = find_vma(mm, address);
    if (!vma || vma->vm_start > address) {
        // VMA不存在 → SIGSEGV (真正访问无效内存)
        bad_area(regs, error_code, address);
        return;
    }
    
    // 3.2 权限检查
    if (error_code & X86_PF_PROT) {
        // Protection fault (读写权限不匹配)
        bad_area_access_error(regs, error_code, address, vma);
        return;
    }
    
    // 3.3 核心:处理缺页
    fault = handle_mm_fault(vma, address,
                           error_code & X86_PF_WRITE ? FAULT_FLAG_WRITE : 0,
                           regs);
    
    if (fault & VM_FAULT_ERROR) {
        if (fault & VM_FAULT_OOM)
            pagefault_out_of_memory();
        else if (fault & VM_FAULT_SIGBUS)
            do_sigbus(regs, error_code, address, fault);
        return;
    }
}

// Step 4: handle_mm_fault → 逐级遍历页表并分配
vm_fault_t handle_mm_fault(struct vm_area_struct *vma,
                            unsigned long address, unsigned int flags,
                            struct pt_regs *regs) {
    struct mm_struct *mm = vma->vm_mm;
    pgd_t *pgd;
    p4d_t *p4d;
    vm_fault_t ret;
    
    pgd = pgd_offset(mm, address);
    p4d = p4d_alloc(mm, pgd, address);
    if (!p4d) return VM_FAULT_OOM;
    
    // 一层层往下走,缺哪页分哪页
    ret = __handle_mm_fault(vma, address, flags);
    
    return ret;
}

static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
                                     unsigned long address,
                                     unsigned int flags) {
    struct mm_struct *mm = vma->vm_mm;
    pud_t *pud;
    pmd_t *pmd;
    
    pud = pud_alloc(mm, p4d, address);  // 缺PUD→分配
    pmd = pmd_alloc(mm, pud, address);   // 缺PMD→分配
    
    // 核心:处理PTE
    return handle_pte_fault(vma, address, (pte_t *)pmd, flags);
}

static vm_fault_t handle_pte_fault(struct vm_area_struct *vma,
                                    unsigned long address, pte_t *pte,
                                    unsigned int flags) {
    if (!pte_present(*pte)) {
        // ★ PTE不存在:do_anonymous_page → 分配新物理页
        if (pte_none(*pte))
            return do_anonymous_page(vma, address, pte, flags);
        // PTE存在但swapped out → do_swap_page
        return do_swap_page(vma, address, pte, flags);
    }
    // Copy-on-Write处理
    if (flags & FAULT_FLAG_WRITE) {
        if (!pte_write(*pte))
            return do_wp_page(vma, address, pte, flags);
    }
    return VM_FAULT_NOPAGE;
}

四、伙伴系统(Buddy Allocator)

// 伙伴系统:最底层的物理页分配器
// 将空闲页组织为2^order的块,分裂/合并满足分配请求

// mm/page_alloc.c
// free_area[order]: 每个order维护一个空闲链表
#define MAX_ORDER 11  // 4MB (2^11 × 4KB)

struct free_area {
    struct list_head free_list[MIGRATE_TYPES];
    unsigned long nr_free;  // 该order空闲页数
};

struct zone {
    struct free_area free_area[MAX_ORDER];
    // ...
    unsigned long managed_pages;   // 可用物理页总数
    unsigned long _watermark[NR_WMARK];  // 水位线
};

// 水位线:
// WMARK_HIGH: 高于此线不回收
// WMARK_LOW:  低于此线启动kswapd异步回收  
// WMARK_MIN:  低于此线同步直接回收(allocator必须等待)

static inline struct page *__rmqueue(struct zone *zone,
                                      unsigned int order,
                                      int migratetype) {
    struct page *page;
    
    // 1. 从目标order的空闲链表取
    page = __rmqueue_smallest(zone, order, migratetype);
    if (page) return page;
    
    // 2. 没有空闲块→从更大order分裂
    // 比如: 需要order=0(4KB),但只有order=3(32KB)
    // → 分裂order=3→两个order=2→分裂→两个order=1→分裂→两个order=0
    page = __rmqueue_fallback(zone, order, migratetype);
    
    return page;
}

// 伙伴合并: 释放时检查"buddy"是否也空闲
// buddy地址: page_pfn ^ (1 << order)
static inline void __free_one_page(struct page *page,
                                     unsigned long pfn,
                                     struct zone *zone,
                                     unsigned int order) {
    unsigned long buddy_pfn = __find_buddy_pfn(pfn, order);
    struct page *buddy = pfn_to_page(buddy_pfn);
    
    // 检查buddy是否空闲且同order
    while (order < MAX_ORDER - 1) {
        buddy = __page_find_buddy(page, pfn, order);
        if (!buddy || !page_is_buddy(page, buddy, order))
            break;
        
        // 合并! 从当前order链表删除buddy
        del_page_from_free_list(buddy, zone, order);
        
        // 合并后的页 = min(pfn, buddy_pfn),order+1
        pfn = min(pfn, buddy_pfn);
        page = pfn_to_page(pfn);
        order++;
    }
    
    // 加入对应order的空闲链表
    add_to_free_list(page, zone, order, migratetype);
}

// 内核实际分配API:
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order) {
    return alloc_pages_current(gfp_mask, order);
}

// GFP标志控制分配行为:
// GFP_KERNEL:  允许睡眠+回收(DMA/普通区)
// GFP_ATOMIC:  不睡眠(中断上下文) → 仅用紧急保留
// __GFP_DIRECT_RECLAIM: 允许直接回收
// __GFP_KSWAPD_RECLAIM: 允许唤醒kswapd

void *kmalloc(size_t size, gfp_t flags) {
    // 找到合适的order
    unsigned int order = get_order(size);
    struct page *page = alloc_pages(flags, order);
    if (!page) return NULL;
    return page_address(page);
}

五、Slab/Slub分配器

// Slab: 伙伴系统按页分配(4KB对齐),但内核大量需要<4KB
// → Slab在伙伴分配的页上做"二次分配"

// Slub: Slab的简化版(2.6.23+默认),关键数据结构:
struct kmem_cache {
    unsigned int size;          // 对象大小
    unsigned int object_size;   // 对齐后的对象大小
    unsigned int offset;        // 下一个空闲对象的偏移
    struct kmem_cache_cpu __percpu *cpu_slab;  // 每CPU缓存(☆热路径)
    struct kmem_cache_node *node[MAX_NUMNODES]; // 每NUMA节点
    const char *name;
};

struct kmem_cache_cpu {
    void **freelist;       // 指向第一个空闲对象(★热路径只需要读这个!)
    struct page *page;     // 当前正在使用的slab页
    unsigned long tid;     // 事务ID(无锁CAS更新)
};

// 核心分配逻辑 (mm/slub.c)
static __always_inline void *slab_alloc(struct kmem_cache *s,
                                         gfp_t gfpflags, unsigned long addr) {
    void *object;
    struct kmem_cache_cpu *c;
    unsigned long tid;
    
again:
    // 1. 禁用抢占(per-CPU数据不迁移)
    c = raw_cpu_ptr(s->cpu_slab);
    tid = READ_ONCE(c->tid);
    
    // 2. ★ fastpath: 从per-CPU freelist取
    object = c->freelist;
    if (unlikely(!object || !node_match(c->page, node))) {
        // 3. slowpath: per-CPU freelist空了 → 从partial/node拿新slab
        object = __slab_alloc(s, gfpflags, node, addr, c);
        if (unlikely(!object)) return NULL;
    } else {
        // ★ fastpath成功:freelist = *freelist(链表下一个)
        void *next = get_freepointer_safe(s, object);
        // CAS更新freelist(保证并发安全)
        if (unlikely(!this_cpu_cmpxchg_double(
                s->cpu_slab->freelist, s->cpu_slab->tid,
                object, tid,
                next, next_tid(tid)))) {
            goto again;  // CAS失败→重试
        }
    }
    
    return object;
}

// Slab释放: 反向操作
static __always_inline void slab_free(struct kmem_cache *s,
                                       struct page *page, void *x) {
    void *prior;
    struct kmem_cache_cpu *c;
    
    c = raw_cpu_ptr(s->cpu_slab);
    // 对象加入freelist头部(链表prepend)
    set_freepointer(s, x, c->freelist);
    
    // CAS更新freelist
    if (likely(this_cpu_cmpxchg_double(
            s->cpu_slab->freelist, s->cpu_slab->tid,
            c->freelist, tid,
            x, next_tid(tid)))) {
        return;  // 成功
    }
    // 失败 → 慢路径
    __slab_free(s, page, x);
}

// 查看所有活跃的kmem_cache:
// cat /proc/slabinfo
// 输出: dentry, inode_cache, buffer_head, vm_area_struct, task_struct...
// 每个task_struct/inode/dentry都有专用slab缓存

六、mmap 内核视角

// mmap: 建立VMA映射→缺页时再分配物理页(惰性分配)
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
                unsigned long, prot, unsigned long, flags,
                unsigned long, fd, unsigned long, off) {
    
    struct file *file = NULL;
    unsigned long retval;
    
    // 1. 如果指定了fd,获取file对象
    if (!(flags & MAP_ANONYMOUS)) {
        file = fget(fd);
        if (!file) return -EBADF;
    }
    
    // 2. 创建VMA (Virtual Memory Area)
    retval = vm_mmap_pgoff(file, addr, len, prot, flags, off >> PAGE_SHIFT);
    
    return retval;
}

// VMA结构体: 描述一段虚拟地址空间
struct vm_area_struct {
    unsigned long vm_start;     // 起始虚拟地址
    unsigned long vm_end;       // 结束虚拟地址
    struct mm_struct *vm_mm;    // 所属进程
    pgprot_t vm_page_prot;      // 访问权限
    unsigned long vm_flags;     // VM_READ|VM_WRITE|VM_EXEC|VM_SHARED...
    
    // 链表+红黑树(快速查找)
    struct list_head anon_vma_chain;
    struct rb_node vm_rb;
    
    // 文件映射相关
    struct file *vm_file;
    unsigned long vm_pgoff;    // 文件内偏移(页单位)
    
    // 操作表 → 缺页时调用
    const struct vm_operations_struct *vm_ops;
};

// 匿名映射的缺页处理函数表:
static const struct vm_operations_struct anonymous_vm_ops = {
    .fault = do_anonymous_page,     // ★ 缺页→分配物理页
    .map_pages = filemap_map_pages,
    .page_mkwrite = NULL,           // 匿名页不支持
};

// 文件映射的缺页处理:
static const struct vm_operations_struct generic_file_vm_ops = {
    .fault = filemap_fault,         // ★ 缺页→从磁盘读页
    .map_pages = filemap_map_pages,  // 预读优化
    .page_mkwrite = filemap_page_mkwrite,
};

七、NUMA感知分配

// NUMA: 访问本地内存快(100ns),跨节点慢(300ns+)
// Linux优先分配当前CPU所在节点的物理页

// 查看NUMA拓扑:
// numactl --hardware
// /sys/devices/system/node/node*/meminfo

static inline struct page *alloc_pages_node(int nid,
                                             gfp_t gfp_mask,
                                             unsigned int order) {
    if (nid == NUMA_NO_NODE)
        nid = numa_mem_id();  // 当前CPU的NUMA节点
    
    return __alloc_pages_node(nid, gfp_mask, order);
}

// mbind/move_pages: 用户态绑定内存到指定NUMA节点
// 减少跨节点访问是HPC/数据库的核心优化点

八、性能指标与调优

# 查看内存碎片指数
cat /proc/buddyinfo
# Node 0, zone Normal  2  4  1  0  2  1  1  0  1  0  3
# order:                0  1  2  3  4  5  6  7  8  9  10
# 值=该order空闲块数,大order为空→内存碎片严重

# 缺页统计
perf stat -e page-faults,minor-faults,major-faults -p PID

# THP大页效果
cat /sys/kernel/mm/transparent_hugepage/enabled
echo always > /sys/kernel/mm/transparent_hugepage/enabled

# SLAB分析
slabtop -s c  # 按缓存大小排序

# vmstat实时监控
vmstat 1
# si/so: swap in/out > 0 → 内存压力
# bi/bo: block in/out → 缺页IO

九、总结

malloc(1) 到物理页分配的完整链路:

  1. malloc → glibc ptmalloc2 arena管理
  2. arena不足 → brk/mmap扩展VMA
  3. 访问VMA地址 → CPU触发Page Fault
  4. 内核缺页处理 → 逐级遍历PGD/PUD/PMD/PTE
  5. 缺PTE → buddy分配物理页
  6. 物理页从buddy取 → 4KB slab二次分配

关键优化点:THP大页减少TLB miss、NUMA本地分配降低延迟、SLUB per-CPU缓存消除锁竞争。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐