Linux系统中的物理存储空间和虚拟存储空间的地址范围分别都是从0x00000000到0xFFFFFFFF,共4GB,但物理存储空间与虚拟存储空间布局完全不同。Linux运行在虚拟存储空间,并负责把系统中实际存在的远小于4GB的物理内存根据不同需求映射到整个4GB的虚拟存储空间中。Linux主要工作在保护模式下。80X86从逻辑地址到物理地址变换中经过了两个阶段。第一阶段使用分段机制把程序的逻辑地址变换成处理器可寻址内存空间(称为线性地址空间)中的地址。第二阶段的分页机制把线性地址转换成物理地址。第一阶段的分段变换机制是必须使用的,但是第二阶段的分页机制是可选择的。如果没有开启分页机制,那么分段机制产生的线性地址空间就直接映射到处理器的物理地址空间上。进行研究之前,一定要对概念定义非常清晰,不能混淆:
一、基本概念
1、 物理地址 : 放在寻址总线上的地址,用于内存芯片级内存单元寻址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就将相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的,是地址变换的最终结果地址,物理地址由32位或36位无符号整数表示。
2、 逻辑地址 :是指由程序产生的与段相关的偏移地址部分,每一个逻辑地址都由一个段和偏移量组成。在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel 保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样)。
3、 线性地址: 是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址,是一个32位无符号整数,可以用来表示高达4GB的地址,也就是说,高达4294967296个内存单元,以十六进制表示,0x00000000到oxffffffff。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。
4、 虚拟内存 :是指计算机呈现出要比实际拥有的内存大得多的内存量。因此它允许程序员编制并运行比实际系统拥有的内存大得多的程序。这使得许多大型项目也能够在具有有限内存资源的系统上实现。一个很恰当的比喻是:你不需要很长的轨道就可以让一列火车从上海开到北京。你只需要足够长的铁轨(比如说3公里)就可以完成这个任务。采取的方法是把后面的铁轨立刻铺到火车的前面,只要你的操作足够快并能满足要求,列车就能象在一条完整的轨道上运行。这也就是虚拟内存管理需要完成的任务。 在现在操作系统中,都使用了MMU的存储管理技术,而MMU管理的地址是虚拟地址,虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。有时我们也把逻辑地址称为虚拟地址。因为和虚拟内存空间的概念类似,逻辑地址也是和实际物理内存容量无关的。
完整的内存管理,包含保护和地址变换两个关键部分。80386的工作模式包括实地址模式和虚地址模式(保护模式)。Linux主要工作在保护模式下。 80X86从逻辑地址到物理地址变换中经过了两个阶段。第一阶段使用分段机制把程序的逻辑地址变换成处理器可寻址内存空间(称为线性地址空间)中的地址。第二阶段的分页机制把线性地址转换成物理地址。第一阶段的分段变换机制是必须使用的,但是第二阶段的分页机制是可选择的。如果没有开启分页机制,那么分段机制产生的线性地址空间就直接映射到处理器的物理地址空间上。
一个逻辑地址由两部份组成, 段标识符: 段内偏移量 。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器,如图1所示。索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样, 可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符 ,这句话很关键,说明段标识符的具体作用,每一个段描述符由8个字节组成,如图2所示,与主题最密切的就是 Base字段,她表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。 完全引用书中的一句话,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。这个过程中有几个基本的 概念,一定要理清楚,如段选择符、段描述符、 局部段描述符表、 全局段描述符表。
图1 段选择符
图2 段描述符
图3详细显示了一个逻辑地址是怎样转换成相应线性地址的,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,即 先检查段选择符中的TI字段,以决定段描述符保存在哪一个描述符表中 ,知道当前要转换是GDT中的段(在这种情况下,分段单元从gdtr寄存器中得到GDT的线性基地址),还是LDT中的段(在这种情况下,分段单元从ldtr寄存器中得到GDT的线性基地址),再根据相应寄存器,得到其地址和大小。
2、 由于一个段描述符是8字节长,因此她在GDT或LDT内的相对地址是由段选择符的最高13位的值乘以8得到 ,拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Offset,即偏移地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。图4 逻辑地址转换为线性地址实例,段选择符为0x7B,指向用户数据段,段起始地址为0x00000000,逻辑偏移地址为0x80495B0,最终的线性地址为Base + offset=0x80495B0。
图3 逻辑地址的转换
图4 逻辑地址转换为线性地址实例
参考文献:1、《深入Linux内核架构》;2、《深入理解Linux内核》;3、《Linux内核设计与实现》;4、《Linux内核设计的艺术》;