Linux 内存管理 (3):fixmap

上一篇:Linux 内存管理 (2):memblock 子系统的建立

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 什么是 fixmap ?

fixmap 是 Linux 内核内存管理机制之一,Linux 在【内核虚拟地址空间】中预留一部分,用于临时映射物理页面

3. fixmap 实现

我们以 ARM32 架构下的 Linux 为例来说明 fixmap。Linux 通过硬编码的方式、在编译时确定了 fixmap 使用的虚拟地址区间:

/* arch/arm/include/asm/fixmap.h */

#define FIXADDR_START		0xffc00000UL
#define FIXADDR_END		0xfff00000UL
#define FIXADDR_TOP		(FIXADDR_END - PAGE_SIZE)

更进一步的,按用途将 fixmap 虚拟地址区间划分为更多的子区间:

/* arch/arm/include/asm/fixmap.h */

enum fixed_addresses {
	FIX_EARLYCON_MEM_BASE,
	__end_of_permanent_fixed_addresses,

	FIX_KMAP_BEGIN = __end_of_permanent_fixed_addresses,
	FIX_KMAP_END = FIX_KMAP_BEGIN + (KM_TYPE_NR * NR_CPUS) - 1,

	/* Support writing RO kernel text via kprobes, jump labels, etc. */
	FIX_TEXT_POKE0,
	FIX_TEXT_POKE1,

	__end_of_fixmap_region,

	/*
	 * Share the kmap() region with early_ioremap(): this is guaranteed
	 * not to clash since early_ioremap() is only available before
	 * paging_init(), and kmap() only after.
	 */
	/*
	 * paging_init() 前, 用于 early ioremap
	 * paging_init() 后, 用于 kmap
	 */ 
#define NR_FIX_BTMAPS		32
#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, 
	__end_of_early_ioremap_region
};

3.1 fixmap 初始化

start_kernel()
	setup_arch()
		early_fixmap_init() // fixmap 初始化
		early_ioremap_init() // early ioremap 使用 fixmap 进行映射相关的初始化

early_fixmap_init()fixmap 进行初始化:

/* arch/arm/mm/mmu.c */

/*
 * FIXMAP 初始化: 
 * . 设定 FIXMAP 的 PTE 页表: PTE 尚未初始化, 即尚未分配物理
 *   内存;
 * . FIXMAP 函数指针 pte_offset_fixmap 设定: 
 *   用于 [虚拟地址 ==> FIXMAP 的 PTE 页表项 虚拟地址] 的转换.
 */
void __init early_fixmap_init(void)
{
	pmd_t *pmd;

	/*
	 * The early fixmap range spans multiple pmds, for which
	 * we are not prepared:
	 */
	BUILD_BUG_ON((__fix_to_virt(__end_of_early_ioremap_region) >> PMD_SHIFT)
		     != FIXADDR_TOP >> PMD_SHIFT);

	/* 设定 FIXMAP (虚拟地址区间 [FIXADDR_TOP, FIXADDR_END)) PTE 页表为 bm_pte[] */
	pmd = fixmap_pmd(FIXADDR_TOP);
	pmd_populate_kernel(&init_mm, pmd, bm_pte);

	/* 设定 将 fixmap 虚拟地址转换为 PTE 页表项的接口 */
	pte_offset_fixmap = pte_offset_early_fixmap;
}

从上面的代码分析看到,fixmap 的 PTE 页表使用预编译到内核的 bm_pte[] 数组空间,其页表映射如下图(2 级分页示例):

image

3.2 示例

以 early console 为例,说明下 fixmap 的使用。

/* drivers/tty/serial/earlycon.c */

static int __init register_earlycon(char *buf, const struct earlycon_id *match)
{
	...
	if (port->mapbase)
		port->membase = earlycon_map(port->mapbase, 64);
	...
}
	
static void __iomem * __init earlycon_map(resource_size_t paddr, size_t size)
{
	void __iomem *base;
#ifdef CONFIG_FIX_EARLYCON_MEM
	/* 设置 fixmap FIX_EARLYCON_MEM_BASE 区间的对应的物理页面地址为 @paddr */
	set_fixmap_io(FIX_EARLYCON_MEM_BASE, paddr & PAGE_MASK);
	/* 获取 fixmap FIX_EARLYCON_MEM_BASE 区间的对应页面的虚拟地址 */
	base = (void __iomem *)__fix_to_virt(FIX_EARLYCON_MEM_BASE);
	base += paddr & ~PAGE_MASK;
#else
	...
#endif

	...

	return base;
}

上面的关键是 set_fixmap_io()__fix_to_virt() 调用:

set_fixmap_io(): 设置 fixmap 区间的物理地址
__fix_to_virt(): 获取 fixmap 区间的虚拟地址

先看 set_fixmap_io() 的实现细节:

/* include/asm-generic/fixmap.h */

/*
 * Some fixmaps are for IO
 */
#define set_fixmap_io(idx, phys) \
	__set_fixmap(idx, phys, FIXMAP_PAGE_IO)
/* arch/arm/mm/mmu.c */

/*
 * To avoid TLB flush broadcasts, this uses local_flush_tlb_kernel_range().
 * As a result, this can only be called with preemption disabled, as under
 * stop_machine().
 */
/*
 * 设定 物理地址 @phys 的 PTE 页表项,
 * 将物理地址到 @phys 映射到 @idx 指向的 FIXMAP 虚拟地址. 
 */
