乐趣区

关于linux:理论实例带你掌握Linux的页目录和页表

摘要:操作系统在加载用户程序的时候,不仅仅须要调配物理内存,来存放程序的内容;而且还须要调配物理内存,用来保留程序的页目录和页表。

本文分享自华为云社区《Linux 从头学 15:【页目录和页表】- 实践 + 实例 + 图文的最齐全、最接地气详解》,作者:道哥。

在 x86 零碎中,为了可能更加充沛、灵便的应用物理内存,把物理内存依照 4KB 的单位进行分页。

而后通过两头的映射表,把间断的虚拟内存空间,映射到离散的物理内存空间。映射表中的每一个表项,都指向一个物理页的开始地址。

然而这样的映射表有一个显著的毛病:映射表本身也是需保留在物理内存中的。

在 32 位零碎中,它应用了多达 4MB 的物理内存空间(每个表项 4 个字节,一共有 4G/4K 个表项)。

为了解决这个问题,x86 处理器应用了两级转换:页目录和页表。

这篇文章,咱们就从最根底的底层计算过程动手,把这个最重要的内存管理机制搞定,当前再学习更深刻的知识点时,就会更容易了解了。

1. 页表的拆分过程

在一个 32 位的零碎中,物理内存的最大可示意空间就是 0xFFFF_FFFF,也就是 4GB。

尽管理论装置的物理内存可能远远没有这么大,然而在设计内存管理机制的时候,还是须要依照最大的可寻址范畴来进行设计的。

依照一个物理页 4KB 的单位来划分,4GB 空间能够宰割为 1024 * 1024 个物理页:

在上一篇文章中,应用繁多的映射表来指向这些物理页,导致了映射表本身占据了太多的物理内存空间。

一个用户程序中定义的几个段,可能实际上只应用了很小的空间,齐全用不到 4 GB。

然而依然须要为它调配多达 4GB 的物理内存空间来保留这个映射表,很节约。

为了解决这个问题,能够把这个繁多映射表拆分成 1024 个体积更小的映射表:

  1. 每一个映射表中,只有 1024 个表项,每一个表项依然指向一个物理页的起始地址;
  2. 一共应用 1024 个这样的映射表;

这样一来,1024(每个表中的表项个数) * 1024(表的个数),依然能够笼罩 4GB 的物理内存空间。

这里的每一个表,就称作页表,所以一共有 1024 个页表。

一个页表中一共有 1024 个表项,每一个页表项占用 4 个字节,所以一个页表就占用 4KB 的物理内存空间,正好是一个物理页的大小。

兴许有的小伙伴就开始算账了:一个页表本身占用 4KB,那么 1024 个页表一共就占用了 4MB 的物理内存空间,依然是很多啊?

是的,从总数上看是这样,然而:一个应用程序是不可能齐全应用全副的 4GB 空间的,兴许只有几十个页表就能够了。

例如:一个用户程序的代码段、数据段、栈段,一共就须要 10 MB 的空间,那么应用 3 个页表就足够了,加上页目录,一共须要 16 KB 的空间。

计算过程:

每一个页表项指向一个 4KB 的物理页,那么一个页表中 1024 个页表项,一共能笼罩 4MB 的物理内存;

那么 10MB 的程序,向上对齐取整之后(4MB 的倍数,就是 12 MB),就须要 3 个页表就能够了。

记住上图中的一句话:一个页表,能够笼罩 4MB 的物理内存空间(1024 * 4 KB)。

页表中,每一个表项的格局如下:

留神上面的这几个属性:

P(Present): 存在位。1 – 物理页存在; 0 – 物理页不存在;

RW(Read/Write): 读 / 写位。1 – 这个物理页可读可写; 0 – 这个物理页只可读;

D(Dirty): 脏位。示意这个物理页中的数据是否被写过;

2. 页目录构造

当初,每一个物理页,都被一个页表中的一个表项来指向了,那么这 1024 个页表的地址,应该怎么来治理呢?

答案是:页目录表!

顾名思义:在页目录中,每一个表项指向一个页表的开始地址(物理地址)。

操作系统在加载用户程序的时候,不仅仅须要调配物理内存,来存放程序的内容;

而且还须要调配物理内存,用来保留程序的页目录和页表。

再来算算账:

方才说过:每一个页表笼罩 4MB 的内存空间,那么页目录中一共有 1024 个表项,指向 1024 个页表的物理地址。

那么页目录能笼罩的内存空间就是 1024 * 4MB,也就是 4GB,正好是 32 位地址的最大寻址范畴。

页目录中,每一个表项的格局如下:

其中的属性字段,与页表中的属性相似,只不过它的形容对象是页表。

还有一点:每一个用户程序都有本人的页目录和页表!下文有具体阐明。

