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#definePGDIR_SHIFT39#definePUD_SHIFT30#definePMD_SHIFT21#definePAGE_SHIFT12// 页表项(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)typedefstruct{unsignedlongpte;}pte_t;staticinlineunsignedlongpte_pfn(pte_tpte){return(pte.ptePHYSICAL_MASK)PAGE_SHIFT;}staticinlinephys_addr_tpte_phys(pte_tpte){return(phys_addr_t)pte_pfn(pte)PAGE_SHIFT;}2.2 内核页表遍历实现// arch/x86/mm/fault.c — 5级页表walk核心staticint__walk_page_table(pgd_t*pgd,unsignedlongaddr,structpage_walk*walk){p4d_t*p4d;pud_t*pud;pmd_t*pmd;pte_t*pte;// Level 1: PGD (Page Global Directory)pgdpgd_offset_k(addr);if(pgd_none(*pgd)||pgd_bad(*pgd))return-EFAULT;// Level 2: P4D (仅5级页表使用4级时fold为pgd)p4dp4d_offset(pgd,addr);if(p4d_none(*p4d))return-EFAULT;// Level 3: PUD (Page Upper Directory)pudpud_offset(p4d,addr);if(pud_none(*pud))return-EFAULT;// 2MB大页检测if(pud_large(*pud)walk-allow_large){phys_addr_tphys(pud_pfn(*pud)PAGE_SHIFT)(addr~PUD_MASK);walk-physphys;return0;}// Level 4: PMDpmdpmd_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);return0;}// Level 5: PTEptepte_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);return0;}三、malloc→brk→缺页异常 全链路// glibc malloc内部:// 小分配(128KB): 使用arena缓存的chunk// 大分配(128KB): 直接mmap// Arena不够: brk()扩展堆 → 缺页异常 → 内核分配物理页// Step 1: brk() 扩展进程堆SYSCALL_DEFINE1(brk,unsignedlong,brk){structmm_struct*mmcurrent-mm;unsignedlongnewbrk,oldbrkmm-brk;newbrkPAGE_ALIGN(brk);if(newbrkoldbrk)gotoout;// 扩展VMA (Virtual Memory Area)if(do_brk_flags(oldbrk,newbrk-oldbrk,0,populate)!newbrk-oldbrk)gotoout;// 可选主动填充物理页(populate)if(populate)mm_populate(oldbrk,newbrk-oldbrk);mm-brknewbrk;out:returnnewbrk;}// 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)staticvoiddo_user_addr_fault(structpt_regs*regs,unsignedlongerror_code,unsignedlongaddress){structmm_struct*mmcurrent-mm;structvm_area_struct*vma;vm_fault_tfault;// 3.1 找到包含该地址的VMAvmafind_vma(mm,address);if(!vma||vma-vm_startaddress){// VMA不存在 → SIGSEGV (真正访问无效内存)bad_area(regs,error_code,address);return;}// 3.2 权限检查if(error_codeX86_PF_PROT){// Protection fault (读写权限不匹配)bad_area_access_error(regs,error_code,address,vma);return;}// 3.3 核心处理缺页faulthandle_mm_fault(vma,address,error_codeX86_PF_WRITE?FAULT_FLAG_WRITE:0,regs);if(faultVM_FAULT_ERROR){if(faultVM_FAULT_OOM)pagefault_out_of_memory();elseif(faultVM_FAULT_SIGBUS)do_sigbus(regs,error_code,address,fault);return;}}// Step 4: handle_mm_fault → 逐级遍历页表并分配vm_fault_thandle_mm_fault(structvm_area_struct*vma,unsignedlongaddress,unsignedintflags,structpt_regs*regs){structmm_struct*mmvma-vm_mm;pgd_t*pgd;p4d_t*p4d;vm_fault_tret;pgdpgd_offset(mm,address);p4dp4d_alloc(mm,pgd,address);if(!p4d)returnVM_FAULT_OOM;// 一层层往下走缺哪页分哪页ret__handle_mm_fault(vma,address,flags);returnret;}staticvm_fault_t__handle_mm_fault(structvm_area_struct*vma,unsignedlongaddress,unsignedintflags){structmm_struct*mmvma-vm_mm;pud_t*pud;pmd_t*pmd;pudpud_alloc(mm,p4d,address);// 缺PUD→分配pmdpmd_alloc(mm,pud,address);// 缺PMD→分配// 核心处理PTEreturnhandle_pte_fault(vma,address,(pte_t*)pmd,flags);}staticvm_fault_thandle_pte_fault(structvm_area_struct*vma,unsignedlongaddress,pte_t*pte,unsignedintflags){if(!pte_present(*pte)){// ★ PTE不存在do_anonymous_page → 分配新物理页if(pte_none(*pte))returndo_anonymous_page(vma,address,pte,flags);// PTE存在但swapped out → do_swap_pagereturndo_swap_page(vma,address,pte,flags);}// Copy-on-Write处理if(flagsFAULT_FLAG_WRITE){if(!pte_write(*pte))returndo_wp_page(vma,address,pte,flags);}returnVM_FAULT_NOPAGE;}四、伙伴系统(Buddy Allocator)// 伙伴系统最底层的物理页分配器// 将空闲页组织为2^order的块分裂/合并满足分配请求// mm/page_alloc.c// free_area[order]: 每个order维护一个空闲链表#defineMAX_ORDER11// 4MB (2^11 × 4KB)structfree_area{structlist_headfree_list[MIGRATE_TYPES];unsignedlongnr_free;// 该order空闲页数};structzone{structfree_areafree_area[MAX_ORDER];// ...unsignedlongmanaged_pages;// 可用物理页总数unsignedlong_watermark[NR_WMARK];// 水位线};// 水位线:// WMARK_HIGH: 高于此线不回收// WMARK_LOW: 低于此线启动kswapd异步回收// WMARK_MIN: 低于此线同步直接回收(allocator必须等待)staticinlinestructpage*__rmqueue(structzone*zone,unsignedintorder,intmigratetype){structpage*page;// 1. 从目标order的空闲链表取page__rmqueue_smallest(zone,order,migratetype);if(page)returnpage;// 2. 没有空闲块→从更大order分裂// 比如: 需要order0(4KB)但只有order3(32KB)// → 分裂order3→两个order2→分裂→两个order1→分裂→两个order0page__rmqueue_fallback(zone,order,migratetype);returnpage;}// 伙伴合并: 释放时检查buddy是否也空闲// buddy地址: page_pfn ^ (1 order)staticinlinevoid__free_one_page(structpage*page,unsignedlongpfn,structzone*zone,unsignedintorder){unsignedlongbuddy_pfn__find_buddy_pfn(pfn,order);structpage*buddypfn_to_page(buddy_pfn);// 检查buddy是否空闲且同orderwhile(orderMAX_ORDER-1){buddy__page_find_buddy(page,pfn,order);if(!buddy||!page_is_buddy(page,buddy,order))break;// 合并! 从当前order链表删除buddydel_page_from_free_list(buddy,zone,order);// 合并后的页 min(pfn, buddy_pfn)order1pfnmin(pfn,buddy_pfn);pagepfn_to_page(pfn);order;}// 加入对应order的空闲链表add_to_free_list(page,zone,order,migratetype);}// 内核实际分配API:structpage*alloc_pages(gfp_tgfp_mask,unsignedintorder){returnalloc_pages_current(gfp_mask,order);}// GFP标志控制分配行为:// GFP_KERNEL: 允许睡眠回收(DMA/普通区)// GFP_ATOMIC: 不睡眠(中断上下文) → 仅用紧急保留// __GFP_DIRECT_RECLAIM: 允许直接回收// __GFP_KSWAPD_RECLAIM: 允许唤醒kswapdvoid*kmalloc(size_tsize,gfp_tflags){// 找到合适的orderunsignedintorderget_order(size);structpage*pagealloc_pages(flags,order);if(!page)returnNULL;returnpage_address(page);}五、Slab/Slub分配器// Slab: 伙伴系统按页分配(4KB对齐)但内核大量需要4KB// → Slab在伙伴分配的页上做二次分配// Slub: Slab的简化版(2.6.23默认)关键数据结构:structkmem_cache{unsignedintsize;// 对象大小unsignedintobject_size;// 对齐后的对象大小unsignedintoffset;// 下一个空闲对象的偏移structkmem_cache_cpu__percpu*cpu_slab;// 每CPU缓存(☆热路径)structkmem_cache_node*node[MAX_NUMNODES];// 每NUMA节点constchar*name;};structkmem_cache_cpu{void**freelist;// 指向第一个空闲对象(★热路径只需要读这个!)structpage*page;// 当前正在使用的slab页unsignedlongtid;// 事务ID(无锁CAS更新)};// 核心分配逻辑 (mm/slub.c)static__always_inlinevoid*slab_alloc(structkmem_cache*s,gfp_tgfpflags,unsignedlongaddr){void*object;structkmem_cache_cpu*c;unsignedlongtid;again:// 1. 禁用抢占(per-CPU数据不迁移)craw_cpu_ptr(s-cpu_slab);tidREAD_ONCE(c-tid);// 2. ★ fastpath: 从per-CPU freelist取objectc-freelist;if(unlikely(!object||!node_match(c-page,node))){// 3. slowpath: per-CPU freelist空了 → 从partial/node拿新slabobject__slab_alloc(s,gfpflags,node,addr,c);if(unlikely(!object))returnNULL;}else{// ★ fastpath成功freelist *freelist(链表下一个)void*nextget_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)))){gotoagain;// CAS失败→重试}}returnobject;}// Slab释放: 反向操作static__always_inlinevoidslab_free(structkmem_cache*s,structpage*page,void*x){void*prior;structkmem_cache_cpu*c;craw_cpu_ptr(s-cpu_slab);// 对象加入freelist头部(链表prepend)set_freepointer(s,x,c-freelist);// CAS更新freelistif(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,unsignedlong,addr,unsignedlong,len,unsignedlong,prot,unsignedlong,flags,unsignedlong,fd,unsignedlong,off){structfile*fileNULL;unsignedlongretval;// 1. 如果指定了fd获取file对象if(!(flagsMAP_ANONYMOUS)){filefget(fd);if(!file)return-EBADF;}// 2. 创建VMA (Virtual Memory Area)retvalvm_mmap_pgoff(file,addr,len,prot,flags,offPAGE_SHIFT);returnretval;}// VMA结构体: 描述一段虚拟地址空间structvm_area_struct{unsignedlongvm_start;// 起始虚拟地址unsignedlongvm_end;// 结束虚拟地址structmm_struct*vm_mm;// 所属进程pgprot_tvm_page_prot;// 访问权限unsignedlongvm_flags;// VM_READ|VM_WRITE|VM_EXEC|VM_SHARED...// 链表红黑树(快速查找)structlist_headanon_vma_chain;structrb_nodevm_rb;// 文件映射相关structfile*vm_file;unsignedlongvm_pgoff;// 文件内偏移(页单位)// 操作表 → 缺页时调用conststructvm_operations_struct*vm_ops;};// 匿名映射的缺页处理函数表:staticconststructvm_operations_structanonymous_vm_ops{.faultdo_anonymous_page,// ★ 缺页→分配物理页.map_pagesfilemap_map_pages,.page_mkwriteNULL,// 匿名页不支持};// 文件映射的缺页处理:staticconststructvm_operations_structgeneric_file_vm_ops{.faultfilemap_fault,// ★ 缺页→从磁盘读页.map_pagesfilemap_map_pages,// 预读优化.page_mkwritefilemap_page_mkwrite,};七、NUMA感知分配// NUMA: 访问本地内存快(100ns)跨节点慢(300ns)// Linux优先分配当前CPU所在节点的物理页// 查看NUMA拓扑:// numactl --hardware// /sys/devices/system/node/node*/meminfostaticinlinestructpage*alloc_pages_node(intnid,gfp_tgfp_mask,unsignedintorder){if(nidNUMA_NO_NODE)nidnuma_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为空→内存碎片严重# 缺页统计perfstat-epage-faults,minor-faults,major-faults-pPID# THP大页效果cat/sys/kernel/mm/transparent_hugepage/enabledechoalways/sys/kernel/mm/transparent_hugepage/enabled# SLAB分析slabtop-sc# 按缓存大小排序# vmstat实时监控vmstat1# 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缓存消除锁竞争。