内核杂谈——页表项存放的是物理地址还是虚拟地址?

目录

L0 L1 L2 表项

L3 表项

总结

pgd_t

不只是物理地址

谈谈对映射的理解

思考


当你不去细细读代码的话,这个问题可能会困扰着你。我们以ARM64四级页表为例,谈谈页表项里藏得是什么。本文讨论的是内核线性映射过程时建立的临时页表,涉及到早期内核页表的建立不做分析,后面有机会分析吧。

L0 L1 L2 表项

从__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的操作一样了,先判断是不是空,如果为空就填充,然后映射下一级。

L3 表项

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_t类型,然后抽离出成员pgd。

#include <stdio.h>

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;
}

ARM手册页表表述

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 用户空间的页表项里存放的是什么?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值