一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射

Handpc
发布于 2023-10-11 10:33
浏览
0收藏

4. 多级页表的演进

在开始为大家介绍多级页表之前,笔者这里还是要和大家不断强化几个核心概念,这些概念非常重要,这关系到大家是否能从本质上理解多级页表的设计。

  1. 页表本质上还是一个物理内存页,只不过这个物理内存页比较特殊,里面存放的是 PTE,保存虚拟内存与物理内存的映射关系。既然它是一个普通的物理内存页,那么也会参与内核的调度,既会被内核 swap in 以及 swap out,也会被缓存在 CPU 高速缓存中加速访问。
  2. 在 32 位系统中,一个 PTE 占用 4 个字节,可以映射 4K 的物理内存,一张页表本身占用 4K 的物理内存,可以映射 4M 的物理内存。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

  1. 定位虚拟内存页在进程页表中对应的 PTE 是通过数组的访问方式进行的,虚拟内存地址中包含了其对应的 PTE 在页表中的偏移(页表数组中的 index),所以这就要求每一级页表都必须是连续的,比如上小节中介绍的单级页表,这 1024 张页表必须是连续的物理内存(4M 大小)。
  2. 进程对系统内存的访问具有明显的局部性,在任意时刻,我们只需要为进程分配很少的内存就能保证进程的正常运行。

在强化了这些核心概念之后,我们继续沿着上小节中介绍的单级页表的思路往下捋,接下来我们还是以 4G 的物理内存为例,根据局部性原理我们知道,进程在启动之后的任意时刻都不可能一下子就要访问全部的 4G 物理内存,但是我们需要给进程提供寻址 4G 物理内存的能力,也就是说你先别管我访问不访问,反正 4G 物理内存的寻址能力我是需要的。

“让社会的不良风气吹进来,我可以不收,但你们不能不送”——范德彪

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

所以在单级页表的情况下,我们必须要为进程额外分配 4M 的连续物理内存来存放 1024 张页表,不管进程访问不访问,这 4M 的开销是不能省的。

那么现在我们在拿出一个 4K 的物理内存页作为页表,然后将这个页表放在单级页表的前面,组成一个二级页表的体系,情况会变成什么样呢?

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

之前笔者不断地和大家强调过,页表的本质是一个物理内存页,页表是 PTE  的数组,而 PTE 的本质是指向其映射的一个物理内存页,既然 PTE 可以指向一个普通的物理内存页,那么它也可以指向一个页表。

根据这个思路,二级页表中的一个 PTE 本质上指向的还是一个物理内存页,只不过这个物理内存页比较特殊,它是一张页表(一级页表),一级页表是用来映射真正的物理内存的,一张一级页表可以映射 4M 物理内存。

这也就是说二级页表中的一个 PTE 就可以映射 4M 物理内存,同样的道理,二级页表中也包含了 1024 个 PTE,所以一张二级页表就可以映射 4G 的物理内存。

虽说二级页表和一级页表本质上都是一样的,它们都是一个物理内存页,但是我们习惯上将二级页表叫做页目录表,用来做一级页表的索引,就好像书中的目录一样,二级页表中的 PTE  我们习惯上称为做页目录项 (Page Directory Entry, PDE)。

因为一张页目录表就可以映射 4G 的物理内存了,所以在二级页表的情况下,我们只需要在进程启动的时候额外为它分配 4K 的连续物理内存就可以了,这相比单级页表下,需要为每个进程额外分配 4M 的连续物理内存节省了非常多宝贵的内存资源。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

但进程运行起来肯定会访问内存对吧,要访问内存就需要有映射,在运行过程中光有一张页目录表肯定是不够的,根据程序局部性原理,进程在运行中的任意时刻,只会访问很小一块的内存,比如这时进程需要访问 4K 的物理内存(一个物理内存页),在二级页表情况下,内核会本着你访问多少,我映射多少的原则来进行内存映射,下面我们来一起看看二级页表下的映射过程并与一级页表对比下内存消耗。

当前系统中,进程只有一张页目录表,页目录表里的 PDE 没有映射任何东西,这时进程需要访问一个物理内存页,而对物理内存页的映射任务主要是在一级页表的 PTE 中,所以现在首要的任务就是建立一张一级页表出来,并用页目录表索引起来。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

在二级页表的情况下,内核只需要一张 4K 的页目录表和一张 4K 的一级页表总共 8K 的内存就可以支持进程访问一个 4K 物理页面了,而根据程序的空间局部性原理,在不久的将来,进程只会访问与该物理内存页临近的页面,所以事实上,即使进程访问 4M 的内存,依然只需要一张 4K 的页目录表和一张 4K 的一级页表就可以满足了。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

当进程需要访问下一个 4M 的物理内存时,这时候第一个一级页表已经映射满了,那就需要再创建第二张页表用来映射下一个 4M 的物理内存,当然了,第二张页表依然需要索引在页目录表的 PDE 中。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

这时候内核就需要一张页目录表和两张一级页表共 12K 额外的物理内存来映射,这依然比单级页表的 4M 连续物理内存开销小很多。

同理,随着进程一个 4M 接着一个 4M 物理内存的访问,在极端的情况下整个页目录表都被映射满了,这时候内核就需要 4K(页目录表)+ 4M(1024张一级页表)的额外内存来保存映射关系了,这种情况下看起来会比单级页表下的 4M 内存开销大了那么一点点,但这种属于极端情况,非常少见,极大部分情况下还是比单级页表开销少很多很多的。

而且在二级页表体系下,上面极端情况中的这 1024 张一级页表不需要是连续的,因为我们只需要保证顶级页表(这里指页目录表)是连续的就可以了,通过页目录表中的 PDE 可以唯一索引到一张一级页表的起始物理内存地址,而页表内肯定是连续的 4K 物理内存,所以依然可以通过数组的方式索引到一级页表中的 PTE,近而找到其映射的物理内存页面。

