fixmap addresses原理

作者:smcdef 发布于:2018-4-29 20:35 分类:内存管理

fixmap addresses原理.md

Fixmap Addresses原理

引言

fixmap是一段固定地址映射。kernel预留一段虚拟地址空间。因此虚拟地址是在编译的时候确定。fixmap可以用来做什么?kernel启动初期,由于此时的kernel已经运行在虚拟地址上。因此我们访问具体的物理地址是不行的,必须建立虚拟地址和物理地址的映射,然后通过虚拟地址访问才可以。例如:dtb中包含bootloader传递过来的内存信息,我们需要解析dtb,但是我们得到的是dtb的物理地址。因此访问之前必须创建映射,创建映射又需要内存。但是由于所有的内存管理子系统还没有ready。因此我们不能使用ioremap接口创建映射。为此kernel提出fixmap的解决方案。

注:文章代码分析基于linux-4.15,架构基于aarch64(ARM64)。

fixmap空间分配

fixmap虚拟地址空间又被平均分成两个部分permanent fixed addresses和temporary fixed addresses。permanent fixed addresses是永久映射,temporary fixed addresses是临时映射。永久映射是指在建立的映射关系在kernel阶段不会改变,仅供特定模块一直使用。临时映射就是模块使用前创建映射,使用后解除映射。

fixmap区域又被继续细分,分配给不同模块使用。kernel中定义枚举类型作为index,根据index可以计算该模拟在fixmap区域的虚拟地址。

enum fixed_addresses {
	FIX_HOLE,
#define FIX_FDT_SIZE		(MAX_FDT_SIZE + SZ_2M)
	FIX_FDT_END,
	FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,    /* 1 */
	FIX_EARLYCON_MEM_BASE,                                   /* 2 */
	FIX_TEXT_POKE0,
	__end_of_permanent_fixed_addresses,
#define NR_FIX_BTMAPS		(SZ_256K / PAGE_SIZE)
#define FIX_BTMAPS_SLOTS	7
#define TOTAL_FIX_BTMAPS	(NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
	FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
	FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,  /* 3 */
	FIX_PTE,
	FIX_PMD,
	FIX_PUD,
	FIX_PGD,
	__end_of_fixed_addresses
};
#define FIXADDR_SIZE	(__end_of_permanent_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_START	(FIXADDR_TOP - FIXADDR_SIZE) 
  1. FIX_FDT映射设备树使用。在ARM64架构,大小是4M。
  2. early console使用,大小1页。1页虚拟地址空间完全够了,毕竟串口操作相关寄存器没有几个。
  3. early_ioremap()接口使用,这部分属于动态映射。大小是7 × 256KB。

fixmap区域是地址空间范围从FIXADDR_START到FIXADDR_TOP结束。FIXADDR_SIZE是permanent fixed addresses区域的大小。我对这个地方很奇怪,为什么不包括temporary fixed addresses区域的大小呢?如果你知道可以告诉我。fixmap区域可以想象成一块内存以页为单位被平均分成__end_of_permanent_fixed_addresses块。而这些枚举值就是这块内存的index。因此虚拟地址可以根据index进行计算。计算方法如下。

#define __fix_to_virt(x)	(FIXADDR_TOP - ((x) << PAGE_SHIFT))
#define __virt_to_fix(x)	((FIXADDR_TOP - ((x) & PAGE_MASK)) >> PAGE_SHIFT) 

FIX_FDT是给dtb创建映射的区域。例如需要得到FDT的虚拟地址,即可以利用__fix_to_virt(FIX_FDT)得到虚拟地址。之所以FIX_FDT放在枚举的最前面,是因为我们针对dtb映射采用section mapping要求虚拟地址2M对齐。FIXADDR_TOP地址本身是2M对齐的,因此FIXADDR_TOP - (FIX_FDT << PAGE_SHIFT)可以很容易2M对齐。

fixmap初始化

fixmap初始化操作在early_fixmap_init函数中完成。主要是建立PGD/PUD/PMD页表。early_fixmap_init实现如下(简化部分代码逻辑)。

