Linux虚拟内存固定映射区-fixmap

1 概述

为了理解fixmap,本文先记录了fixmap的概念和作用,再从Linux源码的角度,分析了fixmap初始化相关的流程。

2 fixmap介绍

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

Fixmap在内核又被分成了许多不同类型的区域,内核用枚举来区分这些区域,如下定义在arch\arm64\include\asm\fixmap.h

复制代码
enum fixed_addresses {
	FIX_HOLE,

	/*
	 * Reserve a virtual window for the FDT that is 2 MB larger than the
	 * maximum supported size, and put it at the top of the fixmap region.
	 * The additional space ensures that any FDT that does not exceed
	 * MAX_FDT_SIZE can be mapped regardless of whether it crosses any
	 * 2 MB alignment boundaries.
	 *
	 * Keep this at the top so it remains 2 MB aligned.
	 */
#define FIX_FDT_SIZE		(MAX_FDT_SIZE + SZ_2M)
	FIX_FDT_END,
	FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,

	FIX_EARLYCON_MEM_BASE,
	FIX_TEXT_POKE0,

#ifdef CONFIG_ACPI_APEI_GHES
	/* Used for GHES mapping from assorted contexts */
	FIX_APEI_GHES_IRQ,
	FIX_APEI_GHES_SEA,
#ifdef CONFIG_ARM_SDE_INTERFACE
	FIX_APEI_GHES_SDEI_NORMAL,
	FIX_APEI_GHES_SDEI_CRITICAL,
#endif
#endif /* CONFIG_ACPI_APEI_GHES */

#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
	FIX_ENTRY_TRAMP_TEXT3,
	FIX_ENTRY_TRAMP_TEXT2,
	FIX_ENTRY_TRAMP_TEXT1,
	FIX_ENTRY_TRAMP_DATA,
#define TRAMP_VALIAS		(__fix_to_virt(FIX_ENTRY_TRAMP_TEXT1))
#endif /* CONFIG_UNMAP_KERNEL_AT_EL0 */
	__end_of_permanent_fixed_addresses,

	/*
	 * Temporary boot-time mappings, used by early_ioremap(),
	 * before ioremap() is functional.
	 */
#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,

	/*
	 * Used for kernel page table creation, so unmapped memory may be used
	 * for tables.
	 */
	FIX_PTE,
	FIX_PMD,
	FIX_PUD,
	FIX_PGD,

	__end_of_fixed_addresses
};

用如下款图更直观了解fixmap区域分布:

3 fixmap源码分析

fixmap源码实现在arch\arm64\include\asm\fixmap.h和arch\arm64\mm\mmu.c,如下红色框图部分fixmap初始化和io/dtb映射代码。

early_fixmap_init是Linux内核对fixmap的最初初始化函数,该函数填充了每一级页表地址到最后一级PTE没有填充,这个工作留给了具体模块使用fixmap映射时来填充,第一级页表内存使用的是swapper_pg_dir(也是后面映射用的页表地址):

复制代码
void __init early_fixmap_init(void)
{
	pgd_t *pgdp;
	p4d_t *p4dp, p4d;
	pud_t *pudp;
	pmd_t *pmdp;
	unsigned long addr = FIXADDR_START;

	pgdp = pgd_offset_k(addr);//取出pgd的页表基地址
	p4dp = p4d_offset(pgdp, addr);//同pgdp
	p4d = READ_ONCE(*p4dp);//gpd页表当前的内容
	if (CONFIG_PGTABLE_LEVELS > 3 &&
	    !(p4d_none(p4d) || p4d_page_paddr(p4d) == __pa_symbol(bm_pud))) {
		/*
		 * We only end up here if the kernel mapping and the fixmap
		 * share the top level pgd entry, which should only happen on
		 * 16k/4 levels configurations.
		 */
		BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
		pudp = pud_offset_kimg(p4dp, addr);
	} else {
		if (p4d_none(p4d))//第一次初始化,pgd没有内容,写入下一级页表地址信息
			__p4d_populate(p4dp, __pa_symbol(bm_pud), P4D_TYPE_TABLE);//将pud物理地址写入pgd映射页表地址中
		pudp = fixmap_pud(addr);//pudp页表地址
	}
	if (pud_none(READ_ONCE(*pudp)))
		__pud_populate(pudp, __pa_symbol(bm_pmd), PUD_TYPE_TABLE);//将bm_pud物理地址写入swapper_pg_dir + addr's pgd valid offset
	pmdp = fixmap_pmd(addr);//取addr的pmd有entry
	__pmd_populate(pmdp, __pa_symbol(bm_pte), PMD_TYPE_TABLE);//将pte地址写入pmd映射页表
...
}