除此之外二级页表体系还有一个优势,就是当内存紧张的时候,那些不经常使用的一级页表可以被 swap out 到磁盘中,当进程再次访问到该页表映射的物理内存时,内核在将页表从磁盘中 swap in 到内存中。当然了,顶级页表(这里指页目录表)必须是常驻内存的,不允许 swap 。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

既然页表的本质是一个物理内存页,那么同理,进程经常访问的那些页表也会被缓存到 CPU 高速缓存中加速下一次的访问速度。

在本小节中我们主要揭露多级页表的本质,除了二级页表之外,根据同样的道理,也会有三级页表,四级页表,Linux 内核甚至还支持五级页表,无论页表有多少级,但是都逃脱不了本小节中介绍的本质。本质的原理我们清楚了之后,下面我们就来看下多级页表具体的工作过程吧~~~

4.1 二级页表

现在我们对单级页表体系下的虚拟内存的寻址过程已经非常熟悉了,那么多级页表体系下的虚拟内存寻址过程也是一样的,都逃脱不了前边为大家介绍的页表本质。在引入二级页表寻址过程之前,我们在来回顾下单级页表寻址的本质核心逻辑:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

  1. 首先无论是几级页表,它们通过虚拟内存寻址的本质就是定位虚拟内存页对应在页表中的 PTE,然后通过 PTE 找到其映射的具体物理内存页。
  2. 页表的本质是一个物理内存页,其中包含了 1024 个 PTE,每个 PTE 可以映射一个具体的物理内存页,PTE 中保存了物理内存页的起始地址,进程地址空间中的一个虚拟内存页对应页表中的一个 PTE。因为在内核中是按照页为单位进行内存映射的。
  3. 在单级页表体系下,前面提到的 1024 张一级页表背后是通过连续的 4M 物理内存保存的,既然是连续的,那么我们可以把单级页表看做一个大的 PTE 数组,只要我们知道了单级页表的起始物理内存地址以及虚拟内存页对应 PTE 在单级页表(1024 张一级页表)中的 index,那么就可以定位到 PTE 了。

这里的单级页表起始物理内存地址指的就是 1024 张一级页表中,第一张页表的起始物理内存地址。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

  1. cr3 寄存器保存了顶级页表的起始物理内存地址,顶级页表随着进程的创建而创建,保存在进程 mm_struct->pgd,当进程被 CPU 调度的时候,会伴随着进程上下文切换,其中就会将 mm_struct->pgd 转换为物理内存地址并加载到 cr3 寄存器中。

这里的顶级页表指的就是单级页表(1024 张一级页表)

  1. 无论在几级页表体系下,进程虚拟内存空间中的虚拟内存地址格式在设计上总共分为两大部分,一部分是用来定位虚拟内存页在页表中对应的 PTE 偏移,另一部分是用来定位物理内存页中要访问的具体字节。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

  1. 单级页表的起始物理内存地址(保存在 cr3 寄存器中)有了,虚拟内存页在单级页表中对应 PTE 的偏移(保存在虚拟内存地址中)有了,通过公式​​页表起始地址 + 页表内偏移 * sizeof(PTE)​​ 就可以定位到虚拟内存页对应的 PTE 了,而 PTE 中保存了映射物理内存页的起始地址,在加上虚拟内存地址中保存的物理内存页内偏移,这样就可以定位到虚拟内存地址对应的物理字节了。

从单级页表演进到二级页表之后,虚拟内存寻址的底层逻辑还是一样的,只不过现在的顶级页表变成了页目录表(Page Directory), cr3 寄存器现在存放的是页目录表的起始物理内存地址。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

通过虚拟内存地址定位 PTE 的步骤由原来的一步变成了现在的两步,因为我们多加了一级页目录表,所以现在需要首先定位页目录表中的 PDE,然后通过 PDE 定位到具体的页表,近而找到页表中的 PTE。

所以在二级页表体系下的虚拟内存地址的格式也就发生了变化,单级页表下虚拟内存地址中只需要保存页表中的 PTE 偏移即可,二级页表下虚拟内存地址还需要多保存一份页目录表中 PDE 的偏移。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

二级页表应用在 32 位系统中,相应的虚拟内存地址由 32 位 bit 组成,在 32 位系统中页目录表中的 PDE 和页表中的 PTE 均占用 4 个字节。而前边我们也介绍过了,页目录表和页表的本质其实就是一个物理内存页,它们分别占用 4K 大小。

因此一张页目录表中有 1024 个 PDE,要寻址这 1024 个 PDE 用 10 个 bit 就可以了,所以在上图中的虚拟内存地址中的 ​​页目录表中 PDE 偏移​​ 部分占用 10 个 bit 位。

同样的道理,一张页表中有 1024 个 PTE, 要寻址这个 1024 个 PTE 也是需要 10 个 bit,上图中虚拟内存地址中的 ​​一级页表中 PTE 偏移​​ 部分也需要占用 10 个 bit 位。

这样一来我们就可以通过虚拟内存地址中的前 10 个 bit 定位到页目录表中的 PDE ,而 PDE 指向的是一级页表的起始物理内存地址,然后我们通过接下来的 10 个 bit 就可以定位到页表中的 PTE,而 PTE 指向的是虚拟内存页最终映射的物理内存页的起始地址。

现在我们找到物理内存页了,那么如何在物理内存页中找到我们要访问的字节呢 ?这就需要上图虚拟内存地址中的最后一部分 ​​物理内存页内偏移​​了,因为一个物理内存页占用 4K 大小,我们用 12 位 bit 就可以寻址内存页中的任意字节了。

这样加起来,刚好可以组成一个 32 位的虚拟内存地址,在我们清楚了二级页表下的虚拟内存地址格式之后,接下来我们就来看下二级页表体系下的寻址过程:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

  1. 当 CPU 访问进程虚拟内存空间中的一个地址时,会先从 cr3 寄存器中拿出页目录表的起始物理内存地址,然后从虚拟内存地址中解析出前 10 bit 的内容作为页目录表中 PDE 的偏移,通过公式​​页目录表起始地址 + 页目录表内偏移 * sizeof(PDE)​​ 就可以定位到该虚拟内存页在页目录表中的 PDE 了。
  2. PDE 中保存了其指向的一级页表的起始物理内存地址,我们在从虚拟内存地址中解析出下一个 10 bit 作为页表中 PTE 的偏移,然后通过公式​​页表起始地址 + 页表内偏移 * sizeof(PTE)​​ 就能定位到虚拟内存页在一级页表中的 PTE 了。
  3. PTE 中保存了最终映射的物理内存页的起始地址,最后我们从虚拟内存地址中解析出最后 12 个 bit,最终定位到虚拟内存地址对应的物理字节上。

现在二级页表下虚拟内存寻址的完整过程笔者就为大家介绍完了,那么我们提到了这么次的 PDE,PTE,它们内部到底长什么样子呢?我们接着往下看~~~

4.1.1.  32 位页表项 PTE

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

在进程的虚拟内存空间中,每一个虚拟内存页在页表中都有一个 PTE 与之对应,在 32 位系统中,每个 PTE 占用 4 个字节大小,其中保存了虚拟内存页背后映射的物理内存页的起始地址,以及进程访问物理内存的一些权限标识位。

PTE  在内核中是用 ​​unsigned long​​ 类型描述的,在 32 位系统中占用 4 个字节:

typedef unsigned long pteval_t;

typedef struct { pteval_t pte; } pte_t;

下面是 PTE 中 32 bit (4 字节) 的布局格式:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

由于内核将整个物理内存划分为一页一页的单位,每个物理内存页大小为 4K,所以物理内存页的起始地址都是按照 4K 对齐的,也就导致物理内存页的起始地址的后 12 位全部是 0,我们只需要在 PTE 中存储物理内存地址的高 20 位就可以了,剩下的低 12 位可以用来标记一些权限位。下面是 PTE 权限位的含义:

​P(0)​​ 表示该 PTE 映射的物理内存页是否在内存中,值为 1 表示物理内存页在内存中驻留,值为 0 表示物理内存页不在内存中,可能被 swap 到磁盘上了。当 PTE 中的 P 位为 0 时,上图中的其他权限位将变得没有意义,这种情况下其他 bit 位存放物理内存页在磁盘中的地址。当物理内存页需要被 swap in 的时候,内核会在这里找到物理内存页在磁盘中的位置。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

当我们通过上述虚拟内存寻址过程找到其对应的 PTE 之后,首先会检查它的 P 位,如果为 0 直接触发缺页中断(page fault),随后进入内核态,由内核的缺页异常处理程序负责将映射的物理页面换入到内存中。

​R/W(1)​​ 表示进程对该物理内存页拥有的读,写权限,值为 1 表示进程对该物理页拥有读写权限,值为 0 表示进程对该物理页拥有只读权限,进程对只读页面进行写操作将触发 page fault (写保护中断异常),用于写时复制(Copy On Write, COW)的场景。

比如,父进程通过 fork 系统调用创建子进程之后,父子进程的虚拟内存空间完全是一模一样的,包括父子进程的页表内容都是一样的,父子进程页表中的 PTE 均指向同一物理内存页面,此时内核会将父子进程页表中的 PTE 均改为只读的,并将父子进程共同映射的这个物理页面引用计数 + 1。

当父进程或者子进程对该页面发生写操作的时候,我们现在假设子进程先对页面发生写操作,随后子进程发现自己页表中的 PTE 是只读的,于是产生写保护中断,子进程进入内核态,在内核的缺页中断处理程序中发现,访问的这个物理页面引用计数大于 1,说明此时该物理内存页面存在多进程共享的情况,于是发生写时复制(Copy On Write, COW),内核为子进程重新分配一个新的物理页面,然后将原来物理页中的内容拷贝到新的页面中,最后子进程页表中的 PTE 指向新的物理页面并将 PTE 的 R/W 位设置为 1,原来物理页面的引用计数 - 1。

后面父进程在对页面进行写操作的时候,同样也会发现父进程的页表中 PTE 是只读的,也会产生写保护中断,但是在内核的缺页中断处理程序中,发现访问的这个物理页面引用计数为 1 了,那么就只需要将父进程页表中的 PTE 的 R/W 位设置为 1 就可以了。

​U/S(2)​​ 值为 0 表示该物理内存页面只有内核才可以访问,值为 1 表示用户空间的进程也可以访问。

​PCD(4)​​ 是 Page Cache Disabled 的缩写,表示 PTE 指向的这个物理内存页中的内容是否可以被缓存再 CPU CACHE  中,值为 1 表示 Disabled,值为 0 表示 Enabled。

​PWT(3)​​ 

当 CPU 修改了高速缓存中的数据之后,这些修改后的缓存内容同步回内存的方式有两种:

  1. Write Back:CPU 修改后的缓存数据不会立马同步回内存,只有当 cache line 被替换时,这些修改后的缓存数据才会被同步回内存中,并覆盖掉对应物理内存页中旧的数据。
  2. Write Through:CPU  修改高速缓存中的数据之后,会立刻被同步回物理内存页中。

​A(5)​​ 表示 PTE 指向的这个物理内存页最近是否被访问过,1 表示最近被访问过(读或者写访问都会设置为 1),0 表示没有。该 bit 位被硬件 MMU 设置,由操作系统重置。内核会经常检查该比特位,以确定该物理内存页的活跃程度,不经常使用的内存页,很可能就会被内核 swap out 出去。

​D(6)​​​ 主要针对文件页使用,当 PTE 指向的物理内存页是一个文件页时,进程对这个文件页写入了新的数据,这时文件页就变成了脏页,对应的 PTE 中 D 比特位会被设置为 1,表示文件页中的内容与其背后对应磁盘中的文件内容不同步了。关于脏页的详细描述,可以回看下笔者之前的这篇文章 ​​《从 Linux 内核角度探秘 JDK NIO 文件读写本质》​​。

