内核杂谈——用户空间的页表

目录

前言

准备工作

正常情况的页表获取

缺页异常的L0 L1 L2级页表获取

缺页异常的L3页表获取

进程建立时页表项的创建

探索进程空间的第一张页表


环境: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级表项是父进程的,也就是父子进程的物理地址相同。

探索进程空间的第一张页表

笔者能力有限暂未找到,你们找到了告诉一下路径~

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目录 1 引子 2 1.1 上电 2 1.2 BIOS时代 3 1.3 内核引导程序 5 2 内核映像的形成 8 2.1 MakeFile预备知识 9 2.1.1 Makefile书写规则 9 2.1.2 Makefile变量 10 2.1.3 条件判断 14 2.1.4 函数 17 2.1.5 隐含规则 17 2.1.6 定义模式规则 19 2.1 KBuild体系 23 2.1.1 内核目标 24 2.1.2 主机程序 26 2.1.3 编译标志 27 2.2 内核编译分析 28 2.2.1 编译配置 29 2.2.2 寻找第一个目标 32 2.2.3 prepare和scripts目标 38 2.2.4 递归编译各对象 41 2.2.5 链接vmlinux 44 2.2.6 制作bzImage 50 3 实模式下的内核代码 57 3.1 内核映像内存布局 58 3.2 实模式汇编代码header.S 60 3.2.1 无用的bootsect代码 60 3.2.2 初始化头变量hdr 63 3.2.3 准备实模式下C语言环境 64 3.3 实模式代码main函数 69 3.3.1 复制初始化头变量 71 3.3.2 初始化堆 74 3.3.3 确保支持当前运行的CPU 75 3.3.4 设置BIOS的x86模式 76 3.3.5 内存的检测 78 3.3.6 设置键盘属性 81 3.3.7 填充系统环境配置表 82 3.3.8 填充IST信息 83 3.3.9 设置Video模式 83 3.4 实模式代码go_to_proteced_mode函数 91 3.4.1 禁止可屏蔽和不可屏蔽中断 92 3.4.2 打开A20地址线 93 3.4.3 安装临时全局描述符表 99 3.4.4 第一次启动保护模式 101 4 保护模式下的内核代码 107 4.1 32位x86保护模式代码 107 4.1.1 内核解压缩的前期工作 108 4.1.2 解压缩内核 111 4.1.3 第二次启动保护模式 121 4.1.4 第一次启动分页管理 124 4.1.5 初始化0号进程 128 4.2 向start_kernel进发 131 4.2.1 初始化中断描述符表 132 4.2.2 第三次启动保护模式 137 4.2.3 启动x86虚拟机 141 5 走向现代:start_kernel函数 144 5.1 初始化同步与互斥环境 148 5.1.1 屏蔽中断 148 5.1.2 启动大内核锁 152 5.1.3 注册时钟通知链 153 5.1.4 激活第一个CPU 155 5.1.5 初始化地址散列表 160 5.1.6 打印版本信息 161 5.2 执行setup_arch()函数 166 5.2.1 拷贝可用内存区信息 171 5.2.2 获得总页面数 175 5.2.3 着手建立永久内核页表 177 5.2.4 第二次启动分页管理 181 5.2.5 建立内存管理架构 186 5.2.6 添砖加瓦 192 5.3 设置每CPU环境 206 5.4 初始化内存管理区列表 211 5.5 利用early_res分配内存 214 5.6 触碰虚拟文件系统 223 5.7 初始化异常服务 224 5.8 初始化内存管理 230 5.8.1 启用伙伴算法 230 5.8.2 初始化slab分配器 241 5.8.3 初始化非连续内存区 250 5.9 初始化调度程序 251 5.10 初始化中断处理系统 256 5.10.1 设置APIC中断服务 256 5.10.2 初始化本地软时钟 264 5.10.3 软中断初始化 268 5.10.4 初始化定时器中断 271 5.11 走进start_kernel尾声 273 5.11.1 初始化slab的后续工作 273 5.11.2 启动console 275 5.11.3 一些简单的函数 276 5.11.4 校准CPU时钟速度 279 5.11.5 创建一些slab缓存 282 5.12 安装根文件系统 287 5.12.1 创建VFS相关slab缓存 288 5.12.2 安装rootfs 291 5.12.3 安装proc文件系统 296 6 后start_kernel时代 298 6.1 创建1号进程 298 6.2 子系统的初始化 306 6.3 启动shell环境 309

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值