浅析线性地址到物理地址的转换
一. 概念介绍:
1.线性地址(linear address)(也称虚拟地址virtual address):是一个32位无符号整数,用来表示高达4GB的地址。
2.物理地址(physical address):实际地址。
3.VM 即虚拟内存 ,PM 即物理内存
4.(1)PGD(Page Global Directory) 即页全局目录。
(2)PUD(Page Upper Directory)即页上级目录。
(3)PMD(Page Middle Directory)即页中间目录。
(4)PT即页面表, PT中的表项称为PTE, 是“Page Table Entry”的缩写。
5.offset 即位移量,偏移量。
二. 模型分析:
Linux从2.6.11开始采用四级分页模型:(为了适用于32位和64位系统),linux目前采用的也是四级分页机制,所以我的讨论也基于此。
1. 线性地址
不管系统采用多少级分页模型,线性地址本质上都是索引+偏移量的形式,甚至你可以将整个线性地址看作N+1个索引的组合,N是系统采用的分页级数。在四级分页模型下,线性地址被分为5部分,如下图1:
图1
在线性地址中,每个页表索引即代表线性地址在对应级别的页表中中关联的页表项。正是这种索引与页表项的对应关系形成了整个页表映射机制。
2. 页表
多个页表项的集合则为页表,一个页表内的所有页表项是连续存放的。页表本质上是一堆数据,因此也是以页为单位存放在主存中的。因此,在虚拟地址转化物理物理地址的过程中,每访问一级页表就会访问一次内存。
3. 页表项
从四种页表项的数据结构可以看出,每个页表项其实就是一个无符号长整型数据。每个页表项分两大类信息:页框基地址和页的属性信息。在x86-32体系结构中,每个页表项的结构图2如下:
图2
这个图是一个通用模型,其中页表项的前20位是物理页的基地址。由于32位的系统采用4kb大小的 页,因此每个页表项的后12位均为0。内核将后12位充分利用,每个位都表示对应虚拟页的相关属性。
不管是哪一级的页表,它的功能就是建立虚拟地址和物理地址之间的映射关系,一个页和一个页框之间的映射关系体现在页表项中。
上图中的物理页基地址是 个抽象的说明,如果当前的页表项位于页全局目录中,这个物理页基址是指页上级目录所在物理页的基地址;如果当前页表项位于页表中,这个物理页基地址是指最 终要访问数据所在物理页的基地址。
三. 转换机制介绍:
如下图3所示,线性地址到物理地址的转换机制结构图:
图3
基本过程如下:
1.从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。
2.第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出(具体位数与平台相关,如果是32系统,则为20位),即页上级页目录的物理基地址。
3.从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。
4.第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。
5.从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。
6.第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。
7.从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。
8.第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。
9.从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。
10.第五次读取内存得到最终要访问的数据。
整个过程是比较机械的,每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。不管是页表还是要访问的数据都是以页为单 位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。
举例分析:
例一:
某任务加载后,在4GB虚拟地址空间创建了一个段,起始地址为0x00800000, 段界限为0x5000,字节粒度。当前任务执行时,段寄存器DS指向该段。又假设执行了下面一条指令: mov edx, [0x1050],问此时对应的物理地址是多少?
答:此时,段部件会输出线性地址0x00801050。在没有开启分页机制时, 这就是要访问的物理地址。但现在开启了分页机制,所以这是一个下虚拟地址,要经过页部件转换,才能得到物理地址。
如下图4所示,处理器的页部件专门负责线性地址到物理地址的转换工作。它首先将段部件送来的32位线性地址分为3段,分别是高10位,中间10位,低12位。高10位是页目录的索引,中间10位是页表的索引,低12位则作为页内偏移量来用。
图4
当前任务页目录的物理地址在处理器的CR3寄存器中,假设它的内容为0x00005000 。段管理部件输出的线性地址是0x00801050 。其二进制的形式如图中给出。高10位是十六进制的0x002。它是页目录表内的索引,处理器将它乘以4(因为每个目录项4字节)。作为偏移量访问页目录。最终处理器从物理地址00005008处取得页表的物理地址0x08001000。
线性地址的中间10位为0x001,处理器用它作为页表索引取得页的物理地址。将该值乘以4,作为偏移量访问页表。最终,处理器又从物理地址08001004处取得页的物理地址,这就是我们一直努力寻找的那个页。
页的物理地址是0x0000C000,而线性地址的低12位是数据所在的页内偏移量,故处理器将它们相加,得到物理地址0x0000C050,这就是线性地址0x00801050所对应的物理地址,要访问的数据就在这里。
当任务加载时,操作系统先创建虚拟的段,并根据段地址的高20位决定它要用到哪些页目录项和页表项。然后,寻找空闲的页,将原本应该写入段中的数据写到一个或者多个页中,并将页的物理地址填写到相对应的页表项中。只有这样做了,当程序运行的时候,才能以相反的顺序进行地址变换,并找到正确的数据。
例二:
通过以下的程序来分析虚拟内存到线性地址再到物理内存的映射,我们还以X86为例:
#include <stdio.h>
int greeting(){
printf("Hello world!/n");
return 0;
}
int
main (){
greeting();
return 0;
}
编译该程序,并查看反汇编文件:
这里我们主要看main和greeting的调用:
第一步:通过页目录表找到页面表
hello程序执行后,调用函数greeting,实验楼环境是64位的系统,为了方便我们将前32位为0的不看。这里的虚拟地址也就是线性地址为0x0040052d
call 0x0040052d <greeting>
分解后的结果是:
0000 0000 0100 0000 0000 0101 0010 1101
第1个段位(高10位):
0000 0000 01
对映十进制的1,也就是在页目录表的偏移1找到其页面表的物理地址,也就是页面表的指针,它的低12位是0。
第二步:通过页面表找到页的起始物理地址高
接下来是线性地址的第二个段位(中间10位):
00 0000 0000
对映十进制的0,,也就是在刚才找到的页面表的偏移0,找到目标页的起始物理地址,高20位有效的地址,低12位填充为0.
第三步:得到最终的物理地址
通过找到的页起始物理地址,加上线性地址的第三个段位的偏移地址得到最终的物理地址.
例如:
第三个段位:0101 0010 1101
对映16进制为0x52d。
如果目标页的起始物理地址为:0x740000,那么最终的物理地址就是:
0x740000+0x52d=0x74052d
分析完成。
四. 为什么采用分页机制:
其实它的主要目的在于实现虚拟存储器。分页是通过内存控制单元(MMU)进行的。分页使得线性地址转换为物理地址。在分页中,为了效率起见,将线性地址分成固定长度,称为页,与页长度一致的是页框(物理页)。每个页框包含一个页。把线性地址映射到物理地址的数据结构称为页表。页表存放在主存之中,并在启用分页之前由内核对其进行初始化。
另外,因为分页机制的存在,程序使用的都是线性地址空间,而不再直接是物理地址。这好像是操作系统位应用程序提供了一个不依赖于硬件(物理内存)的平台,应用程序不必关心实际上有多少物理内存,也不必关心正在使用的是哪一段内存,甚至不必关心某一个地址是在物理内存里面还是在硬盘中。只要像操作系统申请就行,而操作系统全权负责了这其中的转换工作。