Linux 内存管理 (3):fixmap
上一篇:Linux 内存管理 (2):memblock 子系统的建立
文章目录
- 1. 前言
- 2. 什么是 fixmap ?
- 3. fixmap 实现
- 3.1 fixmap 初始化
- 3.2 示例
- 3.3 内存子系统初始化后的 fixmap
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));elsepte_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);
}
