目录
环境:ARM64,四级页表
前言
内核空间的页目录表的基地址为swapper_pg_dir,在内核初始化的时候,建立了swapper_pg_dir为基地址的页表,用于fixmap的永久映射区,获取早期的物理地址设备比如设备树等。而后面的线性映射的页表是临时的,在每级页表项映射完成的时候会清除这段映射,借助的地址是fixmap的临时映射区。这是通读内核启动代码时最直观接触到的。
用户空间的页面获取一般是从缺页异常开始的,然后一步步建立页表项。
关于页目录表的代码:
#define pgd_index(addr) (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#define pgd_offset_raw(pgd, addr) ((pgd) + pgd_index(addr))
用户空间页目录表:
#define pgd_offset(mm, addr) (pgd_offset_raw((mm)->pgd, (addr)))
内核空间页目录表:
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
准备工作
看用户空间的页表首先明确一下头文件,减少代码混乱
ARM64的页表定义头文件:arch/arm64/include/asm/pgtable-types.h
二级页表定义项:
#define __ARCH_USE_5LEVEL_HACK
#include <asm-generic/pgtable-nopmd.h>
三级页表定义项:
#define __ARCH_USE_5LEVEL_HACK
#include <asm-generic/pgtable-nopud.h>
四级页表定义项:
#include <asm-generic/5level-fixup.h>
对于四级页表项asm-generic/5level-fixup.h定义了:
#define __ARCH_HAS_5LEVEL_HACK
#define __PAGETABLE_P4D_FOLDED 1
正常情况的页表获取
假设每级表项里都有下级表项的物理地址
由va获取pgd
follow_page_mask:
pgd = pgd_offset(mm, address);
由pgd获取p4d
follow_p4d_mask:
p4d = p4d_offset(pgdp, address);
由p4g获取pud
follow_pud_mask:
pud = pud_offset(p4dp, address);
由pud获取pmd
follow_pmd_mask:
pmd = pmd_offset(pudp, address);
由pmd获取pte
follow_page_pte:
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
缺页异常的L0 L1 L2级页表获取
__handle_mm_fault:
pgd = pgd_offset(mm, address); //一级表项:获取addr所在的pgd
p4d = p4d_alloc(mm, pgd, address);
if (!p4d)
return VM_FAULT_OOM;
//查看下一级pud表项
vmf.pud = pud_alloc(mm, p4d, address);
if (!vmf.pud)
return VM_FAULT_OOM;
这里四级页表 p4d == pgd
#define p4d_alloc(mm, pgd, address) (pgd)
直接看pud_alloc
#define pud_alloc(mm, p4d, address) \
((unlikely(pgd_none(*(p4d))) && __pud_alloc(mm, p4d, address)) ? \
NULL : pud_offset(p4d, address))
假设pgd_none(*(p4d)) 成立,也就是pgd里内容为空,则执行__pud_alloc
#ifndef __PAGETABLE_PUD_FOLDED
int __pud_alloc(struct mm_struct *mm, p4d_t *p4d, unsigned long address)
{
pud_t *new = pud_alloc_one(mm, address);
...
if (!pgd_present(*p4d)) {
mm_inc_nr_puds(mm);
pgd_populate(mm, p4d, new);
} else
pud_free(mm, new);
...
return 0;
}
#endif
static inline pud_t *pud_alloc_one(struct mm_struct *mm, unsigned long addr)
{
return (pud_t *)__get_free_page(PGALLOC_GFP);
}
这里区别于内核建立页表,内核是从memblock中获取一个PAGE_SIZE大小的物理页面。
而到了这里,线性映射建立完毕,不再需要考虑物理内存,直接以page的方式获取页面,前面谈过只谈page,不谈ddr。
这里获取的页面不从高端内存获取。
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page *page;
//清除高端内存分配标志
page = alloc_pages(gfp_mask & ~__GFP_HIGHMEM, order);
if (!page)
return 0;
//返回page的虚拟地址
return (unsigned long) page_address(page);
}
获取到表项后,使用pgd_populate填充。我们也可以发现,用户进程页表建立是先获取下一级表项,然后把下一级表项的物理地址填到当前表项中,有别于内核页表的建立。
其余表项的建立过程相似。
缺页异常的L3页表获取
函数 handle_pte_fault
4级页表比较麻烦了,涉及了缺页异常的核心部分,这里就不讲了。
进程建立时页表项的创建
直接定位到_do_fork函数中copy_process--copy_mm
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm;
tsk->mm = NULL;
tsk->active_mm = NULL;
//----------------------1-----------------------
oldmm = current->mm;
if (!oldmm)
return 0;
//---------------------2-----------------------
if (clone_flags & CLONE_VM) {
mmget(oldmm);
mm = oldmm;
goto good_mm;
}
//---------------------3----------------------
retval = -ENOMEM;
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
pgd在struct mm结构中
1 内核线程:
使用kernel_thread创建的线程属于内核线程,比如init线程,kthreadd线程
noinline void __ref rest_init(void)
{
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
}
----------------------------------------------
~ # ps
PID USER TIME COMMAND
1 root 0:01 {linuxrc} init
2 root 0:00 [kthreadd]
内核线程对应判断条件1,无进程地址空间,默认使用的页目录表为init_mm中的swapper_pg_dir。
2 CLONE_VM
对应vfork创建进程场景,直接使用父进程的mm,那就公用父进程的pgd了。
3 其余情况需要创建pgd
创建pgd:dup_mm -- mm_init -- mm_alloc_pgd。如果pgd大小为4K,直接获取一个page,否则获取一个pgd_cache slab。不管如何,pgd都是新建的和父进程的pgd没有关系和内核页表swapper_pg_dir也没有关系。
mm->pgd = pgd_alloc(mm);
pgd_t *pgd_alloc(struct mm_struct *mm)
{
if (PGD_SIZE == PAGE_SIZE)
return (pgd_t *)__get_free_page(PGALLOC_GFP);
else
return kmem_cache_alloc(pgd_cache, PGALLOC_GFP);
}
复制页表:dup_mm -- mm_init -- dum_mmap -- copy_page_range。copy_page_range中会一级一级创建子进程的页表项,创建方式同缺页异常,一路创建到copy_pte_range。在copy_pte_range获取pte的后,调用copy_one_pte开始复制父进程的pte内容。
整个过程1~3级表项不是父进程的,4级表项是父进程的,也就是父子进程的物理地址相同。
探索进程空间的第一张页表
笔者能力有限暂未找到,你们找到了告诉一下路径~