static pte_t bm_pte[PTRS_PER_PTE] __page_aligned_bss;               /* 1 */
static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_bss __maybe_unused;/* 1 */
static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_bss __maybe_unused;/* 1 */

void __init early_fixmap_init(void)
{
	pgd_t *pgd;
	pud_t *pud;
	pmd_t *pmd;
	unsigned long addr = FIXADDR_START;                             /* 2 */

	pgd = pgd_offset_k(addr);
	if (pgd_none(*pgd))
		__pgd_populate(pgd, __pa_symbol(bm_pud), PUD_TYPE_TABLE);   /* 2 */
	pud = fixmap_pud(addr);
	if (pud_none(*pud))
		__pud_populate(pud, __pa_symbol(bm_pmd), PMD_TYPE_TABLE);   /* 2 */
	pmd = fixmap_pmd(addr);
	__pmd_populate(pmd, __pa_symbol(bm_pte), PMD_TYPE_TABLE);       /* 2 */

	BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)
		     != (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));       /* 3 */

	if ((pmd != fixmap_pmd(fix_to_virt(FIX_BTMAP_BEGIN)))
			|| pmd != fixmap_pmd(fix_to_virt(FIX_BTMAP_END))) {     /* 3 */
		WARN_ON(1);
	}
} 
  1. 静态定义数组作为页表(PUD/PMD/PTE)使用。PGD页表使用swapper_pg_dir。
  2. 以FIXADDR_START为虚拟地址,建立页表映射关系。假设计算FDT的虚拟地址为addr = __fix_to_virt(FIX_FDT),必然addr是2M对齐的一个地址。由上面的分析可知,FIXADDR_START的值位于[addr - 2M, addr]之间。因此访问[addr - 2M, addr]之间的虚拟地址不需要再建立PUD/PMD页表项。只需要设置PTE页表对应的页表项即可。
  3. [FIX_BTMAP_BEGIN, FIX_BTMAP_END]区域给动态映射使用,保证该区域正好位于[addr - 2M, addr]之间,必须检查动态映射区域小于2M。定义的PTE页表数组其实是给动态映射使用的。当我们需要访问物理地址A,从[addr - 2M, addr]区域找到一个合适的虚拟地址B,填充B地址对应的PTE页表项即可访问。这也是early_ioremap的实现原理。

经过early_fixmap_init函数的探究,我们也可以得到一个结论:为了以page为单位进行映射,必须保证FIX_FDT和__end_of_fixed_addresses之间的虚拟地址空间必须小于2M。如果有超过2M的部分就要使用section mapping(因为只有一个PTE页表),以2M为单位映射。

early ioremap初始化

如果你希望kernel启动早期使用ioremap操作,其实是不行的。我们必须借助early ioremap接口。early ioremap是基于fixmap实现。初始化在early_ioremap_init完成。简化部分代码如下。

static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata;

void __init early_ioremap_setup(void)
{
	int i;

	for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
		slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
}

void __init early_ioremap_init(void)
{
	early_ioremap_setup();
} 

early ioremap利用slot管理映射,最多支持FIX_BTMAPS_SLOTS个映射,每个映射最大支持映射256KB。slot_virt数组存储每个slot的虚拟地址首地址。prev_map数组用来记录已经分配出去的虚拟地址,数组值为0代表没有分配。prev_size记录映射的size。

创建FDT映射

创建FDT映射的函数是__fixmap_remap_fdt,实现如下。