具体流程如下:

FIXADDR_START,定义了Fixed map区域的起始地址,pgd_offset_k(addr),获取addr地址对应pgd全局页表中的entry,而这个pgd全局页表正是swapper_pg_dir全局页表;将bm_pud的物理地址写到pgd全局页目录表中;将bm_pmd的物理地址写到pud页目录表中;将bm_pte的物理地址写到pmd页表目录表中。

其中,bm_pte/bm_pmd/bm_pud是被存放至bss中的变量:

复制代码
static pte_t bm_pte[PTRS_PER_PTE] __page_aligned_bss;
static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_bss __maybe_unused;
static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_bss __maybe_unused;

fixmap_remap_fdt是内核在fixmap使用阶段对dts映射的处理函数:

复制代码
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);//fixmap里fdt的虚拟起始地址
	int offset;
	void *dt_virt;
...
// dt_phys % (1 << 21),取dtb物理地址低22bit的值
	offset = dt_phys % SWAPPER_BLOCK_SIZE;
//偏移物理PGD有效位得到的物理地址,即物理地址要映射的地址
	dt_virt = (void *)dt_virt_base + offset;
//dtb映射函数,后面解析,这里先映射2MB,用来获取dts的头信息,检查dts是否合法
	/* map the first chunk so we can read the size from the header */
	create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE),
			dt_virt_base, SWAPPER_BLOCK_SIZE, prot);
//下面两行检查dts头部magic和文件最大值
	if (fdt_magic(dt_virt) != FDT_MAGIC)
		return NULL;

	*size = fdt_totalsize(dt_virt);
	if (*size > MAX_FDT_SIZE)
		return NULL;
//合法性校验过后,如果此时dts大小小于前面映射的2M,则前面映射的已经覆盖了dtb文件,否则则重新映射整个dts的大小
	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);

	return dt_virt;
}

关于宏SWAPPER_BLOCK_SIZE的大小是 1<<21由来如下:

复制代码
bbd@vin:~/workspace/lx/linux-5.15.158$ cat .config | grep "CONFIG_ARM64_4K_PAGES"
CONFIG_ARM64_4K_PAGES=y

linux-5.15.158\arch\arm64\include\asm\kernel-pgtable.h

复制代码
#ifdef CONFIG_ARM64_4K_PAGES
#define ARM64_KERNEL_USES_PMD_MAPS 1
#else
#define ARM64_KERNEL_USES_PMD_MAPS 0
#endif

linux-5.15.158\arch\arm64\include\asm\kernel-pgtable.h

复制代码
/* Initial memory map size */
#if ARM64_KERNEL_USES_PMD_MAPS
#define SWAPPER_BLOCK_SHIFT	PMD_SHIFT
#define SWAPPER_BLOCK_SIZE	PMD_SIZE
#define SWAPPER_TABLE_SHIFT	PUD_SHIFT
#else
#define SWAPPER_BLOCK_SHIFT	PAGE_SHIFT
#define SWAPPER_BLOCK_SIZE	PAGE_SIZE
#define SWAPPER_TABLE_SHIFT	PMD_SHIFT
#endif

