X86分页机制
x86的分页单元支持两种分页模式:常规分页与扩展分页。
常规分页采用两级结构,固定页大小为4KB。线性地址被划分为三个字段:
- 页目录索引(最高10位)
- 页表索引(中间10位)
- 页内偏移(最低12位)
扩展分页启用时采用单级结构,页大小为4MB。线性地址被划分为两个字段:
- 页目录索引(最高10位)
- 页内偏移(最低22位)
页表结构
常规分页与扩展分页可混合使用。页目录条目中有一个标志位用于指定当前使用哪种分页模式。专用寄存器CR3指向页目录的基地址,页目录条目则指向页表的基地址。
页目录和页表均包含1024个条目,每个条目占4字节。
所有页表均存储于物理内存中,页表地址均为物理地址。
页表条目字段:
- 存在位(Present/Absent)
- 页框号(PFN):物理地址的最高20位
- 访问位(Accessed,硬件不自动更新,供操作系统维护使用)
- 脏位(Dirty,硬件不自动更新,供操作系统维护使用)
- 访问权限(读/写)
- 特权级(用户/管理员)
- 页大小标志(仅页目录条目有效,置位时启用扩展分页)
- 缓存控制位(PCD-禁用页缓存,PWT-直写模式)
Linux分页实现
Linux采用四级分页模型以支持64位架构。下图展示了如何通过虚拟地址的各字段索引页表并计算物理地址:
Linux提供统一的API用于创建和遍历页表。内核与进程地址空间的创建及修改均通过通用代码实现,这些代码依赖宏和函数将通用操作适配到不同架构。
虚拟地址转物理地址示例(使用Linux页表API)
struct page *page;
pgd_t pgd;
pmd_t pmd;
pud_t pud;
pte_t pte;
void *laddr, *paddr;
pgd = pgd_offset(mm, vaddr); // 获取页全局目录项
pud = pud_offset(pgd, vaddr); // 获取页上级目录项
pmd = pmd_offset(pud, vaddr); // 获取页中间目录项
pte = pte_offset(pmd, vaddr); // 获取页表项
page = pte_page(pte); // 获取对应物理页结构
laddr = page_address(page); // 获取逻辑地址
paddr = virt_to_phys(laddr); // 转换为物理地址 为兼容分页层级少于4级的架构(如x86 32位),部分宏/函数会被定义为空操作:
static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address) {
return (pud_t *)pgd; // 直接返回页全局目录项地址
}
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address) {
return (pmd_t *)pud; // 直接返回页上级目录项地址
}
转译后备缓冲器(快表,TLB)
使用虚拟内存时,由于页表的多级结构,地址转换可能需要额外1次(x86扩展分页)、2次(x86常规分页)或3次(x86 64位)内存访问。
快表(TLB)作为专用缓存用于加速虚拟地址到物理地址的转换,其特性如下:
- 缓存分页信息(页框号、权限、特权级)
- 基于内容可寻址存储器(CAM)实现
- 容量极小(64-128条目)
- 速度极快(并行搜索实现单周期访问)
- CPU通常包含独立指令TLB(i-TLB)与数据TLB(d-TLB)
- TLB未命中惩罚:可达数百时钟周期
与其他缓存类似,需注意TLB的一致性问题。例如:
- 修改页表条目使其指向新的物理地址时,必须使旧TLB条目失效,否则MMU仍会使用旧的物理地址转换。
x86平台支持两种TLB失效操作:
- 单地址失效:
mov $addr, %eax
invlpg (%eax) ; 强制刷新指定虚拟地址的TLB条目
- 全局失效:
mov %cr3, %eax
mov %eax, %cr3 ; 通过重载CR3寄存器刷新全部TLB