void *__init __fixmap_remap_fdt(phys_addr_t dt_phys,
		int *size, pgprot_t prot)
{
	const u64 dt_virt_base = __fix_to_virt(FIX_FDT);                 /* 1 */
	int offset;
	void *dt_virt;

	BUILD_BUG_ON(dt_virt_base % SZ_2M);                              /* 2 */

	BUILD_BUG_ON(__fix_to_virt(FIX_FDT_END) >> SWAPPER_TABLE_SHIFT !=
		     __fix_to_virt(FIX_BTMAP_BEGIN) >> SWAPPER_TABLE_SHIFT); /* 3 */

	offset = dt_phys % SWAPPER_BLOCK_SIZE;
	dt_virt = (void *)dt_virt_base + offset;

	create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE),  /* 4 */
			dt_virt_base, SWAPPER_BLOCK_SIZE, prot);

	*size = fdt_totalsize(dt_virt);
	if (offset + *size > SWAPPER_BLOCK_SIZE)
		create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE), dt_virt_base,
			   round_up(offset + *size, SWAPPER_BLOCK_SIZE), prot);  /* 5 */

	return dt_virt;
} 
  1. 根据__fix_to_virt计算fdt虚拟地址。
  2. 虚拟地址必须2M对齐。主要是为了section mapping。
  3. 在early_fixmap_init函数中,已经建立了PUD/PMD页表。为了让映射过程不用分配额外的PMD页表内存,这里必须保证FDT所在的虚拟地址范围落在early_fixmap_init函数建立的PMD页表范围内。
  4. 创建页表。这里映射2M空间。
  5. 如果dtb文件结尾地址超过上一个建立映射的地址范围,就必须紧接着再映射2M空间。

评论:

win.lin
2018-07-09 11:28
恩,我理解错了。这里面有几个词要分清,permanent-fix-map(永久固定映射)/temporary-fix-map(临时固定映射)。还有一个是persisent-kernel-mapping(持久内核映射)。要翻译过来都很接近。
win.lin
2018-07-09 11:11
对的。fix-map分:fix-map与temporary fix_map。和Permanent不是一回事。
蓮之物
2018-07-06 16:44
(  )5.關於fixmap address,下列何者正確
(1) 在kernel啟動初期,我們藉著使用ioremap接口創建虛擬地址和物理地址的映射
(2) fixmap區域是地址空間範圍從FIXADDR_START到FIXADDR_TOP結束, FIXADDR_SIZE是temporary fixed addresses區域的大小
(3) kernel正常運作時,運作temporary fixed addresses的資料必須是特權指令的
(4) FIX_FDT是給dtb創建映射的區域。例如需要得到FDT的虛擬地址,即可以利用__virt_to_fix(x)得到虛擬地址,也就是運作(FIXADDR_TOP - ((x) << PAGE_SHIFT))
(5) Device Tree Blob中包含bootloader傳遞過來的內存信息,而我們得到的是Device Tree Blob的物理地址
蓮之物
2018-07-06 16:46
@蓮之物:up寫得太好
情不自禁的做了個單選題
gnx
2018-05-06 16:51
固定映射空间分为两部分:永久映射和临时映射,其中临时映射和kmap(持久映射)在32位系统上有一些纠葛。在32位系统上,如果使能了高端内存,虚拟地址的最后两部分分别是持久映射和固定映射,这个持久映射不要和永久固定映射搞混淆了。持久映射是可以通过kmap和kunmap建立和解除映射的,持久映射可以理解为是一种将高端内存映射到内核虚拟地址空间的一种方式。但是kmap函数有个问题,他可能导致阻塞因而不能用在中断上下文中,所以内核搞了一个临时映射kmap_atomic,从固定映射空间拿了一块虚拟内存出来,这部门内存空间就处于固定映射的临时映射区域。所以内核在计算固定映射大小时不会把临时映射区域计算在内,当然在64位系统上没有高端内存,也就没有所谓的持久映射,但是代码为了向前兼容,就依然保持这样的计算方式,而且这也并不影响固定映射的使用。以上是我的个人理解,不对之处请指正!
smcdef
2018-05-06 17:57
@gnx:关于文章中“FIXADDR_SIZE是permanent fixed addresses区域的大小。我对这个地方很奇怪,为什么不包括temporary fixed addresses区域的大小呢?”这个疑问,我也有自己的想法。
我是这么想的,临时映射仅仅是系统启动初期,ioremap等子系统还没有ready的时候使用,当内存管理等各个子系统ready之后,临时映射在ARM64中实际上就没有用武之地了,当然此时也不会有相关的模块还是用临时映射区域。所以临时映射区域的虚拟地址实际上并没有占用(内存管理系统没有ready的时候才占用),因此不需要显示的宏定义告诉你这部分地址被分配了。
这位兄弟,你觉得呢?

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog