1. 引言
内核有两种地址转换方式,一种是线性地址转换,一种是基于pgd/pud/pmd/pte的页表转换.
理解这两种页表的转换可以让我们知道虚拟地址和的物理地址之间的关系,方便理解各种用户态工具获取内核数据的原理;
同时也方便理解为什么地址转换为什么需要缓存,因为它确实需要经验一系列计算才能得到我们想要的物理地址;
最后这种地址转换就是mmu的一部分,了解它也助于我们了解mmu.
注意:本文基于arm64计解,arm32有细节上的区别,x86体系有算法上的差别,但思想类似.
2.线性地址
64位机器下,内核的虚拟地址是48位的,内核的虚拟地址分为线性地址和页表地址.
线性地址分为两种,一种是编译时就已经定好了的全局变量的地址,它们的虑拟地址由内核的虚拟偏移地址决定,这个虚拟偏移地址就是kimage_voffset. 如非指针类型全局swapper_pg_dir,mem_section就是这种类型的地址,基本上可以认为只要声明的全局变量的地址都是这种类型的线性地址,不同的是有的初始化了在data区,有的没有初始化在bss区.为了后面方便描述,称为A类线性地址.
另一种线线地址是运行时申请的,此时页表还没有建立,slab也没有初始化好,此时需要调用memblock_virt_alloc_internal函数到memblock去找一段可用的内存. 为了后面方便的描述,称为B类线性地址.
这两种地址虽然都是线性偏移方式计算虚拟地址和物理地址之间的转换,但具体的公式参数不一样,所以需要有一种方式知道当给出虚拟地址时需要能够很方便且快速的得知是哪种类型的地址,由于48位地址的范围可达到256TB, bss/data类型全局变量和代码段都已经存在vmlinux这种elf文件中,它们不可能达到256TB,不然linux vmlinux都这么大了,整个系统不可能加载到内存,即使加载到了内存没有内存空间给其它程序运行了.
所以内核用最高位48位来区分,如果第48位是0那么就表示bss/data类型的全局变量的虚拟地址,实际上代码段函数地址第48位也是0; 如果第48位是1,那么就是动态申请的方式的内存的虚拟地址了.
注意这儿说的48位是虚拟地址,物理地址也可以从0开始一直到48位, 只是全局变量地址的物理地址很小,动态申请的内存地址可以更大.更大的不同是映射成虚拟地址的计算公式的不同.
2.1 swapper_pg_ir全局变量虚拟地址
swapper_pg_dir是保存pgd地址的地方,是一个重要的全局变量,看看它的地址
首先看看它在vmlinux中的地址:
root@uos-PC:/home/uos# readelf -a vmlinux|grep swapper_pg_dir
127146: ffff0000095ed000 0 NOTYPE GLOBAL DEFAULT 26 swapper_pg_dir
再看运行时的地址
cat /proc/kcore |more
下翻可以看到它的地址:
SYMBOL(swapper_pg_dir)=ffff0000095ed000
也可以gdb ./vmlinux /proc/kcore, 然后基于gdb来查看
(gdb) p &swapper_pg_dir
$20 = (pgd_t (*)[512]) 0xffff0000095ed000
结论:
swapper_pg_dir这种全局变量的地址就是vmlinux符号表中的地址,当然这还有一个前提条件就是内核的偏移为0, KERNELOFFSET=0
2.2 mem_section全局变量虚拟地址
mem_section是一个二级指针,利于swapper_pg_dir中的pgd内容,从这个变量出来可以得到所整个系统page的虑拟地址,从而进一步得到它们的物理地址.
这个变量很特殊,因为首先它是一个二级指针,保存这个二级指针的地址是A类线性地址,即三级指针地址.它本身及它里面的值也分别是二级地址和一级地址,由于是动态申请的因此都是B类线性地址.
首先看看它在vmlinux中的地址,这个和/proc/kallsyms中看到的一样
readelf -a |grep mem_section
108904: ffff00000958cc40 8 OBJECT GLOBAL DEFAULT 26 mem_section
通过gdb查看确认
(gdb) p &mem_section
$18 = (struct mem_section ***) 0xffff00000958cc40 <mem_section>
vmlinux中看到的地址是二级指针的地址,也就是将来要保存mem_section的地方
/proc/kcore中看到的是二级地址
SYMBOL(mem_section)=ffff8483ffe3c180
2.3 动态申请的B类线性地址
首先这种动态申请的内存不是基于slab也是不用page_allocs函数申请的,因为这种内存申请非常早,此时slab还不能使用.这时调用memblock_virt_alloc_internal函数申请.这函数的申请原理就是基于memblock中记录的物理内存信息去找一段满足条件的物理内存,然后调用phys_to_virt转换成虚拟地址返回给调用者.调用的代码如下:
ptr = phys_to_virt(alloc);
2.4 A类线性地址的转换
由于这种全局变量类型的地址就是elf文件的一部分,因此它的地址就是在内核文件本身中的地址,内核文件本身的地址又由它加载到的物理内存决定.
当转换成虚拟地址时只需要加载内核自身给的内核虚拟地址偏移量就可以了,这个偏移量保存到了kimage_voffset中.
因此最后的A类线性地址的虚拟地址计算公式是:
#define __phys_to_kimg(x) ((unsigned long)((x) + kimage_voffset))
kimage_voffset的值可以在/proc/kcore中查到,如下:
NUMBER(kimage_voffset)=0xffff000008000000
举例:
swapper_pg_dir的虚拟地地址是0xffff0000095ed000, 那么它的物理地址就是:
p /x 0xffff0000095ed000 - 0xffff000008000000
$40 = 0x15ed000
2.5 B类线性地址的转换
前面说过这种地址的转换函数是phys_to_virt, 它直接调用的是__phys_to_virt
__phys_to_virt的定义如下:
#define __phys_to_virt(x) ((unsigned long)((x) - PHYS_OFFSET) | PAGE_OFFSET)
这儿 PHYS_OFFSET 就是内核的物理偏移地址,是否为0取决于偏移内核时是否让这个地址随机.我编译时去掉了随机,因此基值为0:
NUMBER(PHYS_OFFSET)=0x0
PAGE_OFFSET的定义如下:
#define PAGE_OFFSET (UL(0xffffffffffffffff) - (UL(1) << (VA_BITS - 1)) + 1)
对于64位系统,VA_BITS的值通常为48. 因些 PAGE_OFFSET 的值最终就是 0xffff800000000000,第48位,也就是8对应的这位,刚好是1, 因此B类线性地址最终保证了第48位是1, 很好地区分和A类线性地址的区分.
在内核中这种地址被称为lm地址.
2.6 内核中实现统上的虚拟地址转物理地址函数
#define __virt_to_phys_nodebug(x) ({
phys_addr_t __x = (phys_addr_t)(x);
__is_lm_address(__x) ? __lm_to_phys(__x) :
__kimg_to_phys(__x);
})
上面的函数完成将虚拟地址转换成物理地址的功能,由于虚拟地址分为两类,因此先调用__is_lm_address确定虚拟地址类型,如果第48位为0,则为上面说的A类,否则为B类.
对于A类地址调用__kimg_to_phys, 对于B类地址调用__lm_to_phys
__is_lm_address的实现如下:
#define __is_lm_address(addr) (!!((addr) & BIT(VA_BITS - 1)))
#define VA_BITS (48)
#define BIT(nr) (1UL << (nr))
可以看到__is_lm_address就是测试第48位是否为1, 这儿两次取非很有技巧,这样得到的值不是0就是1.
__kimg_to_phys完成A类线性地址转换,就是减去内核虚拟地址偏移值,在我们的机器上,它是0xffff000008000000
#define __kimg_to_phys(addr) ((addr) - kimage_voffset)
__lm_to_phys完成B类性线地址的转换,PAGE_OFFSET 的值是 0xffff800000000000, 取反之后就是0x7fffffffffff.
所以lm地址方式转换成物理地址就是取低47位,同时加上物理地址的偏移值,如物理地址也许偏移了0x80000
#define __lm_to_phys(addr) (((addr) & ~PAGE_OFFSET) + PHYS_OFFSET)
总结:
虽然说物理地址是48位的,但是最高位第48位是标志位,用它来区分是lm地址, 即本文中说的B类线性地址,不是全局变量类型的A类线性地址.真正有效的地址只有47位,所以支持的内存大小最大为2^47=128TB
A类线性性地址计算公式(48位物理地址情况):
phys = virt - kimage_voffset
反之: virt = phys + kimage_voffset
kimage_voffset的值通常为0xffff000008000000, 此时的虚拟地址是低47位的物理地址多加了一个0x8000000
B类线性地址计算公式:
phys = (virt & 0x7fffffffffff) + PHYS_OFFSET
反之: virt = (phys - PHYS_OFFSET) | 0xffff800000000000
如果PHYS_OFFSET=0, 此时虚拟地址的低47位就是物理地址
3. 基于页表表映射的地址转换
线性地址方式下物理地址和虑拟地址之间的转换还是很快,因为此时只是一个简单的加减和与或运算,稍微复杂一点的概念就是线性地址分为了两类.
对于基于pgd表映射的地址转换过程要繁琐很多,而且为了优化转换性能,存在三种不现情况的映射,分别是基于pud表获取物理地址,基于pmd表获取物理地址,基于pte表获取物理地址.
无论哪种方式都需要先从pgd表出发,即先查pgd表一次,所以如果是第一类,那么再查一次pud表就可以获取到物理地址了;
如果是基于pmd表获取物理地址,还需要查pmd表,总共需要查三次表;
如果是基于pte表获取物理地址,还需要查pte表,总共需要查四次表.
所以基于pte表获取物理地址是最慢的,基于pud表获取物理地址是最快的. 但基于pte表的情况是最多的,看看下面的地址获取细节应该能体会其中的原因.
下面代码包含了上面三种类型的地址转换方式,下面详细分析
if (SYMBOL(swapper_pg_dir) == NOT_FOUND_SYMBOL) {
ERRMSG("Can't get the symbol of swapper_pg_dir.n");
return NOT_PADDR;
}
swapper_phys = __pa(SYMBOL(swapper_pg_dir));
pgda = pgd_offset(swapper_phys, vaddr);
if (!readmem(PADDR, (unsigned long long)pgda, &pgdv, sizeof(pgdv))) {
ERRMSG("Can't read pgdn");
return NOT_PADDR;
}
puda = pud_offset(pgda, &pgdv, vaddr);
if (!readmem(PADDR, (unsigned long long)puda, &pudv, sizeof(pudv))) {
ERRMSG("Can't read pudn");
return NOT_PADDR;
}
if ((pud_val(pudv) & PUD_TYPE_MASK) == PUD_TYPE_SECT) {
/* 1GB section for Page Table level = 4 and Page Size = 4KB */
paddr = (pud_val(pudv) & (PUD_MASK & PMD_SECTION_MASK))
+ (vaddr & (PUD_SIZE - 1));
return paddr;
}
pmda = pmd_offset(puda, &pudv, vaddr);
if (!readmem(PADDR, (unsigned long long)pmda, &pmdv, sizeof(pmdv))) {
ERRMSG("Can't read pmdn");
return NOT_PADDR;
}
pmdtype = pmd_val(pmdv) & PMD_TYPE_MASK;
switch (pmdtype) {
case PMD_TYPE_TABLE:
ptea = pte_offset(&pmdv, vaddr);
/* 64k page */
if (!readmem(PADDR, (unsigned long long)ptea, &ptev, sizeof(ptev))) {
ERRMSG("Can't read pten");
return NOT_PADDR;
}
if (!(pte_val(ptev) & PAGE_PRESENT)) {
ERRMSG("Can't get a valid pte.n");
return NOT_PADDR;
} else {
paddr = (PAGEBASE(pte_val(ptev)) & PHYS_MASK)
+ (vaddr & (PAGESIZE() - 1));
}
break;
case PMD_TYPE_SECT:
/* 512MB section for Page Table level = 3 and Page Size = 64KB*/
paddr = (pmd_val(pmdv) & (PMD_MASK & PMD_SECTION_MASK))
+ (vaddr & (PMD_SIZE - 1));
break;
}
return paddr;
3.1 先判断swapper_pg_dir是否存在
SYMBOL(swapper_pg_dir)表示获取swapper_pg_dir这个全局变量的地址,因为通过它能够得到pgd表的地址.如果这个变量获取不到,那么就没有办法转换了,直接退出.
SYMBOL(swapper_pg_dir) == NOT_FOUND_SYMBOL这种语句实现的就是这种判断,NOT_FOUND_SYMBOL的值为0
3.2 得到swapper_pg_dir的物理地址
实现代码如下:
swapper_phys = __virt_to_phys_nodebug(SYMBOL(swapper_pg_dir));
SYMBOL(swapper_pg_dir)返回的是它的虚拟地址,由于swapper_pg_dir是一个全局变量,因此它就是我们上面说的A类地址,此时__virt_to_phys_nodebug函数将虚拟地址减去内核虚拟地址偏移量kimage_voffset就可以了.
3.3 根据虚拟地址从swapper_pg_dir得到描述这个虚拟地址的pgd表
pgda = pgd_offset(swapper_phys, vaddr);
上面的宏展开后的代码如下:
pgda = ((pgd_t *)(swapper_phys) + (((vaddr) >> (((info->page_shift) - 3) * pgtable_level + 3)) & ((1 << (va_bits - (((info->page_shift) - 3) * pgtable_level + 3))) - 1)));
4K页面情况下, info->page_shift = 12, pgtable_level=4, 因为此时有四级页表pgd->pud->pmd->pte
上面的式了化简后就是:
pgda = ((pgd_t *)(swapper_phys) + ((vaddr >> 39) & 0x1ff));
((vaddr >> 39) & 0x1ff)中,vaddr首先右移39位,低39位不要了,然后与0x1ff相与,这时取的是vaddr的第40位到至第48位. 看来对虚拟地址而言,第48位不仅仅是标志,而是取pgd表索引值的一部分.
所以pgd表的地址就是相对于swapper_phys这个物理地址偏移一个值,这个偏移的值就是虚拟地址的第40位至第48位组成的值.
这个vaddr虚拟地址对应的pgd表的地址得到了,可以从这个地址中读pgd表的值,对应的代码是:
readmem(PADDR, (unsigned long long)pgda, &pgdv, sizeof(pgdv))
总结:pgd表的地址是虚拟48位地址中的高9位作为偏移量从swapper_phys这个基地址中取出来的
3.4 根据pgd表得到pud表的地址
puda = pud_offset(pgda, &pgdv, vaddr);
上面的代码展开后如下:
puda = ((pud_t *)((((*pgdv)).pgd) & ((1UL << 48) - 1) & (int32_t)(~((info->page_size) - 1))) + (((vaddr) >> get_pud_shift_arm64()) & ((1 << ((info->page_shift) - 3)) - 1)));
get_pud_shift_arm64返回30, 因为pgd占9位,pud占9位,还剩48-9-9=30.
info->page_size=4096
puda = ((pud_t *)((((*pgdv)).pgd) & 0xff ff ff ff ff ff & (int32_t)(~4095)) + ((vaddr >> 30) & 0x1ff));
先与xff ff ff ff ff ff 相与得到低48位地址,得到低48位地址,然后与(~4095)相与得去掉低12位,即将低12位变成0.
((vaddr >> 30) & 0x1ff)和虚拟地址相关,它将vaddr中的第31位至第39位的值取出来.
总结:
pud表的地址是由pgd表中内容的高36位与虚拟地址中的第二个9位组成的48位地址.这个的第二个9位就是虚拟地址的第31位至第39位
得到pud表的地址后就可以从这个地址中读出来里面的,这条语句是:
readmem(PADDR, (unsigned long long)puda, &pudv, sizeof(pudv))
3.5 如果是pud类型的地址,那么此时基于pud表计算出物理地址
是否是pug类型的地址的判断如下:
((pud_val(pudv) & PUD_TYPE_MASK) == PUD_TYPE_SECT)
展开后的代码如下:
((pudv.pgd.pgd & 3) == 1)
它表达的意思就是pud表中的值的末两位是1, 即是PUD_TYPE_SECT类型的section
如果是pud类型的地址,计算地址公式代码为:
paddr = (pud_val(pudv) & (PUD_MASK & PMD_SECTION_MASK)) + (vaddr & (PUD_SIZE - 1));
展开后如下:
paddr = (((((pudv).pgd).pgd)) & ((~((1UL << get_pud_shift_arm64()) - 1)) & ((1UL << 48) - 1)))
+ (vaddr & ((1UL << get_pud_shift_arm64()) - 1));
化简后如下:
paddr = (pudv.pgd.pgd & 0xff ff c0 00 00 00) + (vaddr & ((1UL << 30) - 1));
首先从pud表中取出高18位地址, 然后从虚拟地址中取出低30位地址.
总结:
pud类型的物理地址就是从pud表中取出高18位地址,从vaddr中取出低30位地址. 是否为pud类型地址由pud表值的末两位是否为1来决写
3.6 根据pud得到pmd地址
pmda = pmd_offset(puda, &pudv, vaddr);
展开后代码如下:
pmda = ((pmd_t *)((((((*pudv)).pgd).pgd)) & ((1UL << 48) - 1) & (int32_t)(~((info->page_size) - 1))) + (((vaddr) >> (((info->page_shift) - 3) * 2 + 3)) & ((1 << ((info->page_shift) - 3)) - 1)));
化简后如下:
pmda = ((pmd_t *)(*pudv.pgd.pgd & ((1UL << 48) - 1) & (int32_t)(~4095)) + ((vaddr >> 21) & 0x1ff));
pmd表的地址是由pud表中内容的低48位的高36位与虚拟地址中的第三个9位组成的48位地址.这个的第三个9位就是虚拟地址的第22位至第30位. 这个位也是pmd对应的位.
总结:
pmd表的地址是由于pud表中的低48位的高36位与虚拟地址中pmd表对应的那9位组成的,分析发现pgd对应第一个高9位,pud对应第二个高9位,pmd则对应第三个高9位.
得到pmd表地址后,获取值就比较简了,实现代码如下:
readmem(PADDR, (unsigned long long)pmda, &pmdv, sizeof(pmdv))
3.7 如果是pmd类型的地址,那么此时基于pmd表计算出物理地址
判断是否pmd类型的代码如下:
pmd_val(pmdv) & PMD_TYPE_MASK;
展开后如下:
(pmdv.pud.pgd.pgd & 3)
所以就是取出pmd表中的,看看末两位是否是3,即是否是PMD_TYPE_MASK
如果是pmd类型的地址,计算地址公式代码为:
paddr = (pmd_val(pmdv) & (PMD_MASK & PMD_SECTION_MASK))
+ (vaddr & (PMD_SIZE - 1));
展开后如下:
paddr = (((((((pmdv).pud).pgd).pgd))) & ((~((1UL << (((info->page_shift) - 3) * 2 + 3)) - 1)) & ((1UL << 48) - 1)))
+ (vaddr & ((1UL << (((info->page_shift) - 3) * 2 + 3)) - 1));
化简后如下:
paddr = (pmdv.pud.pgd.pgd & ((~((1UL << 21) - 1)) & ((1UL << 48) - 1)))
+ (vaddr & ((1UL << 21) - 1));
(pmdv.pud.pgd.pgd & ((~((1UL << 21) - 1)) & ((1UL << 48) - 1)))表示取48位中的高27位,同时将低21位置0.
最后从vaddr中取出低21位地址,一起组成48位物理地址
总结:
pmd类型的物理地址就是从pmd表中取出高27位地址,从vaddr中取出低21位地址. 是否为pmd类型地址由pmd表值的末两位是否为1来决定
3.8 如果是pte类型地址,获取pte表地址
获取pte表地址代码如下:
ptea = pte_offset(&pmdv, vaddr);
展开后代码如下:
ptea = ((pte_t*)((((((((*&pmdv)).pud).pgd).pgd))) & ((1UL << 48) - 1) & (int32_t)(~((info->page_size) - 1))) + (((vaddr) >> (info->page_shift)) & ((1 << ((info->page_shift) - 3)) - 1)));
化简后代码如下:
ptea = ((pte_t*)(*&pmdv.pud.pgd.pgd & ((1UL << 48) - 1) & (int32_t)(~4095)) + ((vaddr >> 12) & 0x1ff));
pte表的地址是由pmd表中内容的低48位的高36位与虚拟地址中的第4个9位组成的48位地址.这个的第四个9位就是虚拟地址的第13位至第21位. 这个位也是pte对应的位.
3.9 根据pte表计算出pte类型的物理地址
计算物理地址的公式为:
paddr = (PAGEBASE(pte_val(ptev)) & PHYS_MASK) + (vaddr & (PAGESIZE() - 1));
展开后的代码为:
paddr = ((((unsigned long long)(((ptev).pte))) & ~((info->page_size) - 1)) & ((1UL << 48) - 1))
+ (vaddr & ((info->page_size) - 1));
化简后为:
paddr = ((((unsigned long long)ptev.pte) & ~4095) & ((1UL << 48) - 1))
+ (vaddr & 4095);
((((unsigned long long)ptev.pte) & ~4095) & ((1UL << 48) - 1))表示出pte中的低48位,同时将低12位设置为0, 即从pte表中取出来低48位中的高36位.
然后再加上vaddr中的低12位,组成48位物理地址
总结:
pte类型的物理地址就是从pte表中取出低48位中的高36位地址,从vaddr中取出低12位地址. 是否为pte类型地址由pmd表值的末两位是否为3来决定
4.地址映射大统一
从上面的分析可以知道线性地址和页表地址计算上来看相差比较远,线性地址本质上经验一个加减运算就可以了,页表地址只则需要一级级来找,对于pte这种类型的物理地址,需要通过pgd找到pud, 然后通过pud找到pmd, 再通过pmd找到pte,最后才pte中取一部分物理地址,从虚拟地址中取一部分物理地址,它们一起构成最后的物理地址.
实际上基于最后的pte和虚拟地址构成物理地址的方式本质上也是一种线性运算.虚拟地址本身也是一种线性地址,对于48位的地址而言,如果它是pte类型的地址,那么从理论上总是可以被拆分成9|9|9|9|12, 第一个9位可以是指向pgd的索引,第二9位是指向pud的索引,第三个9位是指向pmd的索引,第四个9位是指向pte的索引,最后的12位则是物理地址的低12位,所以此时pte项中只需要保存物理地址的高36位就可以了.
理解页表的保存物理地址的这个本质以后就可以理解即使是上面说的A类线性地址也可以同时在页表中存储一份了,因为任何一个48位的虚拟地址都有一个对应的48位的物理地址.当48位的物理地址被转换成48位的虚拟地址后,任何一个48位的值都可以被按照上面的思想拆分到pgd,pud,pud,pte中去.就是说理论上来看是一个48位的值被拆分出来了,只是现在我们把这个48位的值理解成虚拟地址,因为内核不直接拆物理地址.内核的流程先根据物理地址得到虑拟地址,然后拆解虚拟地址到各级页表中去.
对于pud类型和pmd类型的虚拟地址同样适用.如果是pud类型的页表映射,即只要将虚拟地址拆分成pgd索引,pud索引,那么此时的拆解算法就是9|9|30, 第一个9位找到pgd索引,第二个9位找到pud索引,然后将物理地址的高18位存入pud项中,物理地址的低30位直接保存到虚拟地址本身,即虚拟地址和物理地址的低30位是一样的.
如果是pmd类型的页表映射,那么就将虚拟地址拆分成pgd索引,pud索引,pmd索引,此时的拆解算法是9|9|9|21, 第一个9位找到pgd索引,第二个9位找到pud索引,第三个9位找到pmd索引,pmd项中保存的是物理地址的高27位,物理地址的低21位直接保存到虚拟地址本身,虚拟地址和物理地址的低21位是一样的.
现在A类线性地址也同样基于上面的思想被拆解到了页表中保存,而且有可能是上面三种类型的任何一种.也就是说A类线性地址被申请出来后,同时在页表中也保存了一份,这样后面特别应用层就可以基于统一的页表方式来获取到它物理地址了,所以内核态不管是基于slab, page_alloc还是memblock_virt_alloc_internal方式申请的物理地址,当被转换成虚拟地址后都会在页表中保存一份自己的存在.这也是像makedumpfile这种工具即使面对mem_section这种A类线性地址可以基于基于页表得到其物理地址的根本原因.
5.总结
一共总结了5种类型的虚拟地址
a)A类线性地址,即内核说的非lm类型的地址,全局变量本身都是这种类型的地址,实际上内核中的函数也都是这种类型的地址.
phys = virt - kimage_voffset
virt = phys + kimage_voffset
kimage_voffset的值通常为0xffff000008000000
b)B类线性地址,内核说的lm类型的地址,它通常是内核早期运行时申请的内存,从本质来说是memblock_virt_alloc_internal函数到直接从memblock去找一段可用的物理内存.
phys = (virt & 0x7fffffffffff) + PHYS_OFFSET
virt = (phys - PHYS_OFFSET) | 0xffff800000000000
如果PHYS_OFFSET=0, 此时虚拟地址的低47位就是物理地址
c)pud类型的页表映射地址
pud类型的物理地址就是从pud表中取出高18位地址,从vaddr中取出低30位地址. 是否为pud类型地址由pud表值的末两位是否为1来决写
d)pmd类型的页表映射地址
pmd类型的物理地址就是从pmd表中取出高27位地址,从vaddr中取出低21位地址. 是否为pmd类型地址由pmd表值的末两位是否为1来决定
e)pte类型的页表映射地址
pte类型的物理地址就是从pte表中取出低48位中的高36位地址,从vaddr中取出低12位地址. 是否为pte类型地址由pmd表值的末两位是否为3来决定
f)4K页情况下,虚拟地址用48位不是巧合,从页表映射地址的pte类型地址可以看出,高36位刚好由4个组9组成,分别表示pgd, pud, pmd, pte四种类型表的空间,而最后的12位刚好一个页对齐的大小,当然这刚好是4K页的情况.
g)内核态从物理地址转换成虚拟地址分为5种类型,但是最终用户态通过虚拟地址得到物理地址时只需要基于大统一的页表映射方式来获取物理地址即可.页表映射方式其实也有三种类型,具体是哪种类由pud项,pmd项中的末两位的值来决定.所以这个转换函数完全可以实现成一个通用统一的函数.
h)原则上内核态的虚拟地址都不会暴露到用户态,因为用户态需要的内存都是内核基于缺页原理去申请一个个page,根据page得到物理页编号pfn, 然后将pfn转换物理地址,然后保存到pte,pmd, pud及pgd中.用户态看见的只是用户态的线性地址.
对于64位机器而方言,内核态虚拟地址高16位为0xffff, 用户态的地址高16位为全0.用户态地址访问如何基于缺页原理去申请内存,然后最终访问到内存需要在另一篇文中讲解,这儿需要理解的关键是用户态地址是如何和内核态的虚拟地址对应起来的.
本文只讲解了内核态的虚拟地址是如何和物理地址对应起来的.