3. 几个相干的寄存器

当初,所有页表的物理地址被页目录表项指向了,那么页目录的物理地址,处理器是怎么晓得的呢?

答案就是:CR3 寄存器,也叫做: PDBR(Page Table Base Register)。

这个寄存器中,保留了以后正在执行的那个工作的页目录地址。

每个工作 (程序) 都有本人的页目录和页表,页目录表的地址被记录在工作的 TSS 段中。

当操作系统调度工作的时候,处理器就会找到行将执行的新工作的 TSS 段信息,而后把新工作的页目录开始地址更新到 CR3 寄存器中。

当新工作的指令开始被执行时,处理器在获取指令、操作数据时,操作的是线性地址。

页处理单元就会从 CR3 寄存器中保留的页目录表开始,把这个线性地址最终转换成物理地址。

当然,处理器中还有一个快表,用来放慢从线性地址到物理地址的转换过程。

CR3 寄存器的格局如下:

顺便把官网上的其余几个管制寄存器都贴出来:

其中,CR0 寄存器的最高位 PG,就是开启页处理单元的开关。

也即是说:

当零碎上电之后,刚开始的地址寻址形式始终是 [段: 偏移地址] 的形式。

当启动代码筹备好页目录和页表之后,就能够设置 CR0.PG = 1。

此时,处理器中的页处理单元就开始工作了:面对任何一个线性地址,都要通过页处理单元之后,才失去一个物理地址。

4. 加载用户程序时: 页目录、页表的调配和填充过程

在之前的文章中,介绍过一个用户程序被操作系统加载的全过程,简述如下:

  1. 读取程序 header 信息,解析出程序的总长度,从工作本人的虚拟内存中调配一块足够的间断空间;
  2. 调配一个闲暇物理页,用作程序的页目录,页目录的地址会记录在稍后创立的 TSS 段中;
  3. 应用虚拟内存中的线性地址,调配一个物理页(4 KB),注销到页目录和页表中;
  4. 从硬盘上读取 8 个扇区的数据(每个扇区 512 字节),寄存到方才调配的物理页中;
  5. 检查程序内容是否读取结束:是 - 进入第 6 步;否 - 返回到第 3 步;
  6. 为用户程序创立一些必要的内核数据结构,比方:TSS、TCB/PCB 等等;
  7. 为用户程序创立 LDT,并且在其中创立每一个段描述符;
  8. 把操作系统的页目录中高端地址局部的表项,复制给用户程序的页目录表。

这样的话,所有用户程序的页目录中,高端地址的表项都指向雷同的页表地址,就达到了共享“操作系统空间”的目标。

这里次要聊一下第 3 步,假如用户程序文件在硬盘上的长度是 20 MB,电脑中理论装置的物理内存是 1 GB。

能够先计算一下:页目录中,每一个表项笼罩的空间是 4 MB,那么 20 MB 的数据,须要 5 个表项就能够了。

在初始状态,页目录中的所有表项都是空的,其中的 P 位都是为 0,示意页表不存在。

操作系统首先从虚拟内存中,调配一块 20 MB 的空间,假如从 1 GB(0x4000_0000)的地址处开始吧,这个地址是线性地址。

也就是说把应用程序的文件读取到内存中,从地址 0x4000_0000 开始寄存,向高地址方向增长。

留神:在“平坦”型分段模型下,线性地址等于虚拟地址。

0x4000_0000 = 0100_0000_0000_0000___0000_0000_0000_0000

前 10 位示意该线性地址在页目录中的索引,两头 10 位示意页表中的索引,最初 12 位示意物理页中的偏移地址。

因而,前 10 位就是 0100_0000_00,示意这个线性地址位于页目录中的第 256 个表项:

操作系统发现这个表项中为空,没有指向任何一个页表。

于是就从物理内存中,找一个闲暇的物理页,用作页目录中第 256 个表项指向的页表。

留神:这个物理页是用作页表,而不是用作存储用户程序文件。

假如在物理内存上 128 MB (0x0800_0000)的地址处,找到一个闲暇的物理页,用作这个页表。

把页表中的 1024 个表项全副清空,并且把页表的物理地址 0x0800_0000,注销在页目录中的第 256 个表项中:0x08000(上图黄色局部)。

为什么不是 0x0800_0000?

因为一个物理页的地址肯定是 4KB 对齐的(最初的 12 位全副为 0),所以页目录的表项中只须要记录页表地址的高 20 位即可。

当初,页表也有了,上面就是调配一个物理页来存储程序的内容。

假如在方才那个物理页 (用作页表的那个) 的下面,又找到一个闲暇的物理页,地址是:0x0800_1000。