linux-5.15.158\arch\arm64\include\asm\pgtable-hwdef.h

复制代码
/*
 * PMD_SHIFT determines the size a level 2 page table entry can map.
 */
#if CONFIG_PGTABLE_LEVELS > 2
#define PMD_SHIFT		ARM64_HW_PGTABLE_LEVEL_SHIFT(2)
#define PMD_SIZE		(_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK		(~(PMD_SIZE-1))
#define PTRS_PER_PMD		PTRS_PER_PTE
#endif

替换结果:

复制代码
SWAPPER_BLOCK_SIZE = 1<<21 = 2M

关于create_mapping_noalloc的实现:

复制代码
static void __init create_mapping_noalloc(phys_addr_t phys, unsigned long virt,
				  phys_addr_t size, pgprot_t prot)
{
...
	 //init_mm.pgd:fixmap虚拟地址pgd映射表地址,即前面分析的swapper_pg_dir
    //phys: dtb的物理地址
    //virt: 物理地址要映射的虚拟地址
    //size: 要映射的大小
    //port: 映射地址权限
	__create_pgd_mapping(init_mm.pgd, phys, virt, size, prot, NULL,
			     NO_CONT_MAPPINGS);
}

参数检查后,调用__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)(int),
				 int flags)
{
	unsigned long addr, end, next;
	pgd_t *pgdp = pgd_offset_pgd(pgdir, virt);;//找到pgd的偏移地址(虚拟地址的pgd(39~47bit)+映射表基地址)
...

	phys &= PAGE_MASK;//去掉page对应的位,值保留page以上的位,即0~11bit在这里被置0
	addr = virt & PAGE_MASK;//同样的查看路,去掉page对应的位
	end = PAGE_ALIGN(virt + size);//取出映射的结束虚拟地址,页对齐

	do {
  /*
    * pgdp: pgd页表地址
    * addr: 当前要映射的起始虚拟地址
    * next: 当前要映射的结束虚拟地址
    * phys: 当前要映射的起始物理地址
    * prot: 映射权限值
    * pgtable_alloc:回调,为空
    * flags: 映射方式,NO_CONT_MAPPINGS
    */
		next = pgd_addr_end(addr, end);
		alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc,
			       flags);
		phys += next - addr;//一个pgd能映射 1<< 39的范围,物理地址同样往前挪1<<39bit,即这些内存地址已被上一个pgd映射,取一个地址范围的物理起始地址来映射
	} while (pgdp++, addr = next, addr != end);//pgd页表之前往前移动一个指针(8bytes),虚拟地址pgd往前挪一个pgd,同时判断是否偏移到当前要映射的结束虚拟地址(即映射是否完成),否则继续。
}

后续调用是Linux对映射流程的处理,笔者之前写过关于Linux映射博文,这里不在深入:

https://blog.csdn.net/tang_vincent/article/details/155104152?sharetype=blogdetail&sharerId=155104152&sharerefer=PC&sharesource=tang_vincent&spm=1011.2480.3001.8118https://blog.csdn.net/tang_vincent/article/details/155104152?sharetype=blogdetail&sharerId=155104152&sharerefer=PC&sharesource=tang_vincent&spm=1011.2480.3001.8118 early_ioremap_init的实现,主要将fixmap划分给iomap的虚拟地址存放到slot数组,后续模块调用时直接从该数组取出虚拟地址与物理地址进行映射:

arch\arm64\mm\ioremap.c

复制代码
void __init early_ioremap_init(void)
{
	early_ioremap_setup();
}

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++)
		if (WARN_ON(prev_map[i]))
			break;
