本文基于 Linux 6.4-rc1 源码(commit ac9a78681b92),系统梳理进程虚拟地址空间的数据结构、多级页表的实现细节、mmap 系统调用的完整路径,以及缺页异常的处理流程。所有代码片段均来自真实内核源文件。
一、进程虚拟地址空间布局
1.1 struct mm_struct:进程的内存管理核心
每个进程拥有独立的虚拟地址空间,其元数据由 struct mm_struct 描述,定义在 include/linux/mm_types.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| struct mm_struct { struct { struct maple_tree mm_mt; unsigned long mmap_base; unsigned long mmap_legacy_base; unsigned long task_size; pgd_t *pgd;
atomic_t mm_users; atomic_t mm_count;
int map_count; spinlock_t page_table_lock; struct rw_semaphore mmap_lock;
unsigned long start_code, end_code; unsigned long start_data, end_data; unsigned long start_brk, brk; unsigned long start_stack; unsigned long arg_start, arg_end; unsigned long env_start, env_end; } __randomize_layout; unsigned long cpu_bitmap[]; };
|
值得注意的是,Linux 6.1 起用 Maple Tree(mm_mt)取代了旧的红黑树 + 链表双结构来管理 VMA,查找性能从 O(log n) 提升,同时减少了锁竞争。mmap_lock 是一把读写信号量,读路径(find_vma 等查询)持读锁,写路径(新建/删除 VMA)持写锁。
1.2 典型 64 位进程虚拟地址空间
在 x86-64 四级页表模式下(LA48),用户态地址空间为 0 ~ 0x0000_7fff_ffff_ffff(128 TB),布局如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 高地址 ┌─────────────────────────────┐ 0xFFFF_FFFF_FFFF_FFFF │ 内核空间 │ (用户态不可访问) ├─────────────────────────────┤ 0xFFFF_8000_0000_0000 │ (非规范地址空洞) │ ├─────────────────────────────┤ 0x0000_7FFF_FFFF_FFFF │ 栈 (向下增长) │ ← start_stack │ ... │ ├─────────────────────────────┤ │ mmap / 动态库映射区 │ ← mmap_base │ ... │ ├─────────────────────────────┤ │ 堆 (向上增长) │ start_brk → brk ├─────────────────────────────┤ │ BSS 段 (.bss) │ │ 数据段 (.data) │ start_data ~ end_data ├─────────────────────────────┤ │ 代码段 (.text) │ start_code ~ end_code └─────────────────────────────┘ 0x0000_0000_0040_0000(通常) 低地址
|
五级页表(LA57)将用户态空间扩展至 128 PB,通过 CONFIG_X86_5LEVEL 开启,运行时由 pgtable_l5_enabled() 检测 CPU 的 LA57 特性位决定是否激活。
二、VMA:虚拟内存区域
2.1 struct vm_area_struct
VMA 是虚拟地址空间的最小管理单元,每段具有相同属性(权限、映射文件)的连续地址区间对应一个 struct vm_area_struct:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| struct vm_area_struct { union { struct { unsigned long vm_start; unsigned long vm_end; }; };
struct mm_struct *vm_mm; pgprot_t vm_page_prot; const vm_flags_t vm_flags;
struct { struct rb_node rb; unsigned long rb_subtree_last; } shared;
struct list_head anon_vma_chain; struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops;
unsigned long vm_pgoff; struct file *vm_file; void *vm_private_data; } __randomize_layout;
|
vm_flags 常用标志(定义在 include/linux/mm.h):
1 2 3 4 5 6 7
| #define VM_READ 0x00000001 #define VM_WRITE 0x00000002 #define VM_EXEC 0x00000004 #define VM_SHARED 0x00000008 #define VM_MAYREAD 0x00000010 #define VM_MAYWRITE 0x00000020 #define VM_MAYEXEC 0x00000040
|
VM_SHARED 与 VM_WRITE 的组合决定了写时复制(COW)行为:MAP_PRIVATE | PROT_WRITE 的 VMA 不含 VM_SHARED,写入时会触发 COW;MAP_SHARED | PROT_WRITE 含 VM_SHARED,写入直接反映到文件。
2.2 struct vm_operations_struct
每类映射都通过 vm_ops 提供钩子,定义在 include/linux/mm.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| struct vm_operations_struct { void (*open)(struct vm_area_struct *area); void (*close)(struct vm_area_struct *area); int (*may_split)(struct vm_area_struct *area, unsigned long addr); int (*mremap)(struct vm_area_struct *area); int (*mprotect)(struct vm_area_struct *vma, unsigned long start, unsigned long end, unsigned long newflags); vm_fault_t (*fault)(struct vm_fault *vmf); vm_fault_t (*huge_fault)(struct vm_fault *vmf, enum page_entry_size pe_size); vm_fault_t (*map_pages)(struct vm_fault *vmf, pgoff_t start_pgoff, pgoff_t end_pgoff); vm_fault_t (*page_mkwrite)(struct vm_fault *vmf); vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf); int (*access)(struct vm_area_struct *vma, unsigned long addr, void *buf, int len, int write); const char *(*name)(struct vm_area_struct *vma); };
|
例如,ext4 文件映射使用 ext4_file_vm_ops,其 fault 钩子会调用 filemap_fault() 从页缓存读取数据;/dev/zero 使用 zero_vm_ops,其 fault 直接返回零页。
2.3 VMA 查找:find_vma
在 Linux 6.1 之后,find_vma 基于 Maple Tree 实现,源码在 mm/mmap.c:
1 2 3 4 5 6 7 8 9
| struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr) { unsigned long index = addr;
mmap_assert_locked(mm); return mt_find(&mm->mm_mt, &index, ULONG_MAX); } EXPORT_SYMBOL(find_vma);
|
mt_find 在 Maple Tree 中找到第一个满足 key >= addr 的区间节点。若返回的 VMA 满足 vma->vm_start <= addr < vma->vm_end,说明 addr 落在该 VMA 内;若 addr < vma->vm_start,则 addr 在一个”空洞”里,返回的是下一个 VMA。调用方在缺页异常处理中需要额外验证前者条件。
三、mmap 系统调用路径
3.1 调用链总览
1 2 3 4 5 6
| 用户态 mmap(2) → sys_mmap_pgoff / SYSCALL_DEFINE6(mmap_pgoff) → ksys_mmap_pgoff() [mm/mmap.c] → vm_mmap_pgoff() → do_mmap() [mm/mmap.c] → mmap_region() [mm/mmap.c]
|
3.2 ksys_mmap_pgoff:文件描述符解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, unsigned long pgoff) { struct file *file = NULL; unsigned long retval;
if (!(flags & MAP_ANONYMOUS)) { audit_mmap_fd(fd, flags); file = fget(fd); if (!file) return -EBADF; if (is_file_hugepages(file)) { len = ALIGN(len, huge_page_size(hstate_file(file))); } else if (unlikely(flags & MAP_HUGETLB)) { retval = -EINVAL; goto out_fput; } } else if (flags & MAP_HUGETLB) { ... } retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff); out_fput: if (file) fput(file); return retval; }
|
匿名映射(MAP_ANONYMOUS)时 file 为 NULL,pgoff 对私有匿名映射被设置为 addr >> PAGE_SHIFT(在 do_mmap 中),用于 anon_vma 的索引。
3.3 do_mmap:权限检查与标志计算
do_mmap(mm/mmap.c line 1222)是核心逻辑层,负责:
- 调用
get_unmapped_area() 找到合适的地址区间(ASLR 随机化在此发生);
- 通过
calc_vm_prot_bits / calc_vm_flag_bits 将 POSIX PROT_* 和 MAP_* 转换为内核 VM_* 标志;
- 对文件映射验证文件模式(
FMODE_READ / FMODE_WRITE);
MAP_SHARED 文件映射:添加 VM_SHARED | VM_MAYSHARE;MAP_PRIVATE 文件映射:不添加 VM_SHARED,写入时触发 COW;
- 最终调用
mmap_region()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| vm_flags = calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) | mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; ...
case MAP_SHARED: vm_flags |= VM_SHARED | VM_MAYSHARE; if (!(file->f_mode & FMODE_WRITE)) vm_flags &= ~(VM_MAYWRITE | VM_SHARED); fallthrough;
case MAP_PRIVATE: if (!(file->f_mode & FMODE_READ)) return -EACCES; break;
|
3.4 mmap_region:创建 VMA
mmap_region(mm/mmap.c line 2547)是真正分配 VMA 的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| unsigned long mmap_region(struct file *file, unsigned long addr, unsigned long len, vm_flags_t vm_flags, unsigned long pgoff, struct list_head *uf) { struct mm_struct *mm = current->mm; struct vm_area_struct *vma = NULL; ... if (vma && !vma_expand(&vmi, vma, merge_start, merge_end, vm_pgoff, next)) { khugepaged_enter_vma(vma, vm_flags); goto expanded; }
cannot_expand: vma = vm_area_alloc(mm); vma->vm_start = addr; vma->vm_end = end; vm_flags_init(vma, vm_flags); vma->vm_page_prot = vm_get_page_prot(vm_flags); vma->vm_pgoff = pgoff;
if (file) { vma->vm_file = get_file(file); error = call_mmap(file, vma); ... } else if (vm_flags & VM_SHARED) { error = shmem_zero_setup(vma); } else { vma_set_anonymous(vma); } ... }
|
MAP_PRIVATE vs MAP_SHARED 的本质区别:
| 特性 |
MAP_PRIVATE |
MAP_SHARED |
| vm_flags |
无 VM_SHARED |
含 VM_SHARED |
| 写时行为 |
COW:产生进程私有副本 |
直接修改底层页,其他进程可见 |
| 文件刷盘 |
修改不回写文件 |
修改最终通过 writeback 回写 |
| 匿名映射 |
vm_ops = NULL |
通过 shmem 实现共享 |
四、多级页表结构
4.1 x86-64 五级页表层级
x86-64 采用分级页表将 57 位(五级)或 48 位(四级)虚拟地址翻译为物理地址。各级索引位划分如下(五级,arch/x86/include/asm/pgtable_64_types.h):
1 2 3 4 5 6 7
| 虚拟地址 [56:0](57 位): bits[56:48] → PGD 索引(9 bit,512 项) bits[47:39] → P4D 索引(9 bit,512 项) bits[38:30] → PUD 索引(9 bit,512 项) PUD_SHIFT = 30 bits[29:21] → PMD 索引(9 bit,512 项) PMD_SHIFT = 21 bits[20:12] → PTE 索引(9 bit,512 项) bits[11:0] → 页内偏移(12 bit,4 KB)
|
对应的类型定义(arch/x86/include/asm/pgtable_64_types.h):
1 2 3 4 5 6 7 8 9 10 11
| typedef unsigned long pteval_t; typedef unsigned long pmdval_t; typedef unsigned long pudval_t; typedef unsigned long p4dval_t; typedef unsigned long pgdval_t;
typedef struct { pteval_t pte; } pte_t; typedef struct { pmdval_t pmd; } pmd_t; typedef struct { pudval_t pud; } pud_t; typedef struct { p4dval_t p4d; } p4d_t; typedef struct { pgdval_t pgd; } pgd_t;
|
使用强类型结构体而非裸 unsigned long,可以让编译器在混用不同层级页表项时产生类型错误,是内核防御性编程的典型实践。
4.2 页表项位域
x86-64 每个页表项(PTE)为 64 位,关键位定义在 arch/x86/include/asm/pgtable_types.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #define _PAGE_BIT_PRESENT 0 #define _PAGE_BIT_RW 1 #define _PAGE_BIT_USER 2 #define _PAGE_BIT_PWT 3 #define _PAGE_BIT_PCD 4 #define _PAGE_BIT_ACCESSED 5 #define _PAGE_BIT_DIRTY 6 #define _PAGE_BIT_PSE 7 #define _PAGE_BIT_GLOBAL 8 #define _PAGE_BIT_NX 63
#define _PAGE_PRESENT (_AT(pteval_t, 1) << _PAGE_BIT_PRESENT) #define _PAGE_RW (_AT(pteval_t, 1) << _PAGE_BIT_RW) #define _PAGE_USER (_AT(pteval_t, 1) << _PAGE_BIT_USER) #define _PAGE_ACCESSED (_AT(pteval_t, 1) << _PAGE_BIT_ACCESSED) #define _PAGE_DIRTY (_AT(pteval_t, 1) << _PAGE_BIT_DIRTY) #define _PAGE_PSE (_AT(pteval_t, 1) << _PAGE_BIT_PSE) #define _PAGE_NX (_AT(pteval_t, 1) << _PAGE_BIT_NX)
|
典型用户页保护配置(arch/x86/include/asm/pgtable_types.h):
1 2 3 4
| #define PAGE_SHARED __pg(__PP|__RW|_USR|___A|__NX|0|0|0)
#define PAGE_READONLY_EXEC __pg(__PP|0|_USR|___A|0|0|0|0)
|
4.3 页表遍历宏
include/linux/pgtable.h 提供了标准的多级页表遍历接口:
1 2 3 4 5 6 7 8 9 10 11 12 13
| #define pgd_offset(mm, address) pgd_offset_pgd((mm)->pgd, (address))
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address) { return pud_pgtable(*pud) + pmd_index(address); } static inline pud_t *pud_offset(p4d_t *p4d, unsigned long address) { return p4d_pgtable(*p4d) + pud_index(address); }
#define pte_offset_map(dir, address) pte_offset_kernel((dir), (address))
|
一次完整的地址翻译调用链(见 include/linux/pgtable.h line 153):
1 2 3 4 5 6 7 8
| static inline pmd_t *pmd_off(struct mm_struct *mm, unsigned long va) { return pmd_offset( pud_offset( p4d_offset( pgd_offset(mm, va), va), va), va); }
|
五、缺页异常处理
5.1 入口:exc_page_fault
CPU 触发 #PF 异常后,进入 IDT 注册的处理函数(arch/x86/mm/fault.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault) { unsigned long address = read_cr2(); irqentry_state_t state;
prefetchw(¤t->mm->mmap_lock);
if (kvm_handle_async_pf(regs, (u32)address)) return;
state = irqentry_enter(regs); instrumentation_begin(); handle_page_fault(regs, error_code, address); instrumentation_end(); ... }
|
handle_page_fault 判断缺页地址属于内核空间还是用户空间,用户空间路径调用 do_user_addr_fault,最终到达通用的 handle_mm_fault。
5.2 __handle_mm_fault:分配页表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma, unsigned long address, unsigned int flags) { struct vm_fault vmf = { .vma = vma, .address = address & PAGE_MASK, .flags = flags, .pgoff = linear_page_index(vma, address), .gfp_mask = __get_fault_gfp_mask(vma), }; struct mm_struct *mm = vma->vm_mm; pgd_t *pgd; p4d_t *p4d;
pgd = pgd_offset(mm, address); p4d = p4d_alloc(mm, pgd, address); if (!p4d) return VM_FAULT_OOM;
vmf.pud = pud_alloc(mm, p4d, address); if (!vmf.pud) return VM_FAULT_OOM;
if (pud_none(*vmf.pud) && hugepage_vma_check(vma, vm_flags, false, true, true)) { ret = create_huge_pud(&vmf); if (!(ret & VM_FAULT_FALLBACK)) return ret; }
vmf.pmd = pmd_alloc(mm, vmf.pud, address); if (!vmf.pmd) return VM_FAULT_OOM;
if (pmd_none(*vmf.pmd) && hugepage_vma_check(vma, vm_flags, false, true, true)) { ret = create_huge_pmd(&vmf); if (!(ret & VM_FAULT_FALLBACK)) return ret; }
return handle_pte_fault(&vmf); }
|
5.3 handle_pte_fault:分发具体缺页类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| static vm_fault_t handle_pte_fault(struct vm_fault *vmf) { pte_t entry;
if (unlikely(pmd_none(*vmf->pmd))) { vmf->pte = NULL; } else { vmf->pte = pte_offset_map(vmf->pmd, vmf->address); vmf->orig_pte = *vmf->pte; barrier(); if (pte_none(vmf->orig_pte)) { pte_unmap(vmf->pte); vmf->pte = NULL; } }
if (!vmf->pte) return do_pte_missing(vmf);
if (!pte_present(vmf->orig_pte)) return do_swap_page(vmf);
if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma)) return do_numa_page(vmf);
if (vmf->flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) { if (!pte_write(entry)) return do_wp_page(vmf); } ... }
|
do_pte_missing 会进一步区分:
1 2 3 4
| if (vma->vm_ops) return do_fault(vmf); return do_anonymous_page(vmf);
|
5.4 匿名缺页:do_anonymous_page
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| static vm_fault_t do_anonymous_page(struct vm_fault *vmf) { struct vm_area_struct *vma = vmf->vma; struct folio *folio; pte_t entry;
if (!(vmf->flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(vma->vm_mm)) { entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address), vma->vm_page_prot)); goto setpte; }
if (unlikely(anon_vma_prepare(vma))) goto oom; folio = vma_alloc_zeroed_movable_folio(vma, vmf->address); if (!folio) goto oom;
entry = mk_pte(&folio->page, vma->vm_page_prot); entry = pte_sw_mkyoung(entry); if (vma->vm_flags & VM_WRITE) entry = pte_mkwrite(pte_mkdirty(entry));
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address, &vmf->ptl); set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry); ... }
|
零页优化:进程第一次读未初始化匿名内存时,内核将虚拟地址映射到一个全局共享的只读零页(my_zero_pfn),物理内存实际零分配。当写入发生时,才触发 COW 分配真实物理页。
5.5 文件缺页:__do_fault
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static vm_fault_t __do_fault(struct vm_fault *vmf) { struct vm_area_struct *vma = vmf->vma; vm_fault_t ret;
if (pmd_none(*vmf->pmd) && !vmf->prealloc_pte) { vmf->prealloc_pte = pte_alloc_one(vma->vm_mm); if (!vmf->prealloc_pte) return VM_FAULT_OOM; }
ret = vma->vm_ops->fault(vmf); ... return ret; }
|
对于 ext4/xfs 等文件系统,vm_ops->fault 最终调用 filemap_fault,先在 page cache 中查找,命中则直接返回(minor fault);否则提交 I/O 并等待(major fault)。
5.6 写时复制:do_wp_page
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| static vm_fault_t do_wp_page(struct vm_fault *vmf) __releases(vmf->ptl) { struct vm_area_struct *vma = vmf->vma; struct folio *folio = NULL;
vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
if (vma->vm_flags & (VM_SHARED | VM_MAYSHARE)) { if (!vmf->page) return wp_pfn_shared(vmf); return wp_page_shared(vmf); }
if (folio && folio_test_anon(folio)) { if (PageAnonExclusive(vmf->page)) goto reuse; }
return wp_page_copy(vmf); }
|
wp_page_copy 分配一个新的物理页,调用 __wp_page_copy_user 复制内容,然后将原来的只读 PTE 替换为指向新页的可写 PTE,并 flush TLB 使旧映射失效。
六、TLB 管理
6.1 TLB 的作用
TLB(Translation Lookaside Buffer)是 CPU 内部对最近页表查找结果的缓存,避免每次内存访问都遍历多级页表(每级一次内存读取)。x86-64 通常有独立的 L1 iTLB / dTLB 以及共享的 L2 TLB。
当内核修改页表(unmap、mprotect、mremap 等)后,必须使相关 TLB 条目失效(TLB shootdown),否则其他 CPU 可能使用过时的映射。
6.2 TLB 刷新接口
1 2 3 4 5 6 7 8
| flush_tlb_mm(mm);
flush_tlb_range(vma, start, end);
flush_tlb_page(vma, address);
|
x86 的 SMP TLB shootdown 通过 IPI(Inter-Processor Interrupt)通知其他 CPU 执行 INVLPG 指令(针对单页)或 MOV CR3(针对整个地址空间)。
6.3 PCID 优化
传统 MOV CR3 切换进程时会完全刷新 TLB,代价高昂。Intel Haswell 以后支持 PCID(Process Context Identifier),允许 TLB 条目携带 12 位的进程标识,切换时通过设置 CR3 的第 63 位为 0 来保留其他进程的 TLB 条目:
- 内核在
switch_mm_irqs_off 中维护 PCID → mm 的映射;
- 每个 CPU 可缓存最多 6 个活跃 PCID(内核实现中);
- Meltdown 修复引入了内核/用户 PCID 对(Kaiser/PTI),切换额外开销约 100 ns。
6.4 透明大页(THP)
THP(Transparent Huge Pages)允许匿名映射自动使用 2 MB PMD 大页,由 __handle_mm_fault 中的 create_huge_pmd 触发,实际调用 do_huge_pmd_anonymous_page:
- 分配 512 个连续物理页(order-9 compound page);
- 设置 PMD 项的
_PAGE_PSE 位,指向 2 MB 物理基地址;
- TLB 条目覆盖 2 MB,减少 TLB miss 次数;
- 写时发生 COW 时通过
__split_huge_pmd 降级为 4 KB 页。
七、诊断方法
7.1 /proc/PID/maps
/proc/PID/maps 列出进程所有 VMA,每行对应一个 vm_area_struct:
1 2 3 4 5 6 7
| 地址范围 权限 偏移量 设备 inode 文件路径 7f3a4c000000-7f3a4c021000 r--p 00000000 fd:01 1234567 /lib/x86_64-linux-gnu/libc.so.6 7f3a4c021000-7f3a4c176000 r-xp 00021000 fd:01 1234567 /lib/x86_64-linux-gnu/libc.so.6 7f3a4c176000-7f3a4c1c4000 r--p 00176000 fd:01 1234567 /lib/x86_64-linux-gnu/libc.so.6 7f3a4c1c4000-7f3a4c1c5000 r--p 001c3000 fd:01 1234567 /lib/x86_64-linux-gnu/libc.so.6 7f3a4c1c5000-7f3a4c1c8000 rw-p 001c4000 fd:01 1234567 /lib/x86_64-linux-gnu/libc.so.6 7fff8e200000-7fff8e221000 rw-p 00000000 00:00 0 [stack]
|
权限字段含义:r(VM_READ)、w(VM_WRITE)、x(VM_EXEC)、p(MAP_PRIVATE)/ s(MAP_SHARED)。
7.2 /proc/PID/smaps
smaps 在 maps 基础上提供每个 VMA 的内存统计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 7f3a4c021000-7f3a4c176000 r-xp 00021000 fd:01 1234567 /lib/.../libc.so.6 Size: 1364 kB # VMA 虚拟大小 KernelPageSize: 4 kB # 内核页大小 MMUPageSize: 4 kB # MMU 页大小 Rss: 892 kB # 常驻内存(已映射物理页) Pss: 124 kB # 按共享比例分摊的常驻内存 Shared_Clean: 892 kB # 共享且未脏页 Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 0 kB Referenced: 892 kB # 近期被访问的页 Anonymous: 0 kB # 匿名页数量 LazyFree: 0 kB # madvise(MADV_FREE) 标记待回收 AnonHugePages: 0 kB # 透明大页匿名部分 ShmemPmdMapped: 0 kB FilePmdMapped: 0 kB Swap: 0 kB # 换出到 swap 的页
|
7.3 /proc/PID/pagemap
pagemap 是虚拟地址到物理帧(PFN)的映射接口,每个虚拟页对应一个 8 字节条目:
- bit 63:页存在(Present)
- bit 62:页换出到 swap
- bit 61:文件映射或共享匿名页
- bits 54:0:若存在,为 PFN;若换出,为 swap 偏移
示例读取脚本:
1 2 3 4 5 6 7 8 9 10 11 12
| python3 -c " import struct, os, sys pid = int(sys.argv[1]) va = 0x400000 with open(f'/proc/{pid}/pagemap', 'rb') as f: f.seek((va >> 12) * 8) entry = struct.unpack('Q', f.read(8))[0] present = (entry >> 63) & 1 pfn = entry & ((1 << 55) - 1) print(f'present={present}, pfn=0x{pfn:x}, phys=0x{pfn << 12:x}') " <PID>
|
7.4 /proc/PID/status 内存字段
1 2 3 4 5 6 7
| VmPeak: 102400 kB # 历史峰值虚拟内存 VmSize: 98304 kB # 当前虚拟内存大小 VmRSS: 12288 kB # 常驻物理内存(= RssAnon + RssFile + RssShmem) RssAnon: 8192 kB # 匿名页 RSS RssFile: 4096 kB # 文件映射 RSS RssShmem: 0 kB # 共享内存 RSS VmSwap: 1024 kB # 换出到 swap 的大小
|
7.5 pmap -x
输出中 RSS 列为常驻集大小,Dirty 列为脏页大小(写入但未回写),Mapping 列为文件名或 [ anon ]/ [ stack ]。
7.6 bpftrace 追踪缺页
1 2 3 4 5 6 7 8 9 10 11 12
| bpftrace -e ' kprobe:handle_mm_fault /pid == $1/ { printf("fault addr=0x%lx flags=0x%x comm=%s\n", arg1, arg2, comm); }' <PID>
bpftrace -e 'kprobe:do_anonymous_page { @[comm] = count(); }'
bpftrace -e 'kprobe:do_wp_page { @[comm] = count(); }'
|
7.7 perf 统计缺页频率
1 2 3 4 5 6
| perf stat -e page-faults,major-faults,minor-faults -p <PID> -- sleep 5
perf record -e page-faults -g -p <PID> -- sleep 10 perf script | stackcollapse-perf.pl | flamegraph.pl > pagefault.svg
|
minor-faults(次缺页):页表项缺失但数据已在内存(如零页映射、文件已在 page cache),无需 I/O;major-faults(主缺页):需要从磁盘读取数据,延迟高达毫秒级。
八、源码阅读路线
理解本文涉及的机制,建议按以下顺序阅读源码:
| 文件 |
关键内容 |
include/linux/mm_types.h |
mm_struct、vm_area_struct、vm_fault 等核心数据结构 |
include/linux/mm.h |
vm_operations_struct、VM_* 标志、find_vma/find_vma_intersection 等内联函数 |
include/linux/pgtable.h |
pgd_offset、pmd_offset、pte_offset_map 宏与页表遍历函数 |
arch/x86/include/asm/pgtable_types.h |
x86 页表项位定义、pgd_t/pte_t 类型 |
arch/x86/include/asm/pgtable_64_types.h |
x86-64 各级页表移位常量(PGDIR_SHIFT、PMD_SHIFT 等) |
mm/mmap.c |
do_mmap、mmap_region、find_vma、vma_merge 实现 |
mm/memory.c |
handle_mm_fault、handle_pte_fault、do_anonymous_page、do_fault、do_wp_page |
arch/x86/mm/fault.c |
x86 缺页异常入口 exc_page_fault → handle_page_fault |
总结
虚拟内存是现代操作系统最重要的抽象之一。Linux 通过 mm_struct 和 VMA 体系将进程的虚拟地址空间划分为具有语义的区段,通过多级页表(四级/五级)在硬件层面实现地址翻译,通过缺页异常机制实现按需分页(demand paging)、写时复制(COW)和内存映射文件(mmap)。TLB 则是整个体系的性能关键,PCID 和 Huge Page 是两种重要的 TLB 优化手段。
理解这套机制不仅有助于写出更高效的用户态程序(合理使用 mmap、mlock、madvise),也是分析内存泄漏、性能瓶颈、OOM 问题的基础。后续文章将深入讨论物理内存分配器(Buddy System 与 SLAB)以及内存回收(Kswapd 与 LRU 算法)。