void __set_fixmap(enum fixed_addresses idx, phys_addr_t phys, pgprot_t prot)
{
	unsigned long vaddr = __fix_to_virt(idx);
	pte_t *pte = pte_offset_fixmap(pmd_off_k(vaddr), vaddr); /* 获取虚拟地址 @vaddr 的 PTE 页表项 */

	/* Make sure fixmap region does not exceed available allocation. */
	BUILD_BUG_ON(FIXADDR_START + (__end_of_fixed_addresses * PAGE_SIZE) >
		     FIXADDR_END);
	BUG_ON(idx >= __end_of_fixed_addresses);

	/* we only support device mappings until pgprot_kernel has been set */
	if (WARN_ON(pgprot_val(prot) != pgprot_val(FIXMAP_PAGE_IO) &&
		    pgprot_val(pgprot_kernel) == 0))
		return;

	if (pgprot_val(prot))
		set_pte_at(NULL, vaddr, pte,
			pfn_pte(phys >> PAGE_SHIFT, prot));
	else
		pte_clear(NULL, vaddr, pte);
	local_flush_tlb_kernel_range(vaddr, vaddr + PAGE_SIZE);
}

其中 pte_offset_fixmap 用于返回虚拟地址的 PTE 页表项,此时 pte_offset_fixmap 指向 pte_offset_early_fixmap()

static pte_t * __init pte_offset_early_fixmap(pmd_t *dir, unsigned long addr)
{
	return &bm_pte[pte_index(addr)];
}

再看 __fix_to_virt() 的实现细节:

/* include/asm-generic/fixmap.h */

#define __fix_to_virt(x)	(FIXADDR_TOP - ((x) << PAGE_SHIFT))

3.3 内存子系统初始化后的 fixmap

fixmap 在 Linux 的不同运行阶段,会稍有不同。具体是以初始化内存子系统的 paging_init() 调用为分界,我们将 fixmap 划分为 内存子系统初始化前的 fixmap内存子系统初始化后的 fixmap,它们之间的差别在于使用了不同 PTE 页表映射 fixmap 虚拟地址区间

paging_init() 初始化内存子系统期间,重新为 fixmap 分配 PTE 页表,而不再使用 bm_pte[] 页表:

start_kernel()
	setup_arch()
		//early_fixmap_init()
		paging_init()
/* arch/arm/mm/mmu.c */

void __init paging_init(const struct machine_desc *mdesc)
{
	...
	early_fixmap_shutdown();
	...
}

static void __init early_fixmap_shutdown(void)
{
	int i;
	unsigned long va = fix_to_virt(__end_of_permanent_fixed_addresses - 1);

	pte_offset_fixmap = pte_offset_late_fixmap; /* 重新设定 FIXMAP 虚拟地址 @addr 的 PTE 页表项查找接口 */
	pmd_clear(fixmap_pmd(va)); /* 所有的 FIXMAP 映射只占据一个 PMD 页表项 */
	local_flush_tlb_kernel_page(va);

	/* 
	 * 对于在 early boot 阶段 已经建立 页表映射 的 FIXMAP 恒久映射, 
	 * 不再使用在 early boot 阶段 PTE 页表 bm_pte[] 进行映射, 而是
	 * 重新 create_mapping() 重新建立它们的映射。新的映射中, 虚拟地址 
	 * 到 物理地址 的 映射关系维持不变, 变换的是:
	 * a. PTE 页表不再使用 bm_pte[], 而是使用重新动态分配 PTE 页表;
	 * b. 映射的内存类型设定为 MT_DEVICE 类型。
	 */
	for (i = 0; i < __end_of_permanent_fixed_addresses; i++) {
		pte_t *pte;
		struct map_desc map;

		map.virtual = fix_to_virt(i);
		pte = pte_offset_early_fixmap(pmd_off_k(map.virtual), map.virtual);

		/* Only i/o device mappings are supported ATM */
		if (pte_none(*pte) ||
		    (pte_val(*pte) & L_PTE_MT_MASK) != L_PTE_MT_DEV_SHARED)
			continue;

		map.pfn = pte_pfn(*pte);
		map.type = MT_DEVICE;
		map.length = PAGE_SIZE;

		create_mapping(&map);
	}
}

从上面可以看到,主要做了两点工作:

1. 重新设定 pte_offset_fixmap 为 pte_offset_late_fixmap()
2. 重新为 fixmap 分配了 PTE 页表,并在新 PTE 页表中维持了 fixmap 恒久映射区已经建立的映射

为什么要替换 PTE 页表,一方面因为 bm_pte[] 页表空间为 initdata,在内存子系统建立后会被释放掉;另一方面也是要维护统一的页表管理。

static pte_t bm_pte[PTRS_PER_PTE + PTE_HWTABLE_PTRS]
	__aligned(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE) __initdata;

那么,pte_offset_fixmap 指向 pte_offset_late_fixmap() 后,不再从 bm_pte[] 返回 PTE 页表项,而是从 early_fixmap_shutdown() 中新分配的 PTE 页表返回 PTE 页表项了:

static pte_t *pte_offset_late_fixmap(pmd_t *dir, unsigned long addr)
{
	return pte_offset_kernel(dir, addr);
}
posted @ 2025-05-30 15:33  JiMoKuangXiangQu  阅读(16)  评论(0)    收藏  举报