​PAT(7)​​ 表示是否支持 PAT(Page Attribute Table) , PAT 的相关内容和本文主题无关,这里就不做过多的介绍了。

​G(8)​​ 设置为 1 表示该 PTE 是全局的,该标志位表示 PTE 中保存的映射关系是否是全局的,什么意思呢,一般来说进程都有各自独立的虚拟内存空间,进程的页表也是独立的 ,CPU 每次访问进程虚拟内存地址的时候都需要进行地址翻译(上一小节介绍的寻址过程),为了加速地址翻译的速度,避免每次遍历页表,CPU 会把经常被访问到的 PTE 缓存在一个 TLB 的硬件缓存中,由于 TLB 中缓存的是当前进程相关的 PTE,所以操作系统每次在切换进程的时候,都会重新刷新 TLB 缓存。

而有一些 PTE 是所有进程共享的,比如说内核虚拟内存空间中的映射关系,所有进程进入内核态看到的都是一样的。所以会将这些全局共享的 PTE 中的 G 比特位置为 1 ,这样在每次进程切换的时候,就不会 flush 掉 TLB 缓存的那些共享的全局 PTE(比如内核地址的空间中使用的 PTE),从而在很大程度上提升了性能。

以上介绍的这些 PTE 相关的权限比特位定义在内核文件​​/arch/x86/include/asm/pgtable_types.h​​ 中:

#define _PAGE_BIT_PRESENT 0 /* is present */
#define _PAGE_BIT_RW  1 /* writeable */
#define _PAGE_BIT_USER  2 /* userspace addressable */
#define _PAGE_BIT_PWT  3 /* page write through */
#define _PAGE_BIT_PCD  4 /* page cache disabled */
#define _PAGE_BIT_ACCESSED 5 /* was accessed (raised by CPU) */
#define _PAGE_BIT_DIRTY  6 /* was written to (raised by CPU) */
#define _PAGE_BIT_PAT  7 /* on 4KB pages */
#define _PAGE_BIT_GLOBAL 8 /* Global TLB entry PPro+ */

// 从 PTE 中提取相应比特位的掩码
#define
#define
#define
#define
#define
#define
#define
#define
#define

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

除此之外,内核还定义了一系列的方法,用于操作 PTE 的相关权限比特位,这些方法三个一组,分别用于相应权限比特位的查询,设置,清除操作,定义在内核文件 ​​/arch/x86/include/asm/pgtable.h​​ 中:

// 查询 PTE 指向的物理内存页是否在内存中
static inline int pte_present(pte_t a)

// 查询内存页是否可写
static inline int pte_write(pte_t pte)
// 设置内存页可写
static inline pte_t pte_mkwrite(pte_t pte)
// 禁止读写
static inline pte_t pte_wrprotect(pte_t pte)

// 查询 PTE 是否是 global 
static inline int pte_global(pte_t pte)
static inline pte_t pte_mkglobal(pte_t pte)
static inline pte_t pte_clrglobal(pte_t pte)

// 查询 PTE 指向的内存页是否是脏页
static inline int pte_dirty(pte_t pte)
static inline pte_t pte_mkclean(pte_t pte)
static inline pte_t pte_mkdirty(pte_t pte)

// 查询 PTE 指向的内存页是否最近被访问过
static inline int pte_young(pte_t pte)
static inline pte_t pte_mkold(pte_t pte)
static inline pte_t pte_mkyoung(pte_t pte)

4.1.2. 32 位页目录项 PDE

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

同 PTE 一样,PDE 在 32 位系统中也是用 ​​unsigned long​​ 类型来描述的,同样也是占用 4 个字节大小。

typedef unsigned long pgdval_t;

PDE  是用来指向一级页表的起始物理内存地址的,而页表的本质是一个物理内存页(4K 大小),因此页表的起始内存地址也是按照 4K 对齐的,后 12 位全部为 0 ,我们可以继续用 PDE 的低 12 位来标记页目录项的权限位:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

这里和页表中 PTE 的权限位不同的是,PDE 中的第 6 个比特位脏页标记位没有了,因为 PDE 指向的是一级页表,页表并不是一个文件页,所以脏页标记在这里就没有意义了。

还有就是 PDE 中的第 8 比特位,Global 全局标记位也没有了,因为 TLB 缓存的 PTE 而不是 PDE,所以不需要设置 Global 标记来防止进程切换导致 TLB flush。

最后一个不同的是 PDE 中的第 7 比特位由 PTE 中的 PAT 标记变为了 PS 标记位。那么这个 PS 比特位在这里是干什么用的呢?

当 PS 标记为 ​​0​​ 的时候,PDE 的映射关系确实如本小节第一张图中所示,PDE 指向一级页表的起始内存地址,这种情况下,PDE 的作用确实是我们前边介绍的页目录项的作用。

但是当 PS 标记为 ​​1​​ 的时候,PDE 就会被内核拿来当做 PTE 使用,不过这个 ”PTE“ 比较特殊,其特殊之处在于该 PDE 会指向一个大页内存,这个物理内存页不是普通的 4K 大小,而是 4M 大小。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

笔者在前面的小节中曾介绍过,在二级页表体系下,页目录表中的一个 PDE 可以映射的物理内存空间是 4M ,既然这样,PDE 也可以直接指向一张 4M 的内存大页。为什么内核还需要支持大页内存呢?

我们都知道 Linux 管理内存的最小单位是 page,每个 page 描述 4K 大小的物理内存,但在一些内存敏感的使用场景中,用户往往期望使用一些巨型大页。

因为这些巨型页要比普通的 4K 内存页要大很多,所以遇到缺页中断的情况就会相对减少,由于减少了缺页中断所以性能会更高。