/*
*结合宏定义
#define NR_FIX_BTMAPS       (SZ_256K / PAGE_SIZE)
#define FIX_BTMAPS_SLOTS    7
#define TOTAL_FIX_BTMAPS    (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)//7*64=448
知道slot_virt的大小是 7,下面的循环,使用slot_virt每隔NR_FIX_BTMAPS即64 * page size 4K= 256K,存放一个fixmap偏移FIX_BTMAP_BEGIN地址的首地址
每个slot_virt间隔256K
*/
	for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
		slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
}

模块使用iomap时,将最终调用到__early_ioremap函数,该函数则是真正处理虚拟地址与物理地址映射的函数:

复制代码
static void __init __iomem *
__early_ioremap(resource_size_t phys_addr, unsigned long size, pgprot_t prot)
{
	unsigned long offset;
	resource_size_t last_addr;
	unsigned int nrpages;
	enum fixed_addresses idx;
	int i, slot;

	WARN_ON(system_state >= SYSTEM_RUNNING);

	slot = -1;
//找到空闲的虚拟地址
	for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
		if (!prev_map[i]) {
			slot = i;
			break;
		}
	}
    //槽满
	if (WARN(slot < 0, "%s(%pa, %08lx) not found slot\n",
		 __func__, &phys_addr, size))
		return NULL;

	/* Don't allow wraparound or zero size */
	last_addr = phys_addr + size - 1;
//回环处理,防止数据溢出
	if (WARN_ON(!size || last_addr < phys_addr))
		return NULL;
    
	prev_size[slot] = size;//保存当前slot使用的虚拟地址长度
	/*
	 * Mappings have to be page-aligned
	 */
	offset = offset_in_page(phys_addr);//取物理地址page位(12bit)以上的位
	phys_addr &= PAGE_MASK;//取page位的值,即低12bit的值
	size = PAGE_ALIGN(last_addr + 1) - phys_addr;//对齐后计算当前需要映射的大小,这个大小一定比对齐前大

	/*
	 * Mappings have to fit in the FIX_BTMAP area.
	 */
	nrpages = size >> PAGE_SHIFT;//符合fixmap的大小处理,以page位单位
	if (WARN_ON(nrpages > NR_FIX_BTMAPS))
		return NULL;

	/*
	 * Ok, go for it..
	 */
	idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;//取出当前fixmap区域的index,方便后面取出对应的虚拟地址
	while (nrpages > 0) {
		if (after_paging_init)//物理/虚拟内存管理框架起来后才会置位1,这里分析的是前期的内存使用方案,故不会走当前分支
			__late_set_fixmap(idx, phys_addr, prot);
		else
			__early_set_fixmap(idx, phys_addr, prot);//走次分支,直接写物理地址到pte上,物理地址和虚拟地址直接对应.
		phys_addr += PAGE_SIZE;//以page位单位循环处理
		--idx;
		--nrpages;
	}
	WARN(early_ioremap_debug, "%s(%pa, %08lx) [%d] => %08lx + %08lx\n",
	     __func__, &phys_addr, size, slot, offset, slot_virt[slot]);

	prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);//记录已使用的slot范围,方便下次使用
	return prev_map[slot];
}
相关推荐
企鹅侠客1 小时前
Ubuntu本地部署AnythingLLM实现本地文档RAG
linux·运维·ubuntu·llm
Victoria.a1 小时前
Linux环境基础开发工具使用
linux
被AI抢饭碗的人1 小时前
linux:进程间通信
linux·运维·服务器
森G1 小时前
六、imx6ull驱动实现
linux·c语言·ubuntu
_F_y2 小时前
Linux中项目自动化构建工具-make/Makefile
linux
chao1032 小时前
ubuntu下业务运行环境搭建
linux·运维·ubuntu
weixin_46682 小时前
Docker Dockerfile文件
linux·运维·服务器
赖small强2 小时前
【Linux C/C++ 开发】 GCC 编译过程深度解析指南
linux·c语言·c++·预处理·链接·编译·编译过程
做人不要太理性2 小时前
【Linux系统】ext2文件系统
大数据·linux·操作系统·文件系统