Linux版本:4.14.74
1 建立系统映射的过程
经过前面几章的内容,我们开启了MMU,为kernel image创建了映射,通过调用start_kernel setup_arch建立页表映射,读取kernel建立页表映射的代码和流程如下图所示,下面分章节对这三个部分进行说明
1. void __init setup_arch(char **cmdline_p)
2. {
3. pr_info("Boot CPU: AArch64 Processor [%08x]\n", read_cpuid_id());
4.
5. .....
6.
7. early_fixmap_init();
8. early_ioremap_init();
9.
10. setup_machine_fdt(__fdt_pointer);
11. ....
12. arm64_memblock_init();
13.
14. paging_init();
15.
16. ....
17. }
本文主要描述映射DTB的过程
2 FIXMAP概念
虽然可以通过kernel image mapping和identity mapping来窥探物理地址空间,但终究是管中窥豹,不了解全局,那么内核是如何了解对端的物理世界呢?答案就是DTB,但是问题来了,这时候,内核还没有为DTB这段内存创建映射,因此,打开MMU之后的kernel还不能直接访问,需要先创建dtb mapping,而要创建address mapping,就需要分配页表内存,而这时候,还没有了解内存布局,内存管理模块还没有初始化,如何来分配内存呢?这要用到在内核虚拟内存布局中用到的fix map区域。
fixed map是被linux kernel用来解决一类问题的机制,这类问题的共同特点是:(1)在很早期的阶段需要进行地址映射,而此时,由于内存管理模块还没有完成初始化,不能动态分配内存,也就是无法动态分配创建映射需要的页表内存空间。(2)物理地址是固定的,或者是在运行时就可以确定的。对于这类问题,内核定义了一段固定映射的虚拟地址,让使用fix map机制的各个模块可以在系统启动的早期就可以创建地址映射,当然,这种机制不是那么灵活,因为虚拟地址都是编译时固定分配的。
下图是DTB的映射图
Fixmap区域分为permanent区域和temporary区域,所谓permanent表示映射关系永远都是存在的,例如FDT区域,一旦完成地址映射,内核可以访问DTB之后,这个映射关系一直都是存在的。而temporary fixmap则不然,一般而言,某个模块使用了这部分的虚拟地址之后,需要尽快释放这段虚拟地址,以便给其他模块使用,例如fixmap中为页表预留的区域PGD/PUD/PMD/PTE,在系统进行映射时,这块区域将会被反复使用。
上图为fixmap区域的虚拟地址布局
- FIXADDR_START是fix map虚拟地址的起始地址
- 蓝色区域为fixmap永久映射区域,橙色区域为temporary区域
- FDT为映射DTB的区域,大小为4M,起始地址是2M对齐。
- PGD/PUD/PMD/PTE用于在建立系统映射时,临时的页表虚拟地址
3 FIXMAP的初步映射
好,现在虚拟地址有了,要创建映射还需要有实际的物理地址来存放页表。对于fixed-mapped address这段虚拟地址空间,由于也是位于内核空间,因此PGD当然就是复用swapper进程的PGD了(其实整个系统就一个PGD),而其他LEVEL,如PUD/PMD/PTE的物理地址空间如何获取,此时页表未建立好,后面我们要讲的memblock也还没有建立好,此时是无法分配物理地址空间,所以PUD/PMD/PTE需要提前静态定义好
1. static pte_t bm_pte[PTRS_PER_PTE] __page_aligned_bss;
2. static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_bss __maybe_unused;
3. static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_bss __maybe_unused;
上述定义位于内核bss段,由于所有的Translation table都在kernel image mapping的范围内,因此内核可以毫无压力的访问,并创建fixed-mapped address这段虚拟地址空间对应的PUD、PMD和PTE的entry。所有中间level的Translation table都是在early_fixmap_init函数中完成初始化的。early_fixmap_init的所做工作如下图所示,建立起对FIXADDR_START的初步映射,所谓“初步”映射指的是只建立中间页表的关系,如下面的示意图,只建立了PGD–>PMD–>PTE的关系,而最后一步的映射在最终用的时候才会进行映射(实际上FIXADDR_MAP并不会用到,也就不会进行最终的映射)。
1. /*
2. * The p*d_populate functions call virt_to_phys implicitly so they can't be used
3. * directly on kernel symbols (bm_p*d). This function is called too early to use
4. * lm_alias so __p*d_populate functions must be used to populate with the
5. * physical address from __pa_symbol.
6. */
7. void __init early_fixmap_init(void)
8. {
9. pgd_t *pgd;
10. pud_t *pud;
11. pmd_t *pmd;
12. unsigned long addr = FIXADDR_START;
13.
14. pgd = pgd_offset_k(addr); /*****1****/
15. if (CONFIG_PGTABLE_LEVELS > 3 &&
16. !(pgd_none(*pgd) || pgd_page_paddr(*pgd) == __pa_symbol(bm_pud))) {
17. /*
18. * We only end up here if the kernel mapping and the fixmap
19. * share the top level pgd entry, which should only happen on
20. * 16k/4 levels configurations.
21. */
22. BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
23. pud = pud_offset_kimg(pgd, addr);
24. } else {
25. if (pgd_none(*pgd)) /******2*****/
26. __pgd_populate(pgd, __pa_symbol(bm_pud), PUD_TYPE_TABLE);
27. pud = fixmap_pud(addr);
28. }
29. if (pud_none(*pud)) /******3*****/
30. __pud_populate(pud, __pa_symbol(bm_pmd), PMD_TYPE_TABLE);
31. pmd = fixmap_pmd(addr); /******4******/
32. __pmd_populate(pmd, __pa_symbol(bm_pte), PMD_TYPE_TABLE);
33. …
34. }
- (1)获取FIXADDR_MAP对应的pgd页表项
- (2)以bm_pud的物理地址填充pgd
- (3)以bm_pmd的物理地址填充pud
- (4)以bm_pte的物理地址填充pmd
系统对dtb的size有要求,不能大于2M,这个要求主要是要确保在创建地址映射(create_mapping)的时候不能分配其他的translation table page,也就是说,所有的translation table都必须静态定义。为什么呢?因为这时候内存管理模块还没有初始化,即便是memblock模块(初始化阶段分配内存的模块)都尚未初始化(没有内存布局的信息),不能动态分配内存。最后一个level则是在各个具体的模块进行的,对于DTB而言,这发生在fixmap_remap_fdt函数中
4 DTB的映射
DTB的实际映射位于fixmap_remap_fdt中,在前面的过程中,已经对FIXMAP区域的PGD–>PUD–>PMD–>PTE有了初步的映射,并且DTB区域的虚拟地址是2M的对齐的,这也是为了以2M为单位进行section map的映射。DTB映射的过程如下图
- 第一次映射,PGD–>PMD的映射关系在early_fixmap_init已经建立,第一次映射是建立2M区域的映射,也就是对PMD的一个页表项进行映射,并且,相对于在early_fixmap_init中建立的映射关系,由于FIXADDR_START不是2M对齐的,而FDT是2M对齐的,所以此时PMD的页表项index会大1.
- 第二次映射,虽然FDT的虚拟地址是2M对齐的,但是DTB的物理地址并不一定是2M对齐的,如果DTB正好位于跨2M的区域内,则需要进行第二次映射,在第一次映射中已经对DTB进行了部分映射,这一部分映射就可以读取出DTB的实际大小,在第二次映射中进行完整映射。
void *__init __fixmap_remap_fdt(phys_addr_t dt_phys, int *size, pgprot_t prot)
{
/********1*******
const u64 dt_virt_base = __fix_to_virt(FIX_FDT);
int offset;
void *dt_virt;
/*
* Check whether the physical FDT address is set and meets the minimum
* alignment requirement. Since we are relying on MIN_FDT_ALIGN to be
* at least 8 bytes so that we can always access the magic and size
* fields of the FDT header after mapping the first chunk, double check
* here if that is indeed the case.
*/
BUILD_BUG_ON(MIN_FDT_ALIGN < 8);
if (!dt_phys || dt_phys % MIN_FDT_ALIGN)
return NULL;
/*
* Make sure that the FDT region can be mapped without the need to
* allocate additional translation table pages, so that it is safe
* to call create_mapping_noalloc() this early.
*
* On 64k pages, the FDT will be mapped using PTEs, so we need to
* be in the same PMD as the rest of the fixmap.
* On 4k pages, we'll use section mappings for the FDT so we only
* have to be in the same PUD.
*/
BUILD_BUG_ON(dt_virt_base % SZ_2M);
BUILD_BUG_ON(__fix_to_virt(FIX_FDT_END) >> SWAPPER_TABLE_SHIFT !=
__fix_to_virt(FIX_BTMAP_BEGIN) >> SWAPPER_TABLE_SHIFT);
/*******2******/
offset = dt_phys % SWAPPER_BLOCK_SIZE;
dt_virt = (void *)dt_virt_base + offset;
/*******3******
/* 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);
if (fdt_magic(dt_virt) != FDT_MAGIC)
return NULL;
*size = fdt_totalsize(dt_virt);
if (*size > MAX_FDT_SIZE)
return NULL;
/*******4******/
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;
}
- (1)获取FDT的虚拟基地址
- (2)获取实际的offset和虚拟地址
- (3)映射第一块2M地址
- (4)若跨2M区域,进行第二块2M映射
来源:oschina
链接:https://my.oschina.net/u/4281209/blog/4267849