另外,由于巨型页比普通页要大,所以巨型页需要的页表项要比普通页要少,页表项里保存了虚拟内存地址与物理内存地址的映射关系,当 CPU 访问内存的时候需要频繁通过 MMU 访问页表项获取物理内存地址,由于要频繁访问,所以页表项一般会缓存在 TLB 中,因为巨型页需要的页表项较少,所以节约了 TLB 的空间同时降低了 TLB 缓存 MISS 的概率,从而加速了内存访问。

还有一个使用巨型页受益场景就是,当一个内存占用很大的进程(比如 Redis)通过 fork 系统调用创建子进程的时候,会拷贝父进程的相关资源,其中就包括父进程的页表,由于巨型页使用的页表项少,所以拷贝的时候性能会提升不少。

既然 PS 标记为 1 的情况下,PDE 指向的是一个 4M 的物理大页内存,这种情况下内核就把 PDE 当做一个特殊的 ”PTE“ 使用了,所以 PDE 中的比特位布局又发生了变化,不过大部分还是和 PTE  一样的。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

不过这里笔者还是要向大家特殊说明一下,第 13 到 31 比特位的作用,粗略的从总体来讲这个范围的比特位确实是用来保存 4M 大页的起始内存地址的,所以笔者直接在 ​​31:13​​ 范围内的比特位直接标记成 4M 大页的起始内存地址。

但是进一步细分来说,其实 4M 内存大页的起始地址都是按照 4M 对齐的,也就是说 4M 大页的起始内存地址的后 22 位全部为 0 ,我们只需要用 10 个比特位就可以标记了,事实上,4M 大页的起始内存地址在内核中就是使用 ​​31:22​​ 范围内的比特标记的,剩下的比特用来做内存地址的扩展使用,不过这个和本文主旨无关,笔者就直接忽略了。

和 PTE 一样,PDE 的相关权限比特位也定义在内核文件:​​/arch/x86/include/asm/pgtable_types.h​​ 中:

#define _PAGE_BIT_PSE  7 /* 4 MB page */
// 从 PDE 中提取 PS 比特位掩码
#define

我们可以通过如下位运算,从 PDE 中提取 PS 比特位来确定该 PDE 指向的是一级页表还是 4M 大页。

native_pde_val(pde) & _PAGE_PSE

4.2 四级页表

在 32 位系统中,内核主要采用二级页表体系来进行虚拟内存寻址,但是到了 64 位系统中,二级页表明显就不够用了,因为二级页表最多只能映射 4G 的物理内存空间,而 64 位系统中,进程的虚拟寻址空间是巨大的,进程的用户态需要寻址 128T 的虚拟内存空间,内核态也有 128T 的虚拟内存空间。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

为了能够寻址这么大的虚拟内存空间,内核在 64 位系统中引入了四级页表体系,当我们清楚了二级页表的虚拟寻址过程,四级页表就很简单了,不就是多引入了两级页目录么,前面小节介绍的多级页表的本质还是不变的。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

64 位系统中的四级页表相比 32 位系统中的二级页表来说,多引入了两个层级的页目录,分别是四级页表和三级页表,四级页表体系完整的映射关系如下图所示:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

但是在内核中一般不这么叫,内核中称上图中的四级页表为全局页目录 PGD(Page Global Directory),PGD 中的页目录项叫做 pgd_t,PGD 是四级页表体系下的顶级页表,保存在进程 struct mm_struct 结构中的 pgd 属性中,在进程调度上下文切换的时候,由内核通过 load_new_mm_cr3 方法将 pgd 中保存的顶级页表虚拟内存地址转换物理内存地址,随后加载到 cr3 寄存器中,从而完成进程虚拟内存空间的切换。

上图中的三级页表在内核中称之为上层页目录 PUD(Page Upper Directory),PUD 中的页目录项叫做 pud_t 。

二级页表在这里也改了一个名字叫做中间页目录 PMD(Page Middle Directory),PMD 中的页目录项叫做 pmd_t,最底层的用来直接映射物理内存页面的一级页表,名字不变还叫做页表(Page Table)

由于在四级页表体系下,又多引入了两层页目录(PGD,PUD),所以导致其通过虚拟内存地址定位 PTE 的步骤又增加了两步,首先需要定位顶级页表 PGD 中的页目录项 pgd_t,pgd_t 指向的 PUD 的起始内存地址,然后在定位 PUD 中的页目录项 pud_t,后面的流程就和二级页表一样了。

因此 64 位的虚拟内存地址格式也就随着发生了变化:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

32 位系统中的页目录表,页表和 64 位系统中的页目录表,页表在内核中都是使用一个普通 4K 的物理内存页存储映射关系的,不同的是 64 位系统中的页表中的 PTE 以及页目录表(PGD,PUD,PMD)中的 PDE 都是占用 8 个字节,在内核中都是使用 ​​unsigned long​​ 类型描述:

// 定义在内核文件:/arch/x86/include/asm/pgtable_64_types.h
typedef unsigned long pteval_t;
typedef unsigned long pmdval_t;
typedef unsigned long pudval_t;
typedef unsigned long pgdval_t;

typedef struct { pteval_t pte; } pte_t;

// 定义在内核文件:/arch/x86/include/asm/pgtable_types.h
typedef struct { pmdval_t pmd; } pmd_t;
typedef struct { pudval_t pud; } pud_t;
typedef struct { pgdval_t pgd; } pgd_t;

内核这里使用 struct 结构来包裹 unsigned long 类型的目的是要确保这些页目录项以及页表项只能被专门的辅助函数访问,不能直接访问。

一张页表 4K 大小,页表中的一个 PTE 占用 8 个字节,所以在 64 位系统中一张页表只能包含 512 个 PTE,在内核中使用 ​​PTRS_PER_PTE​​​ 常量来表示一张页表中可以容纳的 PTE 个数,用 ​​PAGE_SHIFT​​ 常量表示一个物理内存页的大小:2^PAGE_SHIFT。

/*
 * entries per page directory level
 */
#define

/* PAGE_SHIFT determines the page size */
#define

