上文介绍了启动页表的创建,通过fix map建立DTB物理地址的映射,以及memblock管理物理内存。现在我们能够通过memblock进行物理内存的分配,但分配的内存还不能够进行访问,我们需要对memblock管理的内存进行映射。
接着上文,开始内核的页表初始化函数paging_init()的分析。
本文基于linux kernel 5.8.0, 平台是arm64.
paging_init
void __init paging_init(void)
{
pgd_t *pgdp = pgd_set_fixmap(__pa_symbol(swapper_pg_dir)); -------- (1)
map_kernel(pgdp); --------- (2)
map_mem(pgdp); --------- (3)
pgd_clear_fixmap();
cpu_replace_ttbr1(lm_alias(swapper_pg_dir)); ---------- (4)
init_mm.pgd = swapper_pg_dir;
memblock_free(__pa_symbol(init_pg_dir),
__pa_symbol(init_pg_end) - __pa_symbol(init_pg_dir)); -------- (5)
memblock_allow_resize();
}
paging_init的代码很简短。
(1)swapper_pg_dir页表的物理地址映射到fixmap的FIX_PGD区域,然后使用swapper_pg_dir页表作为内核的pgd页表。因为页表都是处于虚拟地址空间构建的,所以这里需要转成虚拟地址pgdp。而此时伙伴系统也没有ready, 只能通过fixmap预先设定用于映射PGD的页表。现在pgdp是分配FIX_PGD的物理内存空间对应的虚拟地址
(2) 将kernel image的(.text .init .data .bss)区域进行映射
(3) 将memblock子系统添加的物理内存映射到线性区。
(4) 将ttbr1寄存器指向新准备的swapper_pg_dir页表。需要注意的是ttbr1保存的是物理地址,所以cpu_replace_ttbr1()中会先将swapper_pg_dir的页表的地址转换为物理地址。
static inline void cpu_replace_ttbr1(pgd_t *pgdp)
{
...
phys_addr_t ttbr1 = phys_to_ttbr(virt_to_phys(pgdp));
....
replace_phys = (void *)__pa_symbol(idmap_cpu_replace_ttbr1);
cpu_install_idmap();
replace_phys(ttbr1);
cpu_uninstall_idmap();
}
之后init_mm进程的pgd页表也从init_pg_dir切换到swapper_pg_dir
(5) 上面已经通过map_kernel()重新映射了kernel image的各个段,init_pg_dir已经没有价值了,将init_pg_dir指向的区域释放。
如下图所示, ttbr1在paging_init()执行前后,保存的页表基址从init_pg_dir切换到了swapper_pg_dir。 swapper_pg_dir页表的基地址被用作PGD页表的基地址。
map_kernel
map_kernel主要的动作就是完成内核的各个段的映射。毕竟kernel想要正常的running,它所需要的地址都需要进行映射。上文讲过在内核早期阶段使用过identity mapping, 不过那只是临时的映射, PGD/PUD/PMD/PTE占用的物理内存都是连续的,需要重新映射下。
在内核4.6之前, kernel image都是存放在线性地址中,所以不需要这一动作, 后面补丁 arm64: move kernel image to base of vmalloc area 为了实现kaslr的特性,将kernel image移动到vmalloc区域。
static void __init map_kernel(pgd_t *pgdp)
{
static struct vm_struct vmlinux_text, vmlinux_rodata, vmlinux_inittext,
vmlinux_initdata, vmlinux_data;
pgprot_t text_prot = rodata_enabled ? PAGE_KERNEL_ROX : PAGE_KERNEL_EXEC;
if (arm64_early_this_cpu_has_bti())
text_prot = __pgprot_modify(text_prot, PTE_GP, PTE_GP);
map_kernel_segment(pgdp, _text, _etext, text_prot, &vmlinux_text, 0,
VM_NO_GUARD); ----------------- (1)
map_kernel_segment(pgdp, __start_rodata, __inittext_begin, PAGE_KERNEL,
&vmlinux_rodata, NO_CONT_MAPPINGS, VM_NO_GUARD); ---------------- (2)
map_kernel_segment(pgdp, __inittext_begin, __inittext_end, text_prot,
&vmlinux_inittext, 0, VM_NO_GUARD); ---------------- (3)
map_kernel_segment(pgdp, __initdata_begin, __initdata_end, PAGE_KERNEL,
&vmlinux_initdata, 0, VM_NO_GUARD); ---------------- (4)
map_kernel_segment(pgdp, _data, _end, PAGE_KERNEL, &vmlinux_data, 0, 0); ---------------- (5)
if (!READ_ONCE(pgd_val(*pgd_offset_pgd(pgdp, FIXADDR_START)))) {
set_pgd(pgd_offset_pgd(pgdp, FIXADDR_START),
READ_ONCE(*pgd_offset_k(FIXADDR_START))); ------------------ (6)
} else if (CONFIG_PGTABLE_LEVELS > 3) {
pgd_t *bm_pgdp;
p4d_t *bm_p4dp;
pud_t *bm_pudp;
BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
bm_pgdp = pgd_offset_pgd(pgdp, FIXADDR_START);
bm_p4dp = p4d_offset(bm_pgdp, FIXADDR_START);
bm_pudp = pud_set_fixmap_offset(bm_p4dp, FIXADDR_START);
pud_populate(&init_mm, bm_pudp, lm_alias(bm_pmd));
pud_clear_fixmap();
} else {
BUG();
}
kasan_copy_shadow(pgdp);
}
(1)~ (5)分别调用map_kernel_segment()完成text, rodata, init, bss, data段的映射。
需要注意的是映射rodata段时,设置了flag为NO_CONT_MAPPINGS.
当前flag主要有两种:
#define NO_BLOCK_MAPPINGS BIT(0)
#define NO_CONT_MAPPINGS BIT(1)
NO_BLOCK_MAPPINGS用来标志限制BLOCK_MAPPING(巨页的映射)
NO_CONT_MAPPINGS用来标志限制映射连续的物理页面。
为什么要限制rodata段的连续映射? 可以参考这个补丁:arm64: mm: set the contiguous bit for kernel mappings where appropriate
映射连续的物理页面时,可以通过设置TLB条目的contiguous bit来保存TLB条目(contiguous-tlb可以参考这个补丁arm64: Add support for PTE contiguous bit),可以减少tlb空间的占用。但这会带来一个问题, contiguous mapping需要这整个区域都是有读/写权限的。内核的rodata段是只读的,不可修改的,所以不能使用contiguous mapping.
(6) 将fixaddr_start这一段地址建立映射。
map_sem
map_sem()完成memblock中添加的物理内存的映射。
static void __init map_mem(pgd_t *pgdp)
{
phys_addr_t kernel_start = __pa_symbol(_text);
phys_addr_t kernel_end = __pa_symbol(__init_begin);
struct memblock_region *reg;
int flags = 0;
if (rodata_full || debug_pagealloc_enabled())
flags = NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS;
/*
* Take care not to create a writable alias for the
* read-only text and rodata sections of the kernel image.
* So temporarily mark them as NOMAP to skip mappings in
* the following for-loop
*/
memblock_mark_nomap(kernel_start, kernel_end - kernel_start); ---------------- (1)
#ifdef CONFIG_KEXEC_CORE
if (crashk_res.end)
memblock_mark_nomap(crashk_res.start,
resource_size(&crashk_res));
#endif
/* map all the memory banks */
for_each_memblock(memory, reg) { ---------------- (2)
phys_addr_t start = reg->base;
phys_addr_t end = start + reg->size;
if (start >= end)
break;
if (memblock_is_nomap(reg))
continue;
__map_memblock(pgdp, start, end, PAGE_KERNEL, flags);
}
/*
* Map the linear alias of the [_text, __init_begin) interval
* as non-executable now, and remove the write permission in
* mark_linear_text_alias_ro() below (which will be called after
* alternative patching has completed). This makes the contents
* of the region accessible to subsystems such as hibernate,
* but protects it from inadvertent modification or execution.
* Note that contiguous mappings cannot be remapped in this way,
* so we should avoid them here.
*/
__map_memblock(pgdp, kernel_start, kernel_end,
PAGE_KERNEL, NO_CONT_MAPPINGS); -------------------- (3)
memblock_clear_nomap(kernel_start, kernel_end - kernel_start); --------------------- (4)
#ifdef CONFIG_KEXEC_CORE
/*
* Use page-level mappings here so that we can shrink the region
* in page granularity and put back unused memory to buddy system
* through /sys/kernel/kexec_crash_size interface.
*/
if (crashk_res.end) {
__map_memblock(pgdp, crashk_res.start, crashk_res.end + 1,
PAGE_KERNEL,
NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS);
memblock_clear_nomap(crashk_res.start,
resource_size(&crashk_res));
}
#endif
}
(1)不对设置了MEMBLOCK_NOMAP的标志的memblock进行映射。
(2) 遍历memblock中的各个块并完成内存的映射
(3) 将【kernel_start, kernel_end】的物理地址映射到线性映射区,对应的是kernel 的text和rodata段。
这两段又做了一次映射, 而且这段地址被映射。注释里是这样写的:
This makes the contents of the region accessible to subsystems such as hibernate.
其他子系统(比如hebernate休眠)会映射到线性映射区域,需要通过线性映射地址引用内核文本或数据段。
(4) 清除内核memblock区域中nomap标志
如下图所示, memory.region[0]/[2]/[3] 映射到线性映射区域, memory.region[1]被双重映射到kimage区域和线性映射区域。
create_pgd_mapping
页表映射最终都会调用到create_pgd_mapping函数.
arch/arm64/mmu.c
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); --------(1)
/*
* If the virtual and physical address don't have the same offset
* within a page, we cannot map the region as the caller expects.
*/
if (WARN_ON((phys ^ virt) & ~PAGE_MASK))
return;
phys &= PAGE_MASK; ----|
addr = virt & PAGE_MASK; ----|
end = PAGE_ALIGN(virt + size); ---------------------- (2)
do {
next = pgd_addr_end(addr, end); --------- (3)
alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc,
flags); ------------------ (4)
phys += next - addr;
} while (pgdp++, addr = next, addr != end);
}
(1) 指向pgd页表的页表项。相当于pgd_t *pgdp = &pgd_entry[i]。 pgd_entry存放的地址是物理地址,但后面的计算都是基于虚拟地址,这里做了一个地址的转换。
(2)上面三行分别是 获得起始物理地址的页偏移,获得起始虚拟地址的页偏移,以及检查地址范围内有多少个page,以及地址的内容和大小是否对齐。
(3) (addr, end)范围内的地址可能存在多个entry, 所以以next - addr为步长, 也就是PGDIR_SIZE来循环调用alloc_init_pud函数来完成(addr, end)这段虚拟地址的映射
(4) alloc_init_pud()用来初始化pgd页表项内容和下一级页表PUD
alloc_init_pud
static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot,
phys_addr_t (*pgtable_alloc)(int),
int flags)
{
unsigned long next;
pud_t *pudp;
p4d_t *p4dp = p4d_offset(pgdp, addr);
p4d_t p4d = READ_ONCE(*p4dp);
if (p4d_none(p4d)) { ------------------ (1)
phys_addr_t pud_phys;
BUG_ON(!pgtable_alloc);
pud_phys = pgtable_alloc(PUD_SHIFT);
__p4d_populate(p4dp, pud_phys, PUD_TYPE_TABLE);
p4d = READ_ONCE(*p4dp);
}
BUG_ON(p4d_bad(p4d));
pudp = pud_set_fixmap_offset(p4dp, addr); ------ (2)
do {
pud_t old_pud = READ_ONCE(*pudp);
next = pud_addr_end(addr, end); --------(3)
/*
* For 4K granule only, attempt to put down a 1GB block
*/
if (use_1G_block(addr, next, phys) &&
(flags & NO_BLOCK_MAPPINGS) == 0) {
pud_set_huge(pudp, phys, prot); ------------- (4)
/*
* After the PUD entry has been populated once, we
* only allow updates to the permission attributes.
*/
BUG_ON(!pgattr_change_is_safe(pud_val(old_pud),
READ_ONCE(pud_val(*pudp))));
} else {
alloc_init_cont_pmd(pudp, addr, next, phys, prot,
pgtable_alloc, flags); ---------- (5)
BUG_ON(pud_val(old_pud) != 0 &&
pud_val(old_pud) != READ_ONCE(pud_val(*pudp)));
}
phys += next - addr;
} while (pudp++, addr = next, addr != end);
pud_clear_fixmap();
}
(1) 判断当前的pgd页表项内容是否为空,如果为空就说明下一级pud页表为空,需要动态分配下一级pud页表。这里用memblock来分配内存(512个页表项), 然后通过pgd_populate建立pgd entry和PUD页表内存的关系。
(2) 获取相应的pud页表项。
(3) 和pgd一样,这里的(addr, end)也可能对应不止一个pud entry, 所以以PUD_SIZE为步长, 循环填充pud entry.
(4) 判断是否使用1G大小的block进行映射? 如果是的话,一个PUD页表项可以完成1G大小的地址映射,就不需要PMD和PTE的页表了。
(5) 如果不能进行block映射,那么通过alloc_init_cont_pmd()进行下一级页表的映射。
alloc_init_cont_pmd和上面的步骤都比较类似,就不重复赘述。
现在直接跳到pte页表的映射
alloc_init_cont_pte
static void alloc_init_cont_pte(pmd_t *pmdp, unsigned long addr,
unsigned long end, phys_addr_t phys,
pgprot_t prot,
phys_addr_t (*pgtable_alloc)(int),
int flags)
{
unsigned long next;
pmd_t pmd = READ_ONCE(*pmdp);
BUG_ON(pmd_sect(pmd)); ----------------- (1)
if (pmd_none(pmd)) { ------------------ (2)
phys_addr_t pte_phys;
BUG_ON(!pgtable_alloc);
pte_phys = pgtable_alloc(PAGE_SHIFT);
__pmd_populate(pmdp, pte_phys, PMD_TYPE_TABLE);
pmd = READ_ONCE(*pmdp);
}
BUG_ON(pmd_bad(pmd));
do {
pgprot_t __prot = prot;
next = pte_cont_addr_end(addr, end); ---------- (3)
/* use a contiguous mapping if the range is suitably aligned */
if ((((addr | next | phys) & ~CONT_PTE_MASK) == 0) &&
(flags & NO_CONT_MAPPINGS) == 0)
__prot = __pgprot(pgprot_val(prot) | PTE_CONT); ------(4)
init_pte(pmdp, addr, next, phys, __prot); -------------(5)
phys += next - addr;
} while (addr = next, addr != end);
}
(1) 如果已经建立section mapping, 直接bug_on。 section mappings是内核启动阶段涉及到的映射,执行到这里应该不存在了。
(2) 如果pmd页表项的内容为空,说明下一级pte页表不存在,需要动态分配512个pte页表项。然后通过pmd_populate设置pmd页表项和pte页表内容的关系。
(3)以PAGE_SIZE的大小为步长,循环设置pte页表项
(4)虚拟地址和物理地址内容和大小都是对齐的场景下就会设置连续映射标志
映射连续的物理页面时,kernel image的虚拟地址映射和线性地址映射会启用以下连续范围的大小
granule size | cont PTE | cont PMD |
-------------+------------+------------+
4 KB | 64 KB | 32 MB |
16 KB | 2 MB | 1 GB* |
64 KB | 2 MB | 16 GB* |
(5) init_pte进行pte页表内容的填充
init_pte()
static void init_pte(pmd_t *pmdp, unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot)
{
pte_t *ptep;
ptep = pte_set_fixmap_offset(pmdp, addr); ------------------ (1)
do {
pte_t old_pte = READ_ONCE(*ptep);
set_pte(ptep, pfn_pte(__phys_to_pfn(phys), prot)); --------- (2)
/*
* After the PTE entry has been populated once, we
* only allow updates to the permission attributes.
*/
BUG_ON(!pgattr_change_is_safe(pte_val(old_pte),
READ_ONCE(pte_val(*ptep))));
phys += PAGE_SIZE;
} while (ptep++, addr += PAGE_SIZE, addr != end);
pte_clear_fixmap();
}
(1)将pte表映射到fixmap,并找到与虚拟地址addr对应的pte页表项
(2)循环遍历进行pte页表项的设置。通过__phys_to_pfn将物理地址转化为物理页帧号pfn, 然后和prot标记(设置物理页的读写属性)组合成PTE页表的entry结构。然后将组合后的pte entry的物理地址写入到对应的pte表项中
总体函数的调用过程如下图所示:
小结
从上面的代码流程分析,可以看出pgd/pud/pmd/pte的页表项虽然保存的都是物理地址,但是上面pgd/pud/pmd/pte的计算分析都是基于虚拟地址。
根据paging_init的步骤,在捋一下虚拟地址到物理地址的转换。
假设内核需要访问虚拟地址virt_addr对应的物理地址为phys上的内容:
- 通过存放内核页表的寄存器ttbr1得到swapper_pg_dir页表的物理地址, 然后转换成pgd页表的虚拟地址;
- 根据virt addr计算对应的pgd entry(pgd页表的地址 + virt_addr计算出的offset), PGD entry存放的是PUD页表的物理地址,然后转化成PUD页表基地址的虚拟地址;
- PUD和PMD的处理过程类似;
- 最后从PMD entry中找到PTE 页表的虚拟地址,根据virt addr计算得到对应的pte entry. 从pte entry中得到phys所在的物理页帧地址;
- 加上根据virt addr计算得到的偏移后得到virt对应的物理地址
(图片来自https://www.codenong.com/cs105984564/)
参考资料
- http://jake.dothome.co.kr/map64/
- 内存管理源码分析-内核页表的创建以及索引方式(基于ARM64以及4级页表)
- 内存初始化代码分析(三):创建系统内存地址映射