逻辑地址、线性地址和物理地址之间的转换

       首先说明一点,本篇的文章是根据自己的理解总结,但是图可能是在已有的博客中截图的,在此对那些对我理解该部分知识提供帮助的博客博主表示感谢!

       在逻辑地址、线性地址和物理地址一节中,已经对逻辑地址、线性地址和物理地址的概念做了详细的讲解。现在在这篇文章中,我们可以详细的对段式、页式、段页式内存管理方式以及三种地址之间的转化做一个详细且深入的说明。

       文章按照由简单到深入的顺序分别对以上三种内存管理方式进行说明。这里以32位系统为例进行举例说明,至于64位系统,其原理是大致类似的。

段式内存管理方式:

       段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。

       在介绍段式内存管理方式之前首先介绍逻辑空间。逻辑空间分为若干个段,其中每一个段都定义了一组具有完整意义的信息,逻辑地址对应于逻辑空间,如(主程序的main())函数,如图1所示。

图 1 逻辑地址空间

       段是对程序逻辑意义上的一种划分,一组完整逻辑意义的程序被划分成一段,所以段的长度是不确定的。

       如图1所示,段式内存管理方式经过段表映射到内存空间。先说明一下段表的概念,可以将段表抽象成一个大的数组集合,数组中的元素是什么呢?就是“段描述符”----用于描述一个段的详细信息的结构。段描述符一般是由8个字节组成,也就是64位。操作系统使用的不同的段描述符如图2所示。


图 2 段描述符

       前面已经说过,将逻辑地址转换成下一个环节的地址(物理地址,不适用分页或者使用分页的线性地址)需要使用段表,而获得段表中一个特定的段描述符需要使用段选择符,这里需要区分的概念就是“段选择符"和“段描述符"。段描述符描述了一个段的详细信息,例如,起始地址(BASE的32位,长度20位),适用于转换下一个环节的地址所需要的详细信息;而段选择符是用于找到对应的段描述符的。

       下面说一下段选择符。段选择符是一个由16位长的字段组成的,其中前13位是一个索引号,后面三位包含一些硬件细节,如图3所示。

        
图 3 段选择符结构图

       根据段选择符可以获取段描述符,虽然段描述符比较复杂,但是对于寻址而言,我们只关注Base的32位,它 描述了一个段的开始位置的线性地址。Intel设计的本意是,一些全局的段描述符,就放在"全局段描述符(GDT)"中,一些局部的,例如每个进程自己的,就放在所谓的"局部段描述符表(LDT)中"。那么,究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择描述符中的T1字段表示的,=0标识使用GDT,=1表示使用LDT。GDT在内存中的大小和地址存放在CPU的gdtr寄存器中,而LDT则在ldtr寄存器中。如下图所示。


图 4 逻辑地址转换过程

      首先给定一个完整的逻辑地址[段选择符:段内偏移地址],

      1.看段选择描述符中的T1字段是0还是1,可以知道当前要转换的是GDT中的段,还是LDT中的段,再根据指定的相应的寄存器,得到其地址和大小,我们就有了一个数组了。

      2.拿出段选择符中的前13位,可以在这个数组中查找到对应的段描述符,这样就有了Base,即基地址就知道了。

      3.把基地址Base+Offset,就是要转换的下一个阶段的物理地址。

