目录
继上文介绍内核在启动初期为内核段建立了临时映射页表,实现了内核物理地址到虚拟地址的映射;该映射完成后可以通过虚拟地址访问内核空间,但此段仅限于内核段的访问,内核之外的内存空间还不能使用虚拟地址进行访问,需要继续完成内存空间外的内存映射。要实现其他内存空间的映射就需要从fdt中解析系统的内存情况,要访问fdt就要先实现fdt的映射,即要引出本章的介绍的主角fixmap。
fdt物理内存空间映射到虚拟地址空间是通过fixmap实现的,所谓fixmap是一段固定的虚拟内存空间,在系统编译前划分好虚拟地址。Fixmap的机制实现了系统资源物理地址向虚拟地址的映射,例如fdt、earlycon等内存数据的映射过程。
Fixmap固定虚拟地址
arm64/include/asm/memory.h
#define FIXADDR_TOP (PCI_IO_START - SZ_2M)
arm64/include/asm/fixmap.h
#define FIXADDR_SIZE (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE)
如上Fixmap固定虚拟地址为FIXADDR_START,其为Fixmap的固定虚拟地址的尾部地址FIXADDR_TOP 减去 Fixmap虚拟地址空间的大小 FIXADDR_SIZE。
由FIXADDR_SIZE定义可知Fixmap虚拟空间大小永久fixmap空间,不包含临时fixmap空间。
FIXADDR_TOP 地址为 PCI_IO起始虚拟地址 减 2M。
Fixmap空间特性
- Fixmap枚举结构
arch/arm64/include/asm/fixmap.h
/*
* Here we define all the compile-time 'special' virtual
* addresses. The point is to have a constant address at
* compile time, but to set the physical address only
* in the boot process.
*
* These 'compile-time allocated' memory buffers are
* page-sized. Use set_fixmap(idx,phys) to associate
* physical memory with fixmap indices.
*
*/
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_NMI,
#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结构中每个元素的虚拟地址在,Fixmap结构中包含了永久虚拟地址空间和临时虚拟地址空间。永久虚拟空间即该空间决定后不会再被释放作为他用,临时虚拟空间则相反。
Fixmap虚拟空间地址是以page size对齐,即相邻两个元素的地址的差为page size的整数倍。每个虚拟空间对应的物理地址空间地址在系统启动过程中建立。
- Fixmap ID和虚拟地址转化
#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
#define __virt_to_fix(x) ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)
如上虚拟地址与Fixmap ID之间的转换逻辑,因为Fixmap ID对应的虚拟地址以page size对齐,所以Fixmap ID左移page_shift位后即为该Fixmap ID对应的虚拟地址基于FIXADDR_TOP的偏移。
Fixmap虚拟空间的映射过程
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;
/*
* The p*d_populate functions call virt_to_phys implicitly so they can't be used
* directly on kernel symbols (bm_p*d). This function is called too early to use
* lm_alias so __p*d_populate functions must be used to populate with the
* physical address from __pa_symbol.
*/
void __init early_fixmap_init(void)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
/*
* 内核启动到此函数之前只完成了kernel内存空间的映射,FIXADDR_START虚拟地址不在内核空间,
* 故FIXADDR_START对应的页表目录为空,此函数就是要实现Fixmap空间的映射,完成Fixmap页表目录
* 创建
*/
unsigned long addr = FIXADDR_START;
pgd = pgd_offset_k(addr);
/*
* pgt_level大于3时,pud代表的上级页表项存在,需要完成上级页表项的填充;
*/
if (CONFIG_PGTABLE_LEVELS > 3 &&
!(pgd_none(*pgd) || pgd_page_paddr(*pgd) == __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));
pud = pud_offset_kimg(pgd, addr);
} else {
/*
* 3级页表时,pud等同于pgd;故pgd页表项填充bm_pmd对应物理地址
*/
if (pgd_none(*pgd))
__pgd_populate(pgd, __pa_symbol(bm_pud), PUD_TYPE_TABLE);
pud = fixmap_pud(addr);
}
if (pud_none(*pud))
__pud_populate(pud, __pa_symbol(bm_pmd), PMD_TYPE_TABLE);
/*
* 获取FIXADDR_START虚拟地址对应的pmd页表项,并使用bm_pte物理地址对其进行填充
*/
pmd = fixmap_pmd(addr);
__pmd_populate(pmd, __pa_symbol(bm_pte), PMD_TYPE_TABLE);
/*
* The boot-ioremap range spans multiple pmds, for which
* we are not prepared:
*/
BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)
!= (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));
if ((pmd != fixmap_pmd(fix_to_virt(FIX_BTMAP_BEGIN)))
|| pmd != fixmap_pmd(fix_to_virt(FIX_BTMAP_END))) {
WARN_ON(1);
pr_warn("pmd %p != %p, %p\n",
pmd, fixmap_pmd(fix_to_virt(FIX_BTMAP_BEGIN)),
fixmap_pmd(fix_to_virt(FIX_BTMAP_END)));
pr_warn("fix_to_virt(FIX_BTMAP_BEGIN): %08lx\n",
fix_to_virt(FIX_BTMAP_BEGIN));
pr_warn("fix_to_virt(FIX_BTMAP_END): %08lx\n",
fix_to_virt(FIX_BTMAP_END));
pr_warn("FIX_BTMAP_END: %d\n", FIX_BTMAP_END);
pr_warn("FIX_BTMAP_BEGIN: %d\n", FIX_BTMAP_BEGIN);
}
}
该函数在内核启动过程中通过调用__p*d_populate、__pa_symbol函数实现FIXMAP固定虚拟空间的页表创建(除pte页表)。__pa_symbol该函数获取内核符号对应的物理空间,具体实现逻辑可参考: Linux内存 -- 内核空间物理与虚拟地址转换_hello_yj的博客-CSDN博客__p*d_populate函数将bm_pxx的物理地址及PXX_TYPE_TABLE赋值给对应的页表项。
- early_fixmap_init函数创建Fixmap页表项过程:
1.通过 pgd_offset_k 函数获取FIXADDR_START对应的页表目录项pgd;
2.三级页表系统中pud页表项 同 pgd页表目录项,故内核数组变量bm_pud不会被使用,而是将内核数组变量bm_pmd的物理地址填充到pud页表项,将bm_pmd数组做为Fixmap虚拟空间pud页表项的物理空间;
3.获取FIXADDR_START虚拟地址对应的pmd页表项,并使用内核数组变量bm_pte物理地址对其进行填充;
early_fixmap_init函数只是创建了Fixmap虚拟空间对应的pgd 、pmd两级页表,并没有实现pte页表的创建。Fixmap虚拟空间对应的pte页表项创建,需要知道Fixmap ID对应的物理空间地址;在内核随后的启动过程中会逐步完成该pte页表的创建。
- fixmap_pmd 函数
fixmap_pmd函数目的在于获取fixmap空间特定虚拟地址对应的pmd页表项的虚拟地址。
pgd_level = 3 ,3级页表结构
#define pmd_offset_kimg(dir,addr) ((pmd_t *)__phys_to_kimg(pmd_offset_phys((dir), (addr))))
#define pmd_offset_phys(dir, addr) (pud_page_paddr(*(dir)) + pmd_index(addr) * sizeof(pmd_t))
static inline pmd_t * fixmap_pmd(unsigned long addr)
{
/*
* 获取虚拟地址addr对应的pud页表项
*/
pud_t *pud = fixmap_pud(addr);
BUG_ON(pud_none(*pud) || pud_bad(*pud));
/*
* 获取虚拟地址addr对应的pmd页表项
*/
return pmd_offset_kimg(pud, addr);
}
如上是fixmap_pmd相关代码实现(默认3级别页表结构),其中pmd_offset_kimg宏定义是功能实现(获取指定虚拟地址addr对应的pmd页表项虚拟地址)的核心函数。
pmd_offset_kimg具体实现:
1.dir为pud页表项,pud保存了addr虚拟地址对应的pmd页表项基地址,通过early_fixmap_init函数可知该地址即为 bm_pmd[]数组的物理地址;
2.通过pmd_index(addr)可知addr虚拟地址对应的pmd_index , pmd_index(addr) * sizeof(pmd_t)即为 addr虚拟地址的pmd页表基于bm_pmd的偏移。
3.知道addr对应的pmd页表物理地址后即可知道该页表对应的虚拟地址 (内核段虚拟地址 =内核段物理地址 + kimage_voffset );