仓库源文

.. Kenneth Lee 版权所有 2021

:Authors: Kenneth Lee :Version: 1.0 :Date: 2021-06-03

Linux内核页表


本文分析Linux内核页表的概念空间。

页表的原理是分级描述页到物理地址的对应关系。

如果一个va对应一个pa,那么页表就需要地址空间那么多项,每项至少包含一个va和一个 pa,这空间本身内存还大,这肯定不现实,所以页表(严格来说是地址映射表)的核心设 计逻辑是怎么省空间。

第一个方法当然是页,一个va对4K个pa,这就省了不少了,如果对64K,就更少了。这是最 基本的方法。

第二个方法是目录,这其实没有省空间,但它解决了稀疏化的问题:如果某些空间的va地 址我没有用,我能不能不留映射表在那个区域上?所以,把va地址分成多段,分段来描述, 不在段中的地址就不用分配空间了。

比如RISCV的Sv39,VA地址分配:::

63 38 29 20 11 0 +-----------+-----------+-----------+-----------+-----------+ | NA | VPN2 | VPN1 | VPN0 | offset | +-----------+-----------+-----------+-----------+-----------+

这个地址分配不使用整个可达的64位地址空间,放弃掉63-39位,仅使用低端512G的范围。 把这512G分成512份,每份1G,VPN2就可以寻址任何一个1G,如果这个1G的空间没有人用, 就不用分配下一级的页表了,这就省下了一大片的空间。反过来说,如果这个页表有人用 ,其实一点空间都没有省,还因为分了多段,浪费更多的空间。

所以,本质上分多少级目录,是稀疏管理的需要,是个根据运行数据经验的选择,很多平 台支持不同的页大小,不同的页表分级,都是为了解决这个“不同应用不用配置”的问题。

va->pa映射需要一一对应,但pa空间和va空间不需要一一对应的。还是用这个Sv39为例, 它的pa是这样定义的:::

63 55 29 20 11 0 +-----------+-----------+-----------+-----------+-----------+ | NA | PPN2 | PPN1 | PPN0 | offset | +-----------+-----------+-----------+-----------+-----------+

PPN2比VPN2大得多,这一点问题没有,因为你又不是只有一个进程,进程最多用512G内存, 很多的进程仍可以用完这64T的物理内存啊,只要我最后的PTE里面,每个VPN都能找到对应 的PPN就可以了。

现在看Linux对这个的支持。

每个平台有不同的页表配置,但原理都是基于VPN的数组,Linux内核就把这个抽象为一组数据。 比如,如果我们有4级页表,我们应该有4级的数组,像这样:

.. code-block:: c

pt_l2 = pt_l3[VPN3].ppn pt_l1 = pt_l2[VPN2].ppn pt_l0 = pt_l1[VPN1].ppn pte = pt_l0[VPN0].ppn

最后的pte的ppn拼上offset就是确切的物理地址了。

这种东西,其实直接用1,2,3,4来表示是最简洁的,只是Linux最早是在x86上实现的,用的 都是x86的语言习惯,所以它把这些名字换了,L3, L2, L1, L0的页表项分别叫做pgd, pud, pmd和pte。换句话说,L3页表是pgd的数组,L2是pud的数组,……如此类推。

如果有5级,名字就又不够用了,这时就只好用数字了,这个叫p4d,插到pgd和pud之间( 因为pgd名字是global,必须就是第一级)。所以,Linux内核中的页表项的表示是pgd, p4d, pud, pmd和pte。一些平台(比如x86),页目录和pte的内容是不同的,部分平台(比 如riscv),两者其实是一样的。

为了让4级页表和5级页表的代码可以通用,Linux的代码假设总共就是5级,在4级的平台上 ,它使能一个逻辑,叫__PAGETABLE_P4D_FOLDED,这种模式下,从pgd就p4d,得到的就是 pgd本身,这样,后面引用p4d求后面的页表,就是从pgd开始的了。

然后我们看看基于这些表建立的概念:

我们用XXX表示一级页表项的名字,比如p4d, pgd等,YYY表示它上一级的页表项的名字, XXX[]表示这个页表项组成的页表。

其中:

XXX_val(XXX)/__XXX(val) 把XXX当作值使用和把值当作XXX来使用。

XXX_none(XXX); 检查XXX项是否有效,也就是它的下一级页表是否被分配了。

XXX_present/bad(XXX); 检查XXX指向的页是否存在(有没有被交换出去),present和bad互相取反。 none和bad很相似,但none表示这个值压根就没有填,下一级页表就没有分配,而 bad表示这个值无效,但下一级页表是存在的。

XXX_leaf(XXX); 检查XXX是否是最后一级页表的页表项

XXX_offset(YYY, va); 求va在这个页表上的那个项目的指针。 注意了,不是求偏移,而是求XXX自身。

set_XXX(XXX*, XXX) 在一个XXX[n]上设置一个XXX。

clear_XXX(XXX*) 在一个XXX[n]上清除一个XXX,但不会释放对应的内存。

XXX_alloc(mm, YYY, va); 分配XXX[] 这个函数会考虑在核间同步的问题,所以你不要直接用他们操作和IO相关的页表。 这也意味这,这组函数不是给IO或者IOMMU等子系统用的。

pfn_XXX(XXX, prot) 从pfn求XXX

XXX_pfn(pfn) 从XXX球pfn

XXX_page_vaddr(XXX) 从XXX求下一级页表的虚拟地址

pfn_pte(pte)/pfn_pte(pfn, prot) 从pte求pfn(包括所有层级连在一起的结果),或者反过来。

pte_read/write/exec/hugh/dirty...(pte) 检查pte属性。

pte_mkread/mkwrite/mkexec/mkhugh/mkdirty...(pte) 设置pte属性。