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 级分页示例):

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

浙公网安备 33010602011771号