此时,这个用于存放程序内容的物理页的地址,就须要记录在页表的一个表项中了。

那么应该记录在页表中的什么地位呢?也就是应该注销在哪一个表项中呢?

须要依据线性地址的两头 10 位来确定:

0x4000_0000 = 0100_0000_0000_0000___0000_0000_0000_0000

两头 10 位的全副是 0,阐明索引值就是 0,也就是说页表中的第 0 个表项,保留这个物理页的地址,如下图所示:

一个物理页的地址肯定是 4KB 对齐的(最初的 12 位全副为 0),所以只须要记录物理页地址的高 20 位即可。

用于存储程序文件内容的物理页调配好了,上面就开始从硬盘中读取程序文件的内容了。

一个物理页的大小是 4 KB,硬盘上一个扇区的大小是 512 B,那么从硬盘上间断读取 8 个扇区的数据就能够把一个物理页写满。

方才曾经假如:用户程序文件在硬盘上的长度是 20 MB。

当读取了一个物理页的内容后,通过计算发现用户程序内容还没有读取完,于是持续反复以上流程。

  1. 线性地址减少 4KB:0x4000_1000 = 0100_0000_0000_0000___0001_0000_0000_0000;
  2. 前 10 位没有变,依然是页目录中的第 256 个表项,发现这个表项指向的页表曾经存在了,于是就不必再调配物理页用作页表了;
  3. 调配一个闲暇物理页,用于存放程序内容,假如在 0x0100_4000 处找到一个,把这个地址注销在页表中;

此时,线性地址的两头 10 位的索引值是 1,所以注销在页表中的第 1 个表项。

  1. 从硬盘上读取 8 个扇区的数据,写入这个物理页;

因为页目录中一个表项所笼罩的范畴是 4 MB(也就是一个页表中 1024 个表项所指向的物理页空间的总和)。

所以当读取了 4 MB 的程序内容之后,这个页表中的所有表项就被填满了。

此时,读取的程序内容所占用的【线性地址】空间是:0x4000_0000 ~ 0x403F_FFFF。

上面再持续读取新内容时,就从 0x4040_0000 这个线性地址处开始寄存,读取过程与下面都是一样的:

  1. 确定页目录表项:

0x4040_0000 = 0100_0000_0100_0000___0000_0000_0000_0000,前 10 位的索引值是 257;

  1. 发现 257 这个表项为空,于是调配一个闲暇的物理页,用作页表;
  2. 调配一个物理页,用作存储程序文件的内容,并把这个物理页的地址记录在页表中;

线性地址 0x4040_0000 的两头 10 位的索引值是 0,所以注销在页表的第一个表项中;

前面的过程就不再唠叨了,一样一样的~~

最终的页目录和页表的布局,相似上面这张图:

5. 线性地址到物理地址的查找、计算实例

如果了解了上一个主题的内容,那么局部应该就能够不必再看了,因为它俩是相同的过程,而且查找过程更简略一些。

依然持续咱们的假如:

  1. 用户程序的长度是 20 MB,寄存在虚拟内存 0x4000_0000 ~ 0x4140_0000 (线性地址)这段空间内;
  2. 代码段的长度是 8 MB,从虚拟内存的 0x40C0_0000 处开始寄存;

也就是如下图所示:

当初,用户程序的内容曾经全副读取到内存中了,页目录、页表全副都安顿得当了。

在页目录表中,一共有 5 个表项,正好示意这 20MB 的地址空间。

其中,8 MB 的代码所存储的物理页地址,注销在页目录表中的 259 和 260 这两个表项中(上图右侧的绿色表项)。

指标:处理器在执行代码时,遇到一个线性地址 0x4100_8800,页处理单元须要把它转换失去物理地址。

0x4100_8800 = 0100_0001_0000_0000___1000_1000_0000_0000

首先,依据线性地址的前 10 位(0100_0001_00),失去它在页目录中的索引值为 260。

这个表项中记录的页表地址为 0x08040,因为页表地址的低 12 位肯定是 bit0,因而这个页表的地址就是 0x0804_0000。

页目录表的开始地址,必定是从 CR3 寄存器获取的;

而后,依据线性地址的两头 10 位(00_0000___1000),失去页表中的索引值为 8。

这个表项中记录的物理页地址为 0x02004,补上低位的 12 个 bit0,就失去物理页的开始地址是 0x0200_4000。

最初,依据线性地址的最初 12 位(1000_0000_0000),失去它在物理页的偏移量 2048。

也就是说:从物理页的开始地址 (0x0200_4000),偏移 2048 个字节的中央,就是这个线性地址(0x4100_8080) 对应的物理地址(0x0200_4800)。

功败垂成!

点击关注,第一工夫理解华为云陈腐技术~

退出移动版