本文谈谈内核初始话过程中是如何利用fixmap来实现动态分配内存的。
关于fixmap的学习,可以参考这两份链接:
http://www.wowotech.net/memory_management/440.html
http://www.wowotech.net/memory_management/fixmap.html
物理地址的分配是由memblock来完成,但这不意味这就可以使用了,还需要申请虚拟地址,同时再建立页表完成映射,这样才能使用分配的内存。而建立页表又需要申请内存并建立映射,如此循环下去怎么完成工作?我提出这个疑问是因为我在阅读paging_init的代码时,对那里重新映射的代码非常疑惑,因为那里需要不断动态申请内存。
以early_pgtable_alloc的代码来分析它是如何完成的:
static phys_addr_t __init early_pgtable_alloc(void)
{
phys_addr_t phys;
void *ptr;
phys = memblock_alloc(PAGE_SIZE, PAGE_SIZE);
/*
* The FIX_{PGD,PUD,PMD} slots may be in active use, but the FIX_PTE
* slot will be free, so we can (ab)use the FIX_PTE slot to initialise
* any level of table.
*/
ptr = pte_set_fixmap(phys);
memset(ptr, 0, PAGE_SIZE);
pte_clear_fixmap();
return phys;
}
对于使用memblock_alloc申请页对齐的一页内存,这里没什么好说的,但是把这页内存初始化为0就比较有意思。
它要使用memset函数来完成初始化为0的操作,但是要使用这个函数,需要传递的是虚拟地址,不能是memblock_alloc申请到的物理地址phys,所以需要通过pte_set_fixmap来获取虚拟地址,同时这个函数也会完成虚拟地址到物理地址的映射,这样后面使用这个虚拟地址时才不会发生page fault。
先来讨论下这个映射该如何完成:
1.物理地址作为输入参数,不需要关注了;
2.虚拟地址是固定的,就是根据FIX_PTE这个idx,通过fix_to_virt(idx)就可以得到;
3.映射的大小就是一个page,那么pgd、pmd和pte页表如何存储?
对于pgd,当然可以使用swapper_pg_dir,它本就支持所有的虚拟地址空间映射,相比于__create_page_tables,由于fixmap的虚拟地址访问离vmalloc的较远,所以它会使用swapper_pg_dir[512]中的另外一个index。
而这里的pmd和pte就不能和__create_page_tables那边的共用,一是因为那边是section map,只有多余的一个page用来存储pmd,二是这个pmd已经被占用了,不能共用。
这里的解决方法就是为fixmap自己静态申请两个page。要注意的是,bm_pmd和bm_pud有添加__maybe_unused,这意味这它会根据实际页表的状态而定,比如我们39bit,三级页表,那就没有bm_pud
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;
还有个问题,即便已经有了页来存放页表,但还需要建立这三个页表之间的联系,就是swapper_pg_dir[xx]指向bm_pmd,bm_pmd[xxx]指向bm_pte,关于这部分工作,已经在early_fixmap_init中完成,所以这不用关注了。
下面看看具体的代码实现:
// 这里使用的是FIX_PTE这个index
#define pte_set_fixmap(addr) ((pte_t *)set_fixmap_offset(FIX_PTE, addr))
// 映射时使用的flag就是PAGE_KERNEL
#define FIXMAP_PAGE_NORMAL PAGE_KERNEL
#define set_fixmap_offset(idx, phys) \
__set_fixmap_offset(idx, phys, FIXMAP_PAGE_NORMAL)
// 返回的________addr就是虚拟地址,直接根据fix_to_virt计算就可以得到,重要的不仅仅是返回虚拟地址,还需要建立映射后这个虚拟地址才能被使用,这就由__set_fixmap完成
/* Return a pointer with offset calculated */
#define __set_fixmap_offset(idx, phys, flags) \
({ \
unsigned long ________addr; \
__set_fixmap(idx, phys, flags); \
________addr = fix_to_virt(idx) + ((phys) & (PAGE_SIZE - 1)); \
________addr; \
})
// 这里只做一件事,就是把bm_pte[x]指向物理地址,由set_pte来完成。如前所述,其它的已经在early_fixmap_init中完成
void __set_fixmap(enum fixed_addresses idx,
phys_addr_t phys, pgprot_t flags)
{
unsigned long addr = __fix_to_virt(idx);
pte_t *pte;
BUG_ON(idx <= FIX_HOLE || idx >= __end_of_fixed_addresses);
pte = fixmap_pte(addr);
if (pgprot_val(flags)) {
set_pte(pte, pfn_pte(phys >> PAGE_SHIFT, flags));
} else {
pte_clear(&init_mm, addr, pte);
flush_tlb_kernel_range(addr, addr+PAGE_SIZE);
}
}
这里不禁要问,这样的方式能映射的最大空间是多少?就是bm_pte[512]全部映射到物理空间,那就是512*4K=2M,而且这2M还是受限的,比如虚拟地址的跨度只能在2M内等,不过这里是够用的,因为其实只有几个page会使用这里的映射。
下面看ramdump中bm_pte和bm_pmd的状态:
System.map
ffffff8009cfc000 b bm_pte
ffffff8009cfd000 b bm_pmd
对于bm_pmd,它的范围是[ffffff8009cfd000, ffffff8009cfe000),从ramdump中看,只有两个有效index,虚拟地址分别是:
bm_pmd[499] = ffffff8009cfdf98
bm_pmd[500] = ffffff8009cfdfa0
其中,bm_pmd[500]对应的dtb,我们前面讨论的使用的是bm_pmd[499],它指向bm_pte,bm_pte的物理地址正是11CFC000
回头来看,fixmap只适用于临时申请一个page,它可以帮你完成映射,允许你操作这个page。但是操作之后就需要释放掉这个映射,之后又会建立其它临时page的映射。如果你还想再访问原来的物理地址,那没办法了,再使用这个虚拟地址访问的已经是新申请的物理地址了。所以,它只是提供了临时读写一个物理页的方法。这对于map时是有用的,比如我为了创建线性映射区的映射,需要不停地申请页表然后填充信息,但是填充完成后,软件上不需要再访问这些申请的页表,那时硬件MMU去做的,对MMU而言,只要这个页表中内容有效就可以,MMU可不需要建立映射再访问。