要寻址页表中这 512 个  PTE,我们用 9 个 bit 就可以了,因此上图虚拟内存地址中的 ​​一级页表中的 PTE 偏移​​ 占用 9 个 bit 位。而一个 PTE 可以映射 4K 大小的物理内存(一个物理内存页),所以在 64 位的四级页表体系下,一张一级页表可以映射的物理内存空间大小为 2M 大小。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

一张中间页目录 PMD 也是 4K 大小,PMD  中的页目录项 pmd_t 也是占用 8 个字节,所以一张 PMD 中只能容纳 512 个 pmd_t,内核中使用 ​​PTRS_PER_PMD​​​ 常量来表示 PMD 中可以容纳多少个页目录项 pmd_t。因次 64 位虚拟内存地址中的 ​​PMD中的页目录偏移​​ 使用 9 个 bit 就可以表示了。

/*
 * PMD_SHIFT determines the size of the area a middle-level
 * page table can map
 */
#define
#define

而一个 pmd_t 指向一张一级页表,所以一个 pmd_t 可以映射的物理内存为 2M,内核中使用 ​​PMD_SHIFT​​ 常量来表示一个 pmd_t 可以映射的物理内存范围:2^PMD_SHIFT。一张 PMD 可以映射 1G 的物理内存。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

同理我们知道,一张上层页目录 PUD 中可以容纳 512 个页目录项 pud_t,内核中使用 ​​PTRS_PER_PUD​​​ 常量来表示 PUD 中可以容纳多少个页目录项 pud_t。 64 位虚拟内存地址中的 ​​PUD中的页目录偏移​​ 也是使用 9 个 bit 就可以表示了。

/*
 * 3rd level page
 */
#define
#define

内核中使用 ​​PUD_SHIFT​​ 常量来表示一个 pud_t 可以映射的物理内存范围:2^PUD_SHIFT,一个 pud_t 指向一张 PMD,因此可以映射 1G 的物理内存。一张 PUD 可以映射 512G 的物理内存。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

一样的道理,顶级页目录 PGD 中可以容纳的页目录 pgd_t 个数 ​​PTRS_PER_PGD = 512​​​, 64 位虚拟内存地址中的 ​​PGD中的页目录偏移​​​ 也是使用 9 个 bit 就可以表示了,一个 pgd_t 可以映射的物理内存为 2^PGDIR_SHIFT = ​​512 G​​。一张 PGD 可以映射的物理内存为 256 T,可以说是非常非常巨大了。

/*
 * 4th level page in 5-level paging case
 */
#define
#define

通过以上内容介绍,我们就得到了 64 位虚拟内存地址中的比特位布局情况:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

  • PAGE_SHIFT  用来表示页表中的一个 PTE 可以映射的物理内存大小(4K)。
  • PMD_SHIFT 用来表示 PMD 中的一个页目录项 pmd_t 可以映射的物理内存大小(2M)。
  • PUD_SHIFT 用来表示 PUD 中的一个页目录项 pud_t 可以映射的物理内存大小(1G)。
  • PGD_SHIFT 用来表示 PGD 中的一个页目录项 pgd_t 可以映射的物理内存大小(512G)。

这些 XXX_SHIFT  常量在内核中除了可以表示对应页目录项映射的物理内存大小之外,还可以从一个 64 位虚拟内存地址中获取其在对应页目录中的偏移。

比如我们现在需要从一个 64 位虚拟内存地址中获取它在 PGD 中的偏移,可以讲虚拟内存地址右移 PGD_SHIFT 位来得到:

#define

然后我们可以通过 PGD 的起始内存地址加上 ​​pgd_index​​ 就可以得到虚拟内存地址在 PGD 中的页目录项 pgd_t 了。

#define

同样的道理,我们可以将虚拟内存地址右移 PUD_SHIFT 位,并用掩码 ​​PTRS_PER_PUD - 1​​ 掩掉高 9 位 , 只保留低 9 位,就可以得到虚拟内存地址在 PUD 中的偏移了:

​PTRS_PER_PUD - 1​​ 转换为二进制是 9 个 1,用来截取最低的 9 个比特位。

static inline unsigned long pud_index(unsigned long address)
{
 return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);
}

我们通过 pgd_t 获取 PUD 的起始内存地址 + ​​pud_index​​ 得到虚拟内存地址对应的 pud_t:

/* Find an entry in the third-level page table.. */
static inline pud_t *pud_offset(pgd_t *pgd, unsigned long address)
{
 return (pud_t *)pgd_page_vaddr(*pgd) + pud_index(address);
}

根据相同的计算逻辑,我们可以通过 pmd_offset 函数获取虚拟内存地址在 PMD 中的页目录项 pmd_t:

/* Find an entry in the second-level page table.. */
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long address)
{
 return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(address);
}

static inline unsigned long pmd_index(unsigned long address)
{
 return (address >> PMD_SHIFT) & (PTRS_PER_PMD - 1);
}

通过 pte_offset_kernel 函数可以获取虚拟内存地址在一级页表中的 PTE:

static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{
 return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address);
}

static inline unsigned long pte_index(unsigned long address)
{
 return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
}

现在我们已经清楚了内核如何通过一系列的 XXX_offset 方法从虚拟内存地址中提取对应页目录以及页表中的偏移了,有了这些基础之后,接下来我们就来看一下四级页表体系的寻址过程:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

  1. 首先 MMU 会从 cr3 寄存器中获取顶级页目录 PGD 的起始内存地址,然后通过 pgd_index 从虚拟内存地址中截取​​PGD 中的页目录项偏移​​,这样就定位到了具体的一个 pgd_t。
  2. pgd_t 中保存的是 PMD 的起始内存地址,通过 pud_index 可以从虚拟内存地址中截取​​PUD 中的页目录项偏移​​,从而确定虚拟内存地址在 PUD 中的页目录项 pud_t。
  3. 同样的道理,根据 pud_t 中保存的 PMD 其实内存地址,在加上通过 pmd_index 获取到的​​PMD 中的页目录项偏移​​,定位到虚拟内存地址在 PMD 中的页目录项 pmd_t。
  4. 后面的寻址流程就和二级页表一样了,pmd_t 指向具体页表的起始内存地址,通过 pte_index 截取虚拟内存地址在​​一级页表中的 PTE 偏移​​,最终定位到一个具体的 PTE 中,PTE  指向的正是虚拟内存地址映射的物理内存页面,然后通过虚拟内存地址中的低 12 位(物理内存页内偏移),最终确定到一个具体的物理字节上。

