页表管理
页表管理
本节内容讲解 Chcore 页表管理部分的源码(不包括缺页管理,缺页管理单独讲解),我们将从 Chcore 页表管理的核心数据结构讲起,并进一步解析其页表管理的函数实际实现
核心数据结构
Chcore 中表示页表的核心数据结构如下方源码所示:
1 |
|
共有两种数据结构,分别是:
pte_t
:即页表项,表示一个具体的页表ptp_t
:即页表页,我们可以看见它是由一个pte_t
的数组构成的结构体
我们这里重点看一下 pte_t
的定义——它采用了 bit-fields 和 union 的语法
所谓 bit-fields,即
1 |
|
每个字段后面的数字表示该字段占用的位数,编译器会自动将这些字段打包到一个 u64 中,字段的总位数不能超过基础类型(这里是 u64)的大小,由此我们可以总结该数据结构的特点:
- 使用 union 来表示不同类型的页表项
- 支持 4 种格式:
table
(指向下级页表)、l1_block
、l2_block
(大页)和l3_page
(4KB 页) - 通过 bit-field 精确控制每个控制位的位置
- 允许以不同方式解释同一块内存,可以直接访问原始值或者结构化的字段
这时候还有一个问题,不同架构所用到的页表项是不一样的,所以我们需要一个通用页表项来处理:
1 |
|
它是一个架构无关的页表项抽象,主要作用是提供一个统一的接口来处理不同架构的页表项
在函数实现中,我们会有相应的辅助函数来提供将页表项和通用页表项之间转换的功能,如下例所示
1 |
|
函数功能实现
正如我们的 Lab 文档所提到的,内核启动阶段所做的事情只是配置了一个粗粒度的页表系统。而实际操作系统所需要的页表管理则远不止于此。我们需要一个更细粒度的页表实现,提供映射、取消映射、查询等功能。而这些功能在源码中则以各种接口(接口在 mmu.h
中)呈现,并在 page_table.c
中实现
还是先看看接口是怎么定义的,再来看实现
接口定义
1 |
|
从上到下介绍一遍;
map_range_in_pgtbl
:页表映射函数,又分为内核态与用户态,但是实现逻辑基本是一样的,因此在源码中会用一个 common 辅助函数来实现unmap_range_in_pgtbl
:取消页表映射函数query_in_pgtbl
:页表查询函数mprotect_in_pgtbl
:页表权限修改函数
源码解析
map_range_in_pgtbl
我们先对比一下两个函数的接口以及源码
1 |
|
可以看到,非内核的页表映射函数还多了一个 rss 计数器,它的作用是跟踪用户进程的内存使用情况
再来看其实现,会发现都用到了一个 common 函数:
1 |
|
那么关键就在这个 map_range_in_pgtbl_common
函数,我们学习学习它的源码实现
1 |
|
总体上就是参数检查——计算需要映射的总页数——开 while 循环开始映射
1 |
|
数据结构关系如下:
1 |
|
下面是详细的函数执行逻辑:
- 参数检查,并计算需要映射的总页数,这里的方式是向上取整
- 初始化页表指针,为后面的大循环做准备
- 进入 while 循环,依次获取四级页表的页表页,其本质就是位运算,可以回顾一下机器启动部分关于页表映射的讲解
- 获取到 L3 页表的索引,并尽可能多的去映射,映射时需要设置其页表项字段以及更新 rss 和页表页数组
- 重复 while 循环直到映射完毕,并建立数据和指令同步屏障
这里再来明晰一下 while 循环的作用:因为每个 L3 级别的页表页只能映射 2^9=512 个页表项,因此当映射需求较大的时候就需要多轮循环才能映射完毕
我们假设某次映射需求有 2000 个页表项需要被映射,那么会发生如下事情:
1 |
|
循环会一直执行,直到完成所有映射需求或者遇到错误(如内存不足)
unmap_in_range_pgtbl
还是先上源码
1 |
|
整体逻辑和 map 时候的逻辑相似,只是在 while 大循环里,多了一个“跳过未映射区域”的操作,这样可以避免不必要的页表遍历,减少 unmap 的用时和资源开销
1 |
|
而在 L3 部分真正解除映射的代码,又会涉及到如下操作:
- 判断页面是否有效,有效则更新为无效,并更新 rss 计数器
- 回收掉无效的页表项,即
recycle_pagtable_entry
函数,其实现即用到了之前提到的 kfree 等
同样的,假设我们有解除映射的页表需求,可能的工作流程示例如下:
1 |
|
query_in_pgtbl
和上面两个函数一样,查询函数的实现逻辑是换汤不换药的,但是需要添加相应的特色内容,以满足查询本身的需求
注意后两个参数是输出
1 |
|
1 |
|
整体上除了一级一级页表往下查询+错误处理之外,还添加了一个额外的步骤——支持大页映射查询,如果发现相应的高级页表页是一个大页,那么则直接通过大页基地址+页内偏移得到最终返回值
同样的,如果一路成功到了 L3 页表页,那么说明我们要查询的就是个基本的 4KB 小页,直接同样操作获取物理地址就行。相当于该函数一共支持三种页面大小:
- 1GB (L1 block)
- 2MB (L2 block)
- 4KB (L3 page)
mprotect_in_pgtbl
最后我们再来看看修改页表权限的操作是如何实现的,上源码
1 |
|
看过了上面的源码后,这部分源码就显得很亲切了,和 unmap 如出一辙的跳过未映射区域的操作
最后唯一更改的地方仅仅是 L3 的循环,将之前的 unmap 操作换成了调用 set_pte_flag
函数,即完成了对页表权限的修改
这里的 set_pte_flag
函数已经出镜很多次了,其实就是一个负责设置页表项的各种标志位的一个辅助函数。它把那些繁琐的内容封装在了自己体内,对外仅保留一个简单的接口供其他函数调用。如果感兴趣的同学也可以研究研究其内部实现,这里就不过多赘述了。