理论+实例,带你掌握Linux的页目录和页表保存程序的页目录和页表。本文分享自华为云社区《Linux从头学15:【页目录和页表】-理论 + 实例 + 图文的最完全、最接地气详解》,作者:刀哥。在x86系统中,为了更充分、更灵活地使用物理内存,物理内存以4KB为单位进行分页。然后通过中间映射表,将连续的虚拟内存空间映射到离散的物理内存空间。映射表中的每一项都指向一个物理页的起始地址。但是这样的映射表有一个明显的缺点:映射表本身也需要存储在物理内存中。在32位系统上,它最多使用4MB的物理内存空间(每个条目4字节,总共4G/4K条目)。为了解决这个问题,x86处理器使用了两个级别的转换:页目录和页表。在这篇文章中,我们将从最基本的底层计算过程入手,得到最重要的内存管理机制。当我们后面学习到更深入的知识点时,就会更容易理解。1、页表的拆分过程在32位系统中,物理内存的最大可表示空间为0xFFFF_FFFF,即4GB。虽然实际安装的物理内存可能没有那么大,但是在设计内存管理机制的时候,还是要按照最大的可寻址范围来设计。按照一个物理页4KB为单位划分,4GB空间可以划分为1024*1024个物理页:上一篇中使用单个映射表指向这些物理页导致映射表本身占用过多物理内存空间。用户程序中定义的几个段实际上可能只使用很少的空间,根本不到4GB。但是它仍然需要分配最多4GB的物理内存空间来保存这张映射表,比较浪费。为了解决这个问题,可以将这个单一的映射表拆分成1024个更小的映射表:在每个映射表中,只有1024个表项,每个表项仍然指向一个物理页的起始地址;一共使用了1024个这样的映射表;这样,1024(每张表的条目数)*1024(表数),仍然可以覆盖4GB的物理内存空间。这里的每张表都称为一个页表,所以一共有1024张页表。一个页表一共有1024个表项,每个页表表项占用4个字节,所以一个页表占用4KB的物理内存空间,正好是一个物理页的大小。可能有些朋友开始算账了:一个页表本身就占了4KB,那么1024个页表一共占了4MB的物理内存空间,还是很多?是的,从总量上看,但是:一个应用不可能把所有的4GB空间都用完,也许只有几十个页表就够了。例如:一个用户程序的代码段、数据段、堆栈段一共需要10MB的空间,所以用3个页表就够了,加上页目录,一共需要16KB的空间.计算过程:每个页表项指向一个4KB的物理页,那么一个页表中的1024个页表项总共可以覆盖4MB的物理内存;那么一个10MB的程序,向上对齐四舍五入后(4MB的倍数,就是12MB),需要3个页表。记住上图中的一句话:一个页表可以覆盖4MB的物理内存空间(1024*4KB)。在页表中,每个表项的格式如下:注意以下属性:P(Present):存在位。1-物理页面存在;0——物理页不存在;RW(Read/Write):读/写位。1-这个物理页是可读可写的;0——这个物理页是只可读的;D(Dirty):脏位。表示该物理页中的数据是否已经写入;2、页目录结构现在,每个物理页都由一个页表项指向,那么这1024个页表的地址应该怎么来呢?管理呢?答案是:页目录表!顾名思义:在页目录中,每一个表项都指向一个页表的起始地址(物理地址)。操作系统加载用户程序时,不仅需要分配物理内存来存放程序的内容,还需要分配物理内存来存放程序的页目录和页表。我们再算一算:刚才说了:每个页表占4MB的内存空间,所以页目录中有1024个表项,指向1024个页表的物理地址。那么页目录所能覆盖的内存空间为1024*4MB,也就是4GB,刚好是32位地址的最大寻址范围。在页目录中,每个表项的格式如下:其中的属性字段与页表中的属性类似,只是它的描述对象是页表。还有一点:每个用户程序都有自己的页目录和页表!详情如下。3、几个相关的寄存器现在,所有页表的物理地址都是由页目录项指向的,那么处理器如何知道页目录的物理地址呢?答案是:CR3寄存器,又名:PDBR(PageTableBaseRegister)。在这个寄存器中,保存了当前正在执行的任务的页目录地址。每个任务(程序)都有自己的页目录和页表,页目录表的地址记录在任务的TSS段中。操作系统调度任务时,处理器会找到要执行的新任务的TSS段信息,然后将新任务的页目录起始地址更新到CR3寄存器中。当开始执行新任务的指令时,处理器在获取指令和操作数据时对线性地址进行操作。页处理单元会从保存在CR3寄存器中的页目录表开始,最后把这个线性地址转换成物理地址。当然,处理器中也有一个快速表来加速线性地址到物理地址的转换过程。CR3寄存器的格式如下:顺便贴一下官网其他几个控制寄存器:其中CR0寄存器的最高位PG是打开页面处理单元的开关。也就是说:系统上电后,最开始的地址寻址方式始终是[段:偏移地址]的方式。启动代码准备好页目录和页表后,就可以设置CR0.PG=1。此时处理器中的页处理单元开始工作:面对任何线性地址,都必须有一个物理地址通过页面处理单元后得到。4、加载用户程序时:页目录和页表的分配和填充过程上一篇文章介绍了一个用户程序被操作系统加载的整个过程。简要说明如下:读取程序头信息,解析出程序总长度,从任务自身的虚拟内存中分配足够的连续空间;分配一个空闲的物理页作为程序的页目录,页目录的地址会记录在后面创建的TSS段中;使用虚拟内存线性地址,分配一个物理页(4KB),注册到页目录和页表中;从硬盘中读取8个扇区(每个扇区512字节)的数据,存放到刚刚分配的物理页中;检查节目内容是否被读取:是——转第6步;否——返回第3步;为用户程序创建一些必要的内核数据结构,如:TSS、TCB/PCB等;为用户程序创建LDT,并在其中创建各个段描述符;将操作系统页目录中高端地址部分的表项复制到用户程序的页目录表中。这样,在所有用户程序的页目录中,高端地址项都指向同一个页表地址,达到共享“操作系统空间”的目的。这里主要说说第3步,假设硬盘上用户程序文件的长度为20MB,计算机实际安装的物理内存为1GB。可以先计算一下:在页目录中,每个表项覆盖的空间是4MB,所以20MB的数据需要5个表项。在初始状态下,页目录中的表项全部为空,P位全为0,表示页表不存在。操作系统首先从虚拟内存中分配一个20MB的空间,假设它从1GB(0x4000_0000)的地址开始,这是一个线性地址。也就是说,应用程序的文件被读入内存,从地址0x4000_0000开始存储,向高地址递增。注意:在“平面”类型的分段模型下,线性地址等于虚拟地址。0x4000_0000=0100_0000_0000_0000___0000_0000_0000_0000前10位表示页目录中线性地址的索引,中间10位表示页表中的索引,后12位表示物理页中的偏移地址。因此,前10位是0100_0000_00,说明这个线性地址位于页目录的第256个表项:操作系统发现这个表项为空,不指向任何页表。于是从物理内存中找到一个空闲的物理页,作为页目录中第256项所指向的页表。注意:这个物理页是作为页表使用的,不是用来存放用户程序文件的。假设在物理内存的128MB(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位.分配好存放程序文件内容的物理页后,就会从硬盘中读取程序文件的内容。一个物理页的大小为4KB,硬盘上一个扇区的大小为512B,那么从硬盘连续读取8个扇区的数据就可以填满一个物理页。刚才假设:硬盘上用户程序文件的长度为20MB。读取一个物理页面的内容后,通过计算发现用户程序的内容没有被读取,所以继续重复上面的过程。线性地址增加4KB:0x4000_1000=0100_0000_0000_0000___0001_0000_0000_0000;前10位没有变化,仍然是页目录中的第256个条目。发现这个表项指向的页表已经存在,所以不需要再分配一个物理页作为页表了;分配一个空闲的物理页来存储程序内容,假设在0x0100_4000找到一个,并将这个地址注册到页表中;此时线性地址中间10位的索引值为1,所以它被登记在页表中的第一项。从硬盘读取8个扇区的数据,写入这个物理页;因为页目录中的一个表项覆盖的范围是4MB(即一个页表总和中的1024个表项所指向的物理页空间)。所以当读取到4MB的程序内容时,这个页表中的所有表项都被填满了。此时读取的程序内容占用的【线性地址】空间为:0x4000_0000~0x403F_FFFF。下面继续读取新内容时,从0x4040_0000线性地址开始存储。读取过程同上:确定页目录项:0x4040_0000=0100_0000_0100_0000___0000_0000_0000_0000,前10位索引值为257;found257这个表项是空的,所以分配一个空闲的物理页作为页表使用;分配一个物理页用于存放程序文件的内容,页表中记录该物理页的地址;线性地址中间0x4040_0000的10位索引值为0,因此被寄存在页表的第一项;后面的流程就不再唠叨了,一样的~~最终的页目录和页表的布局类似下图:5、从线性地址到物理地址的查找和计算实例。如果你理解了上一个题目的内容,那么你应该不需要再读一部分,因为它们是相反的过程,查找过程更简单。仍然继续我们的假设:用户程序的长度为20MB,存放在虚拟内存空间0x4000_0000~0x4140_0000(线性地址);代码段长度为8MB,从虚拟内存的0x40C0_0000开始存放;即如下图所示:现在,用户程序的所有内容都已经读入内存,页目录和页表也都安排妥当了。在页目录表中,一共有5个表项,正好代表了20MB的地址空间。其中,8MB代码存储的物理页地址被注册在页目录表中的259和260两个表项中(上图右侧绿色表项)。目标:处理器在执行代码时,遇到线性地址0x4100_8800,页面处理单元需要将其转换为物理地址。0x4100_8800=0100_0001_0000_0000___1000_1000_0000_0000首先,根据线性地址(0100_0001_00)的前10位,其在页目录中的索引值为260。该表项记录的页表地址为0x08040,因为页表地址的低12位必须是bit0,所以这个页表的地址是0x0804_0000。页目录表的起始地址必须从CR3寄存器中获取;然后,根据线性地址(00_0000___1000)的中间10位,在页表中的索引值为8,该表项记录的物理页地址为0x02004,将低12位0相加得到物理页的起始地址为0x0200_4000。最后根据线性地址(1000_0000_0000)的后12位,得到它在物理页中的偏移量2048。也就是说:从物理页的起始地址(0x0200_4000)开始,偏移2048字节就是这个线性地址(0x4100_8080)对应的物理地址(0x0200_4800)。你完成了!点击关注,第一时间了解华为云的新鲜技术~
