目录
L0 L1 L2 表项
L3 表项
总结
pgd_t
不只是物理地址
谈谈对映射的理解
思考
当你不去细细读代码的话,这个问题可能会困扰着你。我们以ARM64四级页表为例,谈谈页表项里藏得是什么。本文讨论的是内核线性映射过程时建立的临时页表,涉及到早期内核页表的建立不做分析,后面有机会分析吧。
从__create_pgd_mapping函数开始看:
static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys,
unsigned long virt, phys_addr_t size,
pgprot_t prot,
phys_addr_t (*pgtable_alloc)(void),
int flags)
{
unsigned long addr, length, end, next;
//获取以virt在pgdir中的表项。
pgd_t *pgdp = pgd_offset_raw(pgdir, virt);
phys &= PAGE_MASK;
addr = virt & PAGE_MASK;
length = PAGE_ALIGN(size + (virt & ~PAGE_MASK));
end = addr + length;
do {
next = pgd_addr_end(addr, end);
//这一步会设计获取表项里的内容
alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc,
flags);
phys += next - addr;
} while (pgdp++, addr = next, addr != end);
}
之前杂谈过页表项的坑比问题,明确了页表可以当成一个数组去看。
pgd_t *pgdp = pgd_offset_raw(pgdir, virt) 可以看成 pgdir[index(virt)],那么 pgdp = &pgdir[index(virt)]。
下面看 alloc_init_pud ,先看上半部分内容。
static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot,
phys_addr_t (*pgtable_alloc)(void),
int flags)
{
unsigned long next;
pud_t *pudp;
//获取表项里的内容
pgd_t pgd = READ_ONCE(*pgdp);
//表项为空,填充内容
if (pgd_none(pgd)) {
phys_addr_t pud_phys;
BUG_ON(!pgtable_alloc);
pud_phys = pgtable_alloc();
__pgd_populate(pgdp, pud_phys, PUD_TYPE_TABLE);
pgd = READ_ONCE(*pgdp);
}
}
接着之前的传递参数,pgd_t pgd = READ_ONCE(*pgdp) 可以看成 pgd = pgdir[index(virt)],如果pgd为0,会从memblock中获取一个page大小的物理内存,然后把这个物理内存的起始物理地址与bm_pte关联。pgtable_alloc()为函数指针,实际调用early_pgtable_alloc。获取物理内存后,使用__pgd_populate,将这个物理内存的起始物理地址填到页表项中,即 pgdir[index(virt)] = pud_phys。实际上会把这个物理地址转换成pdt_t 类型,但不影响理解。
所以页目录表的表项里存放的是物理地址,注意这个物理地址和映射的物理地址不是一个概念,是下级页表的物理地址。
讲到这其实 pgdir[index(virt)] = pud_phys 这个说法是不严谨的,也是因为背后的真面目导致我们看不到物理地址。看如下函数
static inline void __pgd_populate(pgd_t *pgdp, phys_addr_t pudp, pgdval_t prot)
{
set_pgd(pgdp, __pgd(__phys_to_pgd_val(pudp) | prot));
}
#define __phys_to_pgd_val(phys) __phys_to_pte_val(phys)
#define __phys_to_pte_val(phys) (phys)
参数pudp定义成物理地址,里面又对其做了转化 __phys_to_pgd_val(pudp) ,在没有CONFIG_ARM64_PA_BITS_52,也就是定义的物理地址不是52位的情况下__phys_to_pgd_val(pudp) == pudp,依旧是物理地址。但是学问在 __pgd(__phys_to_pgd_val(pudp) | prot) 操作,将物理地址和属性结合在一块,重新定义成 pgd_t 类型,实现 phys_addr_t 到 pgd_t 转换,所以物理地址被盖上了一层纱,但要认清他还是物理地址,使用的时候需要揭开这个纱。
typedef u64 pgdval_t;
typedef struct { pgdval_t pgd; } pgd_t;
#define pgd_val(x) ((x).pgd)
#define __pgd(x) ((pgd_t) { (x) } )
这个定义很神奇,文末做了一个简单分析。把一个数值强转成pgd_t类型,然后取里面的pgd成员。强转在内核中很常用,比如在ARM64开启vmemmap定义后,存放page的全局数组是由vmemmap这个虚拟地址强转成struct page来使用的
继续往下
static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot,
phys_addr_t (*pgtable_alloc)(void),
int flags)
{
...
pudp = pud_set_fixmap_offset(pgdp, addr);
do {
next = pud_addr_end(addr, end);
if (use_1G_block(addr, next, phys) &&
(flags & NO_BLOCK_MAPPINGS) == 0) {
pud_set_huge(pudp, phys, prot);
} else {
alloc_init_cont_pmd(pudp, addr, next, phys, prot,
pgtable_alloc, flags);
}
phys += next - addr;
} while (pudp++, addr = next, addr != end);
...
}
看 pudp = pud_set_fixmap_offset(pgdp, addr) ,主要搞清楚 如何 使用 pgdp 来获取pudp。pud_set_fixmap_offset 由两部分组成
#define pud_set_fixmap_offset(pgd, addr) pud_set_fixmap(pud_offset_phys(pgd, addr))
第一部分 fixmap
#define pud_set_fixmap(addr) ((pud_t *)set_fixmap_offset(FIX_PUD, addr))
第二部分 获取pgd的内容,也就是pud的物理地址
pud_offset_phys(pgd, addr)
第二部分有很多路需要绕,一步步展开后
#define pud_offset_phys(pgd, addr)
(__pte_to_phys(__pte(pgd_val(*(pgd)))) + pud_index(addr) * sizeof(pud_t))
#define __pte_to_phys(pte) (pte_val(pte) & PTE_ADDR_MASK)
#define PTE_ADDR_MASK PTE_ADDR_LOW
#define PTE_ADDR_LOW (((_AT(pteval_t, 1) << (48 - PAGE_SHIFT)) - 1) << PAGE_SHIFT) //mask低12位为0
首先获取pgd里面的内容,然后转成pte_t类型,再通过__pte_to_phys操作获取物理地址。为什么这么做,前面说过了,物理地址被盖上了一层纱,需要揭开面纱才能见到真正的物理地址。根据页表转化流程低12位是和物理地址没关系的,来自虚拟地址的offset,或者某些flag。
后面的alloc_init_cont_pmd 操作就和pgd的操作一样了,先判断是不是空,如果为空就填充,然后映射下一级。
static void init_pte(pmd_t *pmdp, unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot)
{
pte_t *ptep;
ptep = pte_set_fixmap_offset(pmdp, addr);
do {
set_pte(ptep, pfn_pte(__phys_to_pfn(phys), prot));
phys += PAGE_SIZE;
} while (ptep++, addr += PAGE_SIZE, addr != end);
}
四级表项区别于前三级表项的地方在set_pte函数的第二个参数,直接使用将要映射的物理地址。可以看到,pte里存放的只是物理页号+属性。所以默认一个pte囊括4KB的范围。至于具体的物理地址,在做地址翻译的时候,将虚拟地址的后12位作为目标物理页号开始的偏移,从而取得具体的物理地址。
网上的地址转化示意图只是作为地址翻译去理解,如果作为地址建立过程去理解就翻车了。
pgd表项存放的是下级页表pud的物理地址(memblock获取)和属性,pgd表项的地址是要建立映射的虚拟地址在pgdir中的偏移。
pud表项存放的是下级页表pmd的物理地址(memblock获取)和属性,pud表项的地址是pud的物理地址(pgd表项里存放的)映射到bm_pud的虚拟地址。
pmd表项存放的是下级页表pte的物理地址(memblock获取)和属性,pmd表项的地址是pmd的物理地址(pud表项里存放的)映射到bm_pmd的虚拟地址。
pte表项存放的是要映射的物理地址(ddr地址)和属性,ptd表项的地址是ptd的物理地址(pmd表项里存放的)映射到bm_pte的虚拟地址。
各个表项是虚拟地址,利用fixmap暂时性使用的(p*d_set_fixmap)。fixmap会将FIX_P*D地址放在bm_p*d数组中,具体参考early_fixmap_init和p*d_set_fixmap,这里不细说。
其实可以发现,物理地址都是隐藏在参数中,直观给我们的都是虚拟地址,所以我们要抛弃物理地址的思维看os,从单片机的思维中走出来。
直接拷贝内核代码,编写一个程序来理解。体会一下把一个数值强转成pgd_t类型,然后抽离出成员pgd。
#include
typedef unsigned long pteval_t;
typedef struct { pteval_t pte; } pte_t;
#define pte_val(x) ((x).pte)
#define __pte(x) ((pte_t) { (x) } )
typedef unsigned long pgdval_t;
typedef struct { pgdval_t pgd; } pgd_t;
#define pgd_val(x) ((x).pgd)
#define __pgd(x) ((pgd_t) { (x) } )
#define PA 0x13ffff000
#define PROT 3
#define PTE_ADDR_MASK (((1 << 36) - 1) << 12)
int main()
{
pgd_t *pgdp;
pte_t pte;
pgd_t pgd = __pgd(PA|PROT);
pte = __pte(pgd_val(pgd)); //模拟set_pte
int pa = (pte_val(pte) & PTE_ADDR_MASK); //抽离出PA
printf("pa %x\n", pa);
return 0;
}
ARMv8架构支持的最大物理地址宽度为48位,页表本身是unsigned long类型占64位。由于物理地址按页管理,所以页表项里的物理地址是一个个页号。
由此可以得出,如果按4K为单位,那么页表项中bit[47:12]存放的是物理页号,也就是物理地址,解析物理地址的时候就是解析bit[47:12],bit[11:0]沿用虚拟地址,拼凑出某个物理地址。
那页表项中其余bit[63:48]和bit[11:0]和物理地址无关,但也不能浪费,用作属性和标志位管理。具体可见arch/arm64/include/asm/pgtable-hwdef.h中定义了一些页表项的描述符,arch/arm64/include/asm/pgtable-prot.h中定义了一些软件标志位属性,具体存放在哪个bit上这里不做阐述。
附:参考ARM Architecture Reference Manual ARMv8, for ARMv8-A architecture profile
L0 ~ 3 表项 页表对应Table这行
L3 表项 不同大小page,内容不同
详细描述可以去阅读手册。
玩过单片机的同学都知道,访问某个外设就直接给出外设地址然后像指针一样去操作,点灯就完成了。这里操作的是物理地址。
在linux内核就不一样了,加了MMU,cpu看不到物理地址了,只能看到虚拟地址同时只能操作虚拟地址。好处就是让cpu感觉自己的空间很大很大,直接脱离了物理内存的视野。但是虚拟地址终归是虚的,操作这个地址是没有用的,有用的是物理地址,所以要把这些虚拟地址和物理地址建立关系,这样cpu操作虚拟地址就像在操作物理地址一样。内核中把映射好的地址以page为单位进行管理。程序是泡在内存中的,映射了内存就可以像点灯一样去操作内存了。
当然对于其他外设模块(非ddr),可以直接使用ioremap映射到内核空间。
用户空间运行程序也是要物理地址,如果泡在未映射物理地址的虚拟空间里,这时候是以缺页方式去获取一段物理空间。默认用户空间程序是以缺页方式建立。
当然内核实现remap_pfn_range,这类通常是mmap,用户空间可以直接操作物理地址。
当我们在写代码的时候malloc了一个空间,然后操作这个空间的时候说不定背后就跑了缺页异常代码去获取page了呢。
1 页表项里面存放的是物理地址,那页表项本身存放的是什么?
2 用户空间的页表项里存放的是什么?