4.2.1.  64 位页表项

在 64 位系统中,页表中 PTE 在内核中使用 ​​unsigned long​​ 类型描述,占用 8 个字节:

typedef unsigned long   pteval_t;
typedef struct { pteval_t pte; } pte_t;

这 64 位的 PTE 布局如下:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

这里我们以 36 位物理内存地址(最多 52 位)为例进行说明,首先物理内存页的起始内存地址都是按照 4K 对齐的,所以 36 位物理内存地址的低 12 位全部为 0 ,和 32 位的 PTE 一样,内核可以用这低 12 位来描述 PTE 的权限位,其中 0 到 8 之间的比特位,在 32 位 PTE 和 64 位 PTE 中含义都是一样的,这里笔者不在赘述。

​R(11)​​ 这里的 R 表示 restart,该比特位主要用于 HLAT paging,当遍历到 R 位是 1 的 PTE 时,MMU 会重新从 CR3 寄存器开始遍历页表,这里我们只做简单了解。

本例中的物理内存地址是 36 位的,由于物理内存页都是 4K 对齐的,低 12 位全都是 0 ,因此我们只需要在 PTE 中存储物理内存地址的高 24 位即可,这部分存储在 PTE 中的第 12 到 35 比特位。

​Reserved(51:36)​​​ 这些是预留位,全部设置为 0 。​​Protection(62:59)​​ 这 4 个比特位用于对物理内存页的访问进行控制。

​XD(63)​​ 该比特位是 64 位 PTE 中新增的,32 位 PTE 中是没有的,值为 1 表示该 PTE 所映射的物理内存页面中的数据是可以被执行的。

4.2.2. 64 位页目录项

在 64 位系统中使用的四级页表体系中一共包含了三个层级的页目录,它们分别为:全局页目录 PGD(Page Global Directory),上层页目录 PUD(Page Upper Directory),PMD(Page Middle Directory)。

这三种类型的页目录中的页目录项 PDE 在内核中也是使用 ​​unsigned long ​​类型来描述的,在 64 位系统中占用 8 个字节:

typedef unsigned long   pmdval_t;
typedef unsigned long   pudval_t;
typedef unsigned long   pgdval_t;

typedef struct { pmdval_t pmd; } pmd_t;
typedef struct { pudval_t pud; } pud_t;
typedef struct { pgdval_t pgd; } pgd_t;

64 位 PDE 中的比特位布局如下图所示:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

当 64 位 PDE 的 ​PS(7)​ 

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

当 64 位 PDE 的 PS(7) 比特位为 1 时,该 PDE 指向的就是一个内存大页,对于 PMD 中的页目录项 pmd_t 而言,它指向的是一张 2M 大小的物理内存大页。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

对于 PUD 中的页目录项 pud_t 而言,它指向的是一张 1G 大小的物理内存大页。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

当 64 位 PDE 的 PS(7) 比特位为 1 时,这些页目录项 PDE 就被当做了一个特殊的 ”PTE“ 对待了,因此 PDE 中的比特位布局又就变成了 64 位 PTE 中的样子了。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

为了表述严谨,这里笔者需要特殊说明的一点是,方便让大家容易理解,笔者将第 12 到 35 比特位直接标注为了存储大页内存的地址,但事实上,大页内存的地址并不需要这么多位来存储,因为大页中的内存容量比较大,所以大页个数相对较少,它们的起始内存地址不会特别高,使用小于 24 位的比特就可以存放了,多出来的比特位被用作其他目的,但是这些都和本文主旨无关,笔者就直接忽略掉了。

内核当然也会提供一系列的辅助函数来对页目录进行操作:

  • pgd_alloc,pud_alloc,pmd_alloc 负责创建初始化对应的页目录。
  • mk_pgd,mk_pud,mk_pmd,mk_pte 用于创建相应页目录项和页表项,并初始化上述比特位。
  • 以及提供相关 pgd_xxx,pud_xxx,pmd_xxx 等形式的辅助函数,用于对相关比特位的增删改查操作。

5. CPU 的整个寻址过程

本文的重点是要为大家完整清晰地构建出内核中的页表体系,给大家解释清楚虚拟内存是如何与物理内存映射起来的,当我们理解了这些之后,在本小节中,笔者准备带大家一起探秘下,当 CPU 访问一个进程虚拟内存空间中的某个虚拟内存地址之后,操作系统背后到底发生了什么。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

经过本文前边内容的介绍,上图中的这个四级页表的遍历过程,我们已经非常的清楚了,我们可以明显的体会到整个地址翻译的过程需要的步骤还是比较多的,而 CPU 访问内存的操作是非常非常频繁的,如果我们采用内核这种软件的方式对页表进行遍历,效率会非常的差。

而采用一种专门的硬件来对软件进行加速,无疑是一种最简单,最直接有效的优化手段,于是在 CPU 中引入了一个专门对页表进行遍历的地址翻译硬件 MMU(Memory Management Unit),有了 MMU 硬件的加持整个地址翻译的过程就非常的快了。

事实上,上图中展示的四级页表的整个遍历操作均是在 MMU 中进行的:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

经过前边的内容我们知道,这些页目录,页表的本质其实在内核看来都是一张普通的 4K 大小的物理内存页,而物理内存页中经常被访问到的内存数据都是缓存在 CPU 的高速缓存 L1 ,L2,L3 CACHE 中的,这样可以利用局部性原理加速 CPU 对内存的访问。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