页式内存管理方式:

        CPU的页式内存管理单元负责把一个线性地址转换为物理地址。从管理和效率的角度出发,线性地址被划分成固定长度单位的数组,称为页(page)。例如,一个32位的机器,线性地址可以达到4G,用4KB为一个页来划分,这样,整个线性地址就被划分为一个2^20次方的的大数组,共有2的20次方个页,也就是1M个页,我们称之为页表,改页表中每一项存储的都是物理页的基地址。

       这里不得不说的是另一个“页”,我们称之为物理页,或者页框、页桢。是分页单元将所有的物理内存都划分成了固定大小的单元为管理单位,其大小一般与内存页大小一致。

       如果内存页按照这种方式进行管理,管理内存页需要2^20次方的数组,其中每个数组都是32bit,也就是4B(其中前20位存储物理内存页的基地址,后面的12位留空,用于与给定的线性地址的后12位拼接起来一起组成一个真实的物理地址,寻找数据的所在。这样就需要为每个进程维护4B*2^20=4MB的内存空间,极大地消耗了内存。

       为了能够尽可能的节约内存,CPU在页式内存管理方式中引入了两级的页表结构,如图5所示。


图 5 二级页表结构

       如图5所示,这种页式管理方式中,第一级的页表称之为“页目录”,用于存放页表的基地址;第二级才是真正的“页表”用于存放物理内存中页框的基地址。

       1、二级页目录的页式内存管理方式中,第一级的页目录的基址存放在CPU寄存器CR3中,这也是转换的开始点;

       2、每一个活动的进程,都有其对应的独立虚拟内存(页目录也是唯一的),那么它对应一个独立的页目录地址。--运行一个进程,需要将它的页目录地址放到CR3寄存器中,将别的页目录的基址暂时换到内存中;

       3、每个32位的线性地址被划分成三部分,页目录索引(10位),页表索引(10位),偏移量(12位)。

线性地址转换成物理地址的过程如下:

       1、从CR3中取出进程的页目录的地址(操作系统在负责进程的调度的时候,将这个地址装入对应的CR3地址寄存器),取出其前20位,这是页目录的基地址;

       2、根据取出来的页目录的基地址以及线性地址的前十位,进行组合得到线性地址的前十位的索引对应的项在页目录中地址,根据该地址可以取到该地址上的值,该值就是二级页表项的基址;当然你说地址是32位,这里只有30位,其实当取出线性地址的前十位之后还会该该前十位左移2位,也就是乘以4,一共32位;之所以这么做是因为每个地址都是4B的大小,因此其地址肯定是4字节对齐的,因此左移两位之后的32位的值恰好就是该前十位的索引项的所对应值的起始地址,只要从该地址开始向后读四个字节就得到了该十位数字对应的页目录中的项的地址,取该地址的值就是对应的页表项的基址;

       3、根据第二步取到的页表项的基址,取其前20位,将线性地址的10-19位左移2位(原因和第2步相同),按照和第2步相同的方式进行组合就可以得到线性地址对应的物理页框在内存中的地址在二级页表中的地址的起始地址,根据该地址向后读四个字节就得到了线性地址对应的物理页框在内存中的地址在二级页表中的地址,然后取该地址上的值就得到线性地址对应的物理页框在内存中的基地址;(这一步的地址比较绕,还请仔细琢磨,反复推敲)

       4、根据第3步取到的基地址,取其前20位得到物理页框在内存中的基址,再根据线性地址最后的12位的偏移量得到具体的物理地址,取该地址上的值就是最后要得到值;

       其实,对比一级页表机制和二级页表机制可以发现,二级页表机制中同样需要1024个二级页表,每个页表都有1024项,每项的大小都是4B,因此一个二级页表需要4KB,1024个二级页表需要4MB的空间,再加上页目录,好像比只有一级页表机制占用了更多的内存,而且寻址方式变得更复杂了,似乎是在自己给自己找麻烦。从CPU的开销中可以看到,如果是一级页表机制,那么CPU取到一个数需要访问内存两次,而使用二级页表机制之后,CPU想要取得一个操作数,需要访问内存三次。其实,使用二级页表机制还是有很多优点的,如下:

      1、二级表结构也许页表分散在内存的各个页面中,而不需要保存在连续的4M的内存块中;

      2、不需要位不存在的线性地址空间分配二级页表,虽然目录表页面总是必须存在于物理内存中,但是二级页表可以在需要的时候再分配,这使得页表结构的大小对应于实际使用的线性地址空间的大小;

      3、页目录和页表中的每个表项都有一个存在属性,页目录中的存在属性指明对应的页表结构是否存在。如果页目录指明对应的二级页表存在,那么通过访问二级表,表查找过程就像上面的查找过程一样进行下去;如果存在标志表明对应的二级表项不存在,那么处理器就会产生一个缺页异常来通知操作系统。页目录表项的存在属性使得操作系统可以根据实际使用的线性地址范围来分配二级页表页面;当然,页目录表项中的存在位还可以在虚拟内存中存放二级页表,这意味着在任何的时候只有部分二级页表需要存放在物理内存中,其余部分可以保存在磁盘中。处于物理内存中的页表对应的页目录项可以被标注为存在,以表明可用它们进行分页转换。处于磁盘上的页表对应的页目录项被标注为不存在。由于二级页表不存在而引发的异常会通知操作系统将缺少的页表从磁盘上加载进物理内存。将页表存储在虚拟内存中减少了保存页表所需要的物理内存;

段页式内存管理方式:

       上面已经对段式内存管理方式和页式内存管理方式做了详细的说明和介绍,段页式内存管理方式就是结合段式内存管理方式和页式内存管理方式,将逻辑地址先转换为线性地址,再将线性地址转换为物理内存中的地址。

       下面结合linux系统的具体寻址方式对段页式内存管理做一个介绍。

        linux下的逻辑地址与线性地址是一致的,之所以说一致而不是说完全相同,是因为linux使用巧妙的线性地址的基地址“欺骗了”CPU,导致转换之后的线性地址和逻辑地址在数值上是一致的,但是逻辑地址转换成线性地址的过程中CPU使用了分段机制中的某些功能。

        按照Intel的本意,全局的使用GDT,每个进程有自己的LDT---不过linux则对所有的进程都使用了相同的段来对指令和数据进行寻址。即用户数据段、用户代码段(用户态使用);对应的还有内核数据段和内核代码段(对应内核态)。

        为了理解linux下逻辑地址和线性地址之间的转换,需要知道几个寄存器:

        1、8个通用寄存器,名字不重要,记住这8个寄存器是32位的;

        2、4个段寄存器,记住这四个家伙是16位的,在32位的CPU体系结构中,这几个寄存器根本存放不下段的地址,所以这四个寄存器中存放了一个叫做段选择符的东西,当然,这个段选择符就是16位的,后面会提到这个段选择符,这四个段寄存器分别是CS、DS、SS、ES。分别存放着代码段、数据段、堆栈段、还有其他段的段选择符;

        3、4个控制寄存器,都是32位的,这四兄弟的名字很好记,分别是CR0、CR1、CR2和CR3。其中,CR1是个无用的家伙,它里面全是0。其他三个控制寄存器,比较重要的是CR0和CR3。

       当CPU处于用户态的时候,使CS和DS存放的是用户代码段和用户数据段的段选择符;当用户处于内核态的时候,CS和DS存放的是内核代码段和内核数据段的段选择符。因此linux下的段选择符根据CPU所处的状态可以确定。之所以这么说是因为在linux系统的include/asm-is86/segment.h中有如下的定义。

#define GDT_ENTRY_DEFAULT_USER_CS 14
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
#define GDT_ENTRY_DEFAULT_USER_DS 15
#define GDT_ENTRY_KERNEL_BASE 12
#define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)
#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)
#define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE + 0)
/*将上述的段选择符展开成二进制如下:*/
#define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE + 1)
#define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8)
#define __KERNEL_CS 96 [00000000 0 1100 0 00]
#define __USER_CS 115 [00000000 0 1110 0 11]
#define __USER_DS 123 [00000000 0 1111 0 11]
#define __KERNEL_DS 104 [00000000 0 1101 0 00]

        之所以定义_USER_CS、_USER_DS、_KERNEL_CS和_KERNEL_DS分别为上述GDT_ENTRY_DEFAULT_USER_CS_、GDT_ENTRY_DEFAULT_USER_DS、GDT_ENTRY_KENEL_CS以及GDT_ENTRY_KERNEL_DS的8倍,这是因为段选择额符的最低三位为标志位,并不是段选择符,所以需要将12、13、14、15等右移三位,当然也就是乘以8;但是用户状态的+3,而内核空间的加0,这是因为该两位对应的是标志位RPL的数值,RPL=3表示这是用户态,具有最低的权限,为0表示这是内核态,具有最高的权限。

        T1=0表示均使用的是GDT,我们可以看看GDT初始化中的12-15项的内容(arch/i386/head.s),如下:

        .quad 0x00cf9a000000ffff        /* 0x60 kernel 4GB code at 0x00000000 */

        .quad 0x00cf92000000ffff        /* 0x68 kernel 4GB data at 0x00000000 */

        .quad 0x00cffa000000ffff        /* 0x73 user 4GB code at 0x00000000 */

        .quad 0x00cff2000000ffff        /* 0x7b user 4GB data at 0x00000000 */

        根据段描述符的结构可知,上述四项的BASE字段的值全为0x00000000,这样基址就是0x00000000。所以,linux下给出的逻辑地址(32位)其实是偏移量,根据逻辑地址转线性地址的方式,可知转换之后的逻辑地址和线性地址的数值是一致的,但是二者并不是概念上的一直,之所以这么说是因为逻辑地址转线性地址的时候,需要查看段选择符的权限位,这是中间的一个必经过程。

        那么逻辑地址转换为线性地址的过程可以描述如下:

        1、CPU根据进程所处的状态将上述的选择描述符装入到那四个16位段寄存器中的一个;

        2、然后将逻辑地址的偏移量装入到8个32位的通用寄存器中的某一个中;

        3、然后MMU中的分段部件开始对逻辑地址进行处理;

        4、首先根据选择描述符中的前13位得到段描述符的索引,根据这个索引可以得到对应的段描述符;

        5、找到段描述符之后,这几乎等于是找到了所有相应段的信息,当然我们可以提取出段的基地址,有了这个基地址,再加上逻辑地址的偏移量就得到了新的地址,这就是线性地址;

       至此,逻辑地址转换成线性地址的步骤完毕。到目前,我们已经有了线性地址,那么根据线性地址得到物理地址的步骤,就和上述的线性地址转换成物理地址的步骤是一致的,此处不再赘述。用一张图概括逻辑地址转换成物理地址的过程如图6。




图 6 逻辑地址转换成物理地址的过程

猜你喜欢

转载自blog.csdn.net/gdj0001/article/details/80135196