Linux从头学15:【页目录和页表】-理论 实例 图文的最完全、最接地气详解
扫描二维码
随时随地手机看文章
作 者:道哥,10 年嵌入式开发老兵,专注于:C/C 、嵌入式、Linux。关注下方公众号,回复【书籍】,获取 Linux、嵌入式领域经典书籍;回复【PDF】,获取所有原创文章( PDF 格式)。
-
页表的拆分过程
-
页目录结构
-
几个相关的寄存器
-
加载用户程序时: 页目录、页表的分配和填充过程
-
线性地址到物理地址的查找、计算实例
页表
在一个32位的系统中,物理内存的最大可表示空间就是0xFFFF_FFFF,也就是4GB。
一个用户程序中定义的几个段,可能实际上只使用了很小的空间,完全用不到 4 GB。为了解决这个问题,可以把这个单一映射表拆分成1024个体积更小的映射表:但是仍然需要为它分配多达 4GB 的物理内存空间来保存这个映射表,很浪费。
这样一来,1024(每个表中的表项个数) * 1024(表的个数),仍然可以覆盖4GB的物理内存空间。
- 每一个映射表中,只有 1024 个表项,每一个表项仍然指向一个物理页的起始地址;
- 一共使用 1024 个这样的映射表;
计算过程:记住上图中的一句话:一个页表,可以覆盖 4MB 的物理内存空间(1024 * 4 KB)。每一个页表项指向一个 4KB 的物理页,那么一个页表中 1024 个页表项,一共能覆盖 4MB 的物理内存;
那么 10MB 的程序,向上对齐取整之后(4MB 的倍数,就是 12 MB),就需要 3 个页表就可以了。
P(Present): 存在位。1 - 物理页存在; 0 - 物理页不存在;RW(Read/Write): 读/写位。1 - 这个物理页可读可写; 0 - 这个物理页只可读;
D(Dirty): 脏位。表示这个物理页中的数据是否被写过;
页目录
现在,每一个物理页,都被一个页表中的一个表项来指向了,那么这1024个页表的地址,应该怎么来管理呢?
操作系统在加载用户程序的时候,不仅仅需要分配物理内存,来存放程序的内容;再来算算账:而且还需要分配物理内存,用来保存程序的页目录和页表。
相关寄存器
现在,所有页表的物理地址被页目录表项指向了,那么页目录的物理地址,处理器是怎么知道的呢?
当然,处理器中还有一个快表,用来加快从线性地址到物理地址的转换过程。CR3寄存器的格式如下:
加载用户程序时: 物理页分配过程
在之前的文章中,介绍过一个用户程序被操作系统加载的全过程,简述如下:
这里主要聊一下第3步,假设用户程序文件在硬盘上的长度是20 MB,电脑中实际安装的物理内存是1 GB。
- 读取程序 header 信息,解析出程序的总长度,从任务自己的虚拟内存中分配一块足够的连续空间;
- 分配一个空闲物理页,用作程序的页目录,页目录的地址会记录在稍后创建的 TSS 段中;
- 使用虚拟内存中的线性地址,分配一个物理页(4 KB),登记到页目录和页表中;
- 从硬盘上读取 8 个扇区的数据(每个扇区 512 字节),存放到刚才分配的物理页中;
- 检查程序内容是否读取完毕:是-进入第 6 步;否-返回到第 3 步;
- 为用户程序创建一些必要的内核数据结构,比如:TSS、TCB/PCB 等等;
- 为用户程序创建 LDT,并且在其中创建每一个段描述符;
- 把操作系统的页目录中高端地址部分的表项,复制给用户程序的页目录表。
这样的话,所有用户程序的页目录中,高端地址的表项都指向相同的页表地址,就达到了共享“操作系统空间”的目的。
可以先计算一下:页目录中,每一个表项覆盖的空间是 4 MB,那么 20 MB的数据,需要 5 个表项就可以了。在初始状态,页目录中的所有表项都是空的,其中的P位都是为0,表示页表不存在。
注意:在“平坦”型分段模型下,线性地址等于虚拟地址。0x4000_0000 = 0100_0000_0000_0000___0000_0000_0000_0000
一个物理页的地址一定是4KB对齐的(最后的12位全部为0),所以只需要记录物理页地址的高 20 位即可。用于存储程序文件内容的物理页分配好了,下面就开始从硬盘中读取程序文件的内容了。
刚才已经假设:用户程序文件在硬盘上的长度是20 MB。当读取了一个物理页的内容后,通过计算发现用户程序内容还没有读取完,于是继续重复以上流程。
因为页目录中一个表项所覆盖的范围是4 MB(也就是一个页表中1024个表项所指向的物理页空间的总和)。
- 线性地址增加 4KB:0x4000_1000 = 0100_0000_0000_0000___0001_0000_0000_0000;
- 前 10 位没有变,仍然是页目录中的第 256 个表项,发现这个表项指向的页表已经存在了,于是就不用再分配物理页用作页表了;
- 分配一个空闲物理页,用于存放程序内容,假设在 0x0100_4000处找到一个,把这个地址登记在页表中;
此时,线性地址的中间 10 位的索引值是 1,所以登记在页表中的第 1 个表项。
- 从硬盘上读取 8 个扇区的数据,写入这个物理页;
后面的过程就不再唠叨了,一样一样的~~
- 确定页目录表项:
0x4040_0000 = 0100_0000_0100_0000___0000_0000_0000_0000,前 10 位的索引值是 257;
- 发现 257 这个表项为空,于是分配一个空闲的物理页,用作页表;
- 分配一个物理页,用作存储程序文件的内容,并把这个物理页的地址记录在页表中;
线性地址 0x4040_0000 的中间 10 位的索引值是 0,所以登记在页表的第一个表项中;
线性地址到物理地址的变换过程
如果理解了上一个主题的内容,那么部分应该就可以不用再看了,因为它俩是相反的过程,而且查找过程更简单一些。
也就是如下图所示:
- 用户程序的长度是 20 MB,存放在虚拟内存 0x4000_0000 ~ 0x4140_0000 (线性地址)这段空间内;
- 代码段的长度是 8 MB,从虚拟内存的 0x40C0_0000 处开始存放;
页目录表的开始地址,肯定是从 CR3 寄存器获取的;然后,根据线性地址的中间 10 位(00_0000___1000),得到页表中的索引值为8。
------ End ------