所以页目录表和页表中那些经常被 MMU 遍历到的页目录项 PDE,页表项 PTE 均会缓存在 CPU 的 CACHE 中,这样 MMU 就可以直接从 CPU 高速缓存中获取 PDE , PTE 了,近一步加速了整个地址翻译的过程。

当 MMU 拿到一个 CPU 正在访问的虚拟内存地址之后, MMU 首先会从 CR3 寄存器中获取顶级页目录表 PGD 的起始内存地址,然后从虚拟内存地址中提取出 pgd_index,从而定位到 PGD 中的一个页目录项 pdg_t,MMU 首先会从 CPU 的高速缓存中去获取这个 pgd_t,如果 pgd_t 经常被访问到,那么此时它已经存在于高速缓存中了,MMU 直接可以进行下一级页目录的地址翻译操作,避免了慢速的内存访问。

同样的道理,在 MMU 经过层层的页目录遍历之后,终于定位到了一级页表中的 PTE,MMU 也是先会从 CPU 高速缓存中去获取 PTE,如果 PTE 不在高速缓存中,MMU 才会去内存中去获取。获取到 PTE 之后,MMU 就得到了虚拟内存地址背后映射的物理内存地址了。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

在我们引入 MMU 之后,虽然加快了整个页表遍历的过程,但是 CPU  每访问一个虚拟内存地址,MMU 还是需要查找一次 PTE,即便是最好的情况,MMU 也还是需要到 CPU 高速缓存中去找一下的,即便这样开销已经很小了,但是我们还是想近一步降低这个访问 CPU CACHE 的开销,让 CPU 访存性能达到极致,那么该怎么办呢?

既然 MMU 每次都需要查找一次 PTE,那么我们能不能在 MMU 中引入一层硬件缓存,这样 MMU 可以把查找到的 PTE 缓存在硬件中,下次再需要的时候直接到硬件缓存中拿现成的 PTE 就可以了,这样一来,CPU 的访存效率又被近一步加快了。

这个 MMU 中的硬件缓存就叫做 TLB(Translation Lookaside Buffer),TLB 是一个非常小的,虚拟寻址的硬件缓存,专门用来缓存被 MMU 翻译之后的热点 PTE。当我们引入 TLB 之后,整个寻址过程就又有了一些新的变化:

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

当 CPU 将要访问的虚拟内存地址送到 MMU 中翻译时,MMU 首先会在 TLB 中通过虚拟内存寻址查找其对应的 PTE 是否缓存在 TLB 中,如果 cache hit ,那么 MMU 就可以直接获得现成的 PTE,避免了漫长的地址翻译过程。

如果 cache miss,那么 MMU 就需要重新遍历页表,然后获取 PTE 的内存地址,从 CPU 高速缓存或者内存中去查找 PTE,慢速路径下获取到 PTE 之后,MMU 会将 PTE 缓存到 TLB 中,加快下一次获取 PTE 的速度。

当 MMU 获取到 PTE 之后,就可以从 PTE 中拿到物理内存页的起始地址了,在加上虚拟内存地址的低 12 位(物理内存页内偏移)这样就获取到了虚拟内存地址映射的物理内存地址了。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

那么当 MMU 拿到我们最终要访问的物理内存地址之后,又该怎么办呢?

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

  • 当 MMU 获取到最终的物理内存地址,首先会根据物理内存地址到 CPU 高速缓存中去查找数据,如果 cache hit,整个访存操作快速结束。
  • 如果 cache miss,那么 MMU 会将物理内存地址放到系统总线上传输,随后 IO bridge 会将系统总线上传输的地址信号传递到存储总线上。
  • 内存中的存储控制器感受到存储总线上的地址信号之后,会将物理内存地址从存储总线上读取出来。并根据物理内存地址定位到具体的存储器模块,随后解析物理内存地址从  DRAM 芯片中取出对应物理内存地址里的数据。

一步一图带你构建 Linux 页表体系 —— 详解虚拟内存如何与物理内存进行映射-鸿蒙开发者社区

关于 DRAM 芯片的具体访问细节,感兴趣的读者朋友可以回看下笔者之前文章 ​​《一步一图带你深入理解 Linux 虚拟内存管理》​​ 的 ”8. 到底什么是物理内存地址“ 小节。

  • 存储控制器将读取到的数据放到存储总线传输上,随后 IO bridge 将存储总线上的数据信号转换为系统总线上的数据信号,然后继续沿着系统总线传递。
  • CPU 芯片感受到系统总线上的数据信号之后,将数据从系统总线上读取出来并拷贝到寄存器中,随后通过 ALU  完成计算。

总结

本文笔者通过页表体系这条主线脉络,为大家串讲了一下之前介绍的虚拟内存管理以及物理内存管理的相关内容,在我们回顾完虚拟内存管理和物理内存管理之后,随后我们引出了虚拟内存如何与物理内存进行映射这个问题,并在这个过程中为大家揭露了页表的本质。

在我们清楚了页表的本质之后,笔者又沿着页表体系的演进这条主线,对单级页表,二级页表,四级页表展开了介绍,其中花了一定的篇幅为大家详细的介绍了 32 位和 64 位页表项以及页目录想的比特位布局,让大家真真实实的看到了页表项和页目录项到底长什么样子。

在这个基础之上,笔者又对虚拟内存地址格式的组成进行了详细的剖析,并深入到内核中,带着大家梳理了内核是如何从虚拟内存地址中提取对应页目录以及页表中的偏移的,在这些基础之上详细介绍了页表的整个遍历过程。

在本文的最后,笔者带大家又梳理了一遍 CPU 寻址的完整过程,对前边的知识内容做一个串联回顾。

到这里页表相关的知识内容,笔者就为大家介绍完了,感谢大家的收看,我们下篇文章见~~~~~~


文章转载自公众号:bin的技术小屋

分类
标签
已于2023-10-11 10:33:32修改
收藏
回复
举报
回复
    相关推荐