前面我们说到,为kernel image设置了虚实地址转换表,并且开启了mmu。但是现在从虚拟地址空间CPU只能看到kernel image,如果此时想访问其他物理地址空间怎么办?用ioremap吗?要知道,此时内存子系统还没有初始化,ioremap无法工作。为了解决这一问题,Linux内核定义了一段固定的虚拟地址空间,所谓固定就是说在编译时就确定的,内核启动早期会将某些物理地址映射到这段固定虚拟地址空间。
我们先来想一个问题,谁会使用这段固定虚拟地址空间?乍一看这个问题可不好回答,那么可以换一种问法,CPU要使用哪些物理地址空间?这个问题就比较容易了吧!哈哈,想到点什么没有?至少有两点是比较容易想到的:
DTB:目前DTB在linux内核中可谓深入人心,DTB用来描述硬件拓扑及资源信息,linux启动早期需要解析DTB文件,获取硬件拓扑及资源信息。
Console:调试过Linux内核启动的朋友应该都是知道串口控制台的重要性,基于调试的考虑,我们往往希望尽可能早的通过串口控制台打印我们需要的信息,那么就需要访问UART的控制器。
下面是linux所定义的固定虚拟地址空间的layout:
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,
__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
};
- FIX_FDT—FIX_FDT_END(4M)
用于DTB.
- FIX_EARLYCON_MEM_BASE
用于早期串口控制台。
- FIX_TEXT_POKE0
用法不明。
- FIX_BTMAP_BEGIN—FIX_BTMAP_END
用于早期ioremap。
- FIX_PTE
pte table固定虚拟地址,用于临时映射。
- FIX_PMD
pmd table固定虚拟地址,用于临时映射。
- FIX_PUD
pud table固定虚拟地址,用于临时映射。
- FIX_PGD
pgd table固定虚拟地址,用于临时映射。
1. 固定映射初始化:early_fixmap_init
为虚拟地址FIXADDR_START(即固定虚拟地址空间的起始地址)设置前三级转换。
单看这个函数的逻辑,其实特别简单,就是根据虚拟地址找到pgd table中的某个entry,并填充pud table的物理基地址;再根据虚拟地址找到pud table中的某个entry,并填充pmd table的物理地址;在根据虚拟地址找到pmd table中的某个entry,并填充pte table的物理地址,并没有将虚拟地址映射到物理地址。
代码逻辑看似简单,实际上暗藏玄机,如果不把此间玄机窥探清除,读后面的代码会非常迷惑,下面我们就把其中玄机一点一点呈现出来。
- 玄机1:FIXADDR_START神秘之处。
回头看下固定虚拟地址layout,FIXADDR_TOP是2M对齐的,这个从FIXADDR_TOP定义可以知道,而FIXADDR_START并非2M对齐,这一点其实非常重要,后面的两个玄机都与此相关。
- 玄机2:为DTB固定虚拟地址设置了前两级转换表,即PDG,PUD转换表。
DTB是按section来作映射的,section映射需要三级转换,一条section映射覆盖2M空间,由于FIXADDR_TOP是2M对齐的,因而两个连续的pmd entry即可覆盖4M的DTB虚拟地址空间,后面只需要填充最多两个PMD entry即可。
- 玄机3:为其他固定虚拟地址设置了前三级转换表,及PGD,PUD,PMD转换表。
由于FIXADDR_START不是2M对齐的,此时设置的pmd entry覆盖所有的非DTB虚拟地址空间,后面映射到实际物理地址的时候,只需要填充该PMD entry指向的PTE table中的某个entry即可。
2. 早期ioremap初始化:early_ioremap_init
为了在内存子系统起来之前使用ioremap,linux实现了早期ioremap机制。该函数实现相关的初始化,实际上该初始化函数仅仅是将用于早期ioremap的固定虚拟地址空间FIX_BTMAP_BEGIN_FIX_BITMAP_END的虚拟地址存放到数组slot_virt中。
3. 完成DTB虚实地址映射:__fixmap_remap_fdt
前面early_fixmap_init已经为DTB固定虚拟地址设置了前两级转换表,此时再设置一个或者两个PMD entry即可。那么一个或者两个是怎么定的呢?主要是看DTB在物理地址空间是否跨2M的边界,因为DTB是按section(2M)来做映射的,如果DTB跨2M边界,则需要连续两个pmd entry。由于DTB最大2M,如果DTB物理基地址不是2M对齐的,则有可能出现DTB跨2M边界的情况。
3.1. dt_virt_base = __fix_to_virt(FIX_FDT);
计算得出分配给dtb的虚拟基地址。
3.2. create_mapping_noalloc:first
设置第一个section mapping,主要就是设置PMD entry。注意,这里noalloc的意思是,不会分配新的地址转换table,只会填充某个table的entry。该函数的具体代码我们不去一一分析了,我这里把其中比较重要的点列出来,供有兴趣的朋友分析代码是参考:
3.3. create_mapping_noalloc:second
如果第一个section mapping无法覆盖完成的DTB,则需要设置第二个section mapping。
第二阶段主要是分析固定虚拟地址的映射,当然主要讲的是DTB,还有一些其他固定虚拟地址,我们后面遇到还会分析。