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.8118
https://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];
}