当用户访问一个虚拟地址时,MMU(内存管理单元)会首先查找对应的页表项是否存在。若页表项存在,这意味着该虚拟地址对应的物理页帧及其 page 结构体已存在于内存中,且物理地址有效,此时可直接通过页表获取物理地址并完成内存访问。页表项不存在但 page 和物理页存在的情况若页表项不存在,但 page 结构体和物理页已存在于内存中,会触发缺页异常。以下分文件映射和匿名页两种情况进行处理:
文件映射情况
首先,通过虚拟地址在当前进程的虚拟地址空间中查找对应的 vm_area_struct。该结构体包含 vm_start 和 vm_end,可用于判断虚拟地址是否处于此范围内。计算虚拟地址相对于 vm_start 的偏移量 page_offset,再根据此偏移量计算页索引 pgoff = page_offset >> PAGE_SHIFT。
对于文件映射,可通过 vm_area_struct 的内部成员 vm_file 查找该区域所属的文件 file,进而找到该文件的元数据 inode。在 inode 内部,借助 address_space 结构体的成员 xarray 可获取所有的 page 结构体。利用之前计算得到的页索引 pgoff,调用 struct page *page = xarray_lookup(&mapping->pages, pgoff) 就能定位到具体的那一页。
获取到实际的物理页帧后,通过虚拟地址逐层解析页目录结构,找到最后一级页表项(PTE)的存储位置。将计算得到的物理页帧号(pfn)与标志位(如存在位、读写权限等)组合后填充到该 PTE 中。标志位的设置需要依据 vm_area_struct 的权限(vm_flags)来确定,例如若 vm_flags 包含 VM_WRITE,则需设置 PTE 的写权限标志。
匿名页情况
匿名页通常用于进程的堆、栈等区域,不与具体文件关联。当触发缺页异常且判断为匿名页情况时,首先检查该匿名页是否已被换出到交换空间。
若匿名页已被换出,会根据 vm_area_struct 中记录的交换项信息,从交换空间中将其换入内存。这涉及到分配一个新的物理页,将交换空间中的数据读取到该物理页,并更新 page 结构体的相关信息,如设置 page 的映射信息和引用计数。
若匿名页未被换出,只是页表项缺失,可通过 vm_area_struct 中的 vm_pgoff(页偏移量)和虚拟地址计算出对应的 page。这通常是通过 anon_vma 结构来管理的,它维护了匿名页的反向映射关系。找到 page 后,同样获取其物理页帧号(pfn),并根据 vm_area_struct 的权限设置标志位,将物理页帧号和标志位组合后填充到对应的 PTE 中。根据虚拟地址找不到 page 或者 vm_area_struct 的情况
若根据虚拟地址既找不到 page 结构体,也找不到对应的 vm_area_struct,这通常意味着该虚拟地址对应的内存区域尚未分配或初始化。对于文件映射场景,需要通过 inode 找到文件在磁盘中的位置。从磁盘读取相应的数据,为其分配一个新的 vm_area_struct 来描述该虚拟内存区域,同时更新 address_space 结构体,将新读取的数据对应的 page 结构体添加到 address_space 的 xarray 中。对于匿名页场景,会直接分配一个新的物理页,创建对应的 page 结构体,并将其与当前的虚拟地址关联起来。同时,创建一个新的 vm_area_struct 来管理该虚拟内存区域,更新相关的内存管理数据结构。
最后,无论是文件映射还是匿名页场景,都需要更新页表。通过逐层解析虚拟地址对应的页目录结构,找到最后一级页表项(PTE)的位置,将新分配或读取的物理页帧号(pfn)与相应的标志位组合后填充到该 PTE 中,从而建立起虚拟地址到物理地址的映射关系。这样,下次访问该虚拟地址时,MMU 就能通过页表找到对应的物理地址。
1. 页表项存在时的处理
步骤:
- MMU 查找页表:用户访问虚拟地址时,MMU 通过多级页表(PGD→P4D→PUD→PMD→PTE)查找对应的页表项(PTE)。
具体流程见:四级页表与五级页表_level 5 page table-CSDN博客
检查 PTE 存在位 :若 PTE 存在且有效(
_PAGE_PRESENT
标志置位),直接获取物理页帧号(PFN),转换为物理地址完成访问。无需内核介入:整个过程由硬件完成,无缺页异常触发。
示例 :
进程 A 多次读取同一文件映射区域,首次访问触发缺页异常并加载页到内存,后续访问直接通过有效 PTE 获取物理地址。
2. 页表项不存在但物理页已存在(触发缺页异常)
2.1 文件映射场景
步骤:
查找 VMA :通过虚拟地址在进程的
mm_struct
红黑树中找到对应的vm_area_struct
(VMA)。计算页索引:
cpppage_offset = vaddr - vma->vm_start; pgoff = page_offset >> PAGE_SHIFT; // 计算文件内的页索引
定位文件与页缓存:
通过
vma->vm_file
获取文件对象struct file
。通过
file->f_inode->i_mapping
获取文件的address_space
。查找缓存页:
cppstruct page *page = xa_load(&mapping->i_pages, pgoff); // 从 xarray 中查找页
填充 PTE:
若页存在,获取其 PFN,根据 VMA 权限(
vma->vm_flags
)设置 PTE 标志(如VM_READ
→PTE_RDONLY
)。更新 PTE,建立虚拟地址到物理页的映射。
示例 :
进程 B 映射同一文件的不同区域,首次访问某偏移时,内核通过
address_space
的 xarray 查找缓存页,若存在则直接映射。2.2 匿名页场景
步骤:
检查交换状态:
若页被换出(
PageSwapCache(page)
为真),根据swap_info_struct
从交换分区读取数据到新物理页。更新页的
mapping
指向swapper_space
(交换空间的address_space
)。未换出页的处理:
通过反向映射(
anon_vma
)找到所有共享该页的 VMA。计算虚拟地址对应的页偏移,定位物理页。
填充 PTE:
- 设置 PFN 和权限(如
VM_WRITE
→PTE_WRITE
),更新 PTE。示例 :
进程 C 调用
fork()
,子进程写时复制(COW)父进程的堆内存:
父进程匿名页的 PTE 变为只读。
子进程写入时触发缺页异常,内核复制新页,更新子进程 PTE 为可写。
3. 找不到 page 或 VMA 的情况
3.1 文件映射场景(新区域访问)
步骤:
扩展 VMA:若虚拟地址超出现有 VMA 范围,内核可能扩展或创建新 VMA。
读取磁盘数据:
通过
inode
定位磁盘数据,分配物理页并读取数据。将新页加入
address_space->i_pages
。更新页表:填充 PTE,建立映射。
示例 :
进程 D 通过
mmap
扩展文件映射区域,访问新范围时触发缺页,内核从磁盘读取数据并创建新 VMA。3.2 匿名页场景(新内存分配)
步骤:
分配物理页 :调用
alloc_page()
分配新物理页,初始化page
结构体。关联 VMA:
创建新 VMA(如堆扩展),设置
anon_vma
管理反向映射。将页的
mapping
指向anon_vma
。填充 PTE:设置 PFN 和权限,更新页表。
示例 :
进程 E 调用
malloc()
分配内存,首次访问时触发缺页,内核分配匿名页并关联到堆 VMA。
4. 页表更新与后续访问
步骤:
逐级填充页表:从 PGD 到 PTE,确保各级页目录项存在。
设置 PTE 标志 :根据 VMA 权限(如
VM_EXEC
→PTE_X
)设置访问控制。TLB 刷新 :通过
flush_tlb_page()
刷新 TLB 缓存,确保新映射生效。示例 :
进程 F 修改文件映射页的权限(如
mprotect(PROT_WRITE)
),内核更新 PTE 的写标志并刷新 TLB。
关键数据结构关系
文件映射 :
vma→vm_file→f_inode→i_mapping→i_pages
(xarray 管理页缓存)。匿名映射 :
vma→anon_vma
管理反向映射,page→mapping
指向anon_vma
或交换空间。