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];
}
相关推荐
陈希12313 分钟前
Linux 防火墙配置实战:firewalld、iptables 与云安全组
linux·运维·服务器
女神下凡1 小时前
芯参谋(95):嵌入式文件系统_开发指南
linux·服务器·arm开发·驱动开发·设计规范
且从容.4 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道5 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.8 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落9 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨9 小时前
WSL2的安装与使用
linux·wsl
微小的鱼9 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng10 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮10 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频