Linux内存管理深度解析:从Level 5页表到缺页异常的完整旅程
·
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) 到物理页分配的完整链路:
- malloc → glibc ptmalloc2 arena管理
- arena不足 → brk/mmap扩展VMA
- 访问VMA地址 → CPU触发Page Fault
- 内核缺页处理 → 逐级遍历PGD/PUD/PMD/PTE
- 缺PTE → buddy分配物理页
- 物理页从buddy取 → 4KB slab二次分配
关键优化点:THP大页减少TLB miss、NUMA本地分配降低延迟、SLUB per-CPU缓存消除锁竞争。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)