代码实现
static u32 user_va2_pa(struct mm_struct *mm, u32 address) {
pgd_t *pgd;
pmd_t *pmd;
pte_t *ptep, pte;
if(!mm)
{
return 0;
}
pgd = pgd_offset(mm, address);
if (pgd && !(pgd_none(*pgd) || pgd_bad(*pgd))) {
ptep = pte_offset_map((pmd_t *)pgd, address);
if (ptep) {
pte = *ptep;
if (pte_present(pte))
{
return pte & PAGE_MASK;
}
}
}
}
return 0;
}
代码解析
在内存管理单元控制下,主要分用户态映射和内核态映射。内核态虚拟地址从0xC0000000开始,且为线性映射,虚拟地址=物理地址+映射偏移。
需要注意的是,页表存放的值为物理内存地址,可以被内核态引用,也会被硬件分页地址计算引用,区别在于内核态引用时,需要转换为虚拟地址访问。
内核页表有四层,包括pgd->pud->pmd->pte。
arm-linux使用两级页表:pgd->pmd->page,pgd指向一级页目录项。
具体公式如下:
pdg首地址 = mm->pgd,是一级页表(页目录,page dir)虚拟地址,保存在线程的mm结构内
pgd变量 = pgd首地址 + 虚拟地址高12位,pgd变量指向的内存保存二级页表位置。
pmd变量 = pud变量 = pgd变量
static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}
#define __va(x) ((void *)__phys_to_virt((phys_addr_t)(x)))
static inline pte_t *pmd_page_vaddr(pmd_t pmd)
{
return __va(pmd_val(pmd) & PHYS_MASK & (s32)PAGE_MASK);
}
pte_offset_map(实际为pte_offset_kernel),用于从pmd(pgd)继续索引二级页表,当对pmd变量进行指针操作时,需要注意获取的内存值是二级页表的物理地址,因此要启用如下的宏pmd_page_vaddr将地址转换回虚拟地址后,才能正确访问。
ptep是二级页表项指针,这里区分下术语:
1. 页表,指整个表内存,也可以认为是页表首地址
2. 页表项,项这个字指页表内一个项,包含两个要素:页表项所在地址和页表项保存的下一级页面地址
ptep=转换虚拟地址(*pmd) + 目标虚拟地址[20:12]位偏移,ptep(页表项指针)就是pte(页表项)也就是二级页表项的地址
pte=*ptep,pte就是页表项内容,保存指向页面具体的物理地址。至此,用户地址向物理地址的寻址就完成了。如果需要在内核态下访问pte指向的页,还需要通过__va宏,将pte保存的页物理地址转换为软件可访问的虚拟地址。
页表参考文献
Linux的线程内存空间参考:Linux 内核线程及普通进程总结