前一篇我们介绍了内存管理中的分页试内存管理,分页的主要作用就是使得每个进程有一个独立的,完整的内存空间,通过虚拟内存技术,使得程序可以在较小的内存上运行,而进程之间内存空间相互独立,提高了安全性。这一篇将主要介绍内存管理中分段管理,以及两种的结合,也是目前计算机普遍采用的段页式内存管理。这也直接决定了的后面程序的编译,加载以及允许时的内存布局。
在x86-16体系中,为了解决16位寄存器对20位地址线的寻址问题,引入了分段式内存管理。而CPU则使用CS,DS,ES,SS等寄存器来保存程序的段首地址。当CPU执行指令需要访问内存时,只会送出段内的偏移地址,而通过指令的类型类确定访问那一个段寄存器。具体可以参考:计算机原理学习(5)-- x86-16 CPU和内存管理
到了IA-32,Intel引入了保护模式,所以在IA-32中为了保持兼容性,所以同样支持内存分段管理。另外我们讨论过了内存分页,页面中包含了程序的代码,数据等信息,它们都有各自的地址。这些地址是在编译的时候就确定的,因为每个进程都有独立完整的内存空间,只需要把页和物理页映射就能运行,所以这个地址是可以在编译时就决定的。在编译时,编译器会等程序进行语法词法等分析,在编译过程中会建立许多的表,来确定代码和变量的虚拟地址:
前面4张表会随着编译的进行不断增大,而堆栈的数据也会变化,现在的问题就是,每一张表的大小都不确定,那么如何指定每一张表在虚拟内存空间的地址呢?
如上图,没一张表都有自己的起始地址,但是当变量很多的时候,符号表需要的空间可能会超过程序正文的起始地址,这个时候就会把源程序的表的地址覆盖掉。当然编译器没有这么傻,它可以提示无法继续编译,当然这样并不合适,另一个办法就是拿出一部分没有使用的空间给符号表。造成这个问题的原因就是分页系统中的虚拟地址是一维的,所以在编译过程中必须给变量,代码分配虚拟地址。这个有点类似没有采用分页之前,进程之间使用物理地址导致相互覆盖的问题。
所以我们可以为不同的表分配自己的空间地址,也就是分段,这样他们地址都是相对地址,全部编译完成后确定了每张表的大小,就可以计算出实际的虚拟地址了。
分页实际是一个纯粹逻辑上的概念,因为实际的程序和内存并没有被真正的分为了不同的页面。而分段则不同,他是一个逻辑实体。一个段中可以是变量,源代码或者堆栈。一般来说每个段中不会包含不同类型的内容。而分段主要有以下几个作用:
所以在现在的x86的体系结构中分段内存管理是必选的,而分页管理则是可选的。
Intel对分段内存管理逻辑地址的定义是【段号+段内地址】。在x86-16体系的分段管理中,CPU给出的内存地址是16位的段内偏移地址,段的基址从段寄存器中获得,最后计算出24位的物理地址。 而在IA-32体系中引入了保护模式,每个进程有4G的独立地址空间,CPU直接给出的是32位的段内偏移地址,段的基址从内存中获得,最后计算出32为的物理地址。他们最大的不同就在于获取基址的方式以及计算方法。
IA-32为了保持向前兼容,保留了CS/DS/ES/SS这4个寄存器,但因为不在从段寄存器中获得段价值,这4个段寄存器实际上已经失去了原本的作用(但不代表没有使用)。IA-32在内存中使用一张段表来记录各个段映射的物理内存地址(如下图)。
在译地的过程中,x86-16是通过16位的段基址和16位的段内偏移不是简单的相加,而是通过 段值*0x10 + 偏移地址 对基址重定向的方式计算得到物理地址,而IA-32中则相对简单,不需要对基址重定向,这一点和前面分页内存管理是相似的。而CPU只需要为这个段表提供一个记录其首地址的寄存器就可以了。 同样也可以使用TLB来加速。
与x86-16中分段管理另一个不同是,在IA-32中,因为有了独立的地址空间,对多程序也支持的非常好。而分段可以很好的支持进程间数据的共享。
在IA-32中保留的CS/DS/ES/SS这4个16位段寄存器不再被解释为段的基地址,Intel为了保持兼容性将这些寄存器的16个位分成3个用于不同功能的域,称为段选择器。
其中3-15是选择子,存放的是段描述符的索引(可以理解为段号),该描述符为64bit用于描述存储器段的位置、长度和访问权限。而段描述符可以分为两种,全局描述符(GDT)和局部描述符(LDT),对应着第2位,而0,1两位是表示CPU的权限级别(0-4级)。在IA-32中一共有6个段选择器
段描述符就是前面说到的段表中的每一个项目的,一个段描述符由8个字节组成。它描述了段的特征。前面提到段描述符可以分为GDT和LDT两类。通常来说系统只定义一个GDT,而每个进程如果需要放置一些自定义的段,就可以放在自己的LDT中。IA-32中引入了GDTR和LDTR两个寄存器,就是用来存放当前正在使用的GDT和LDT的首地址。
上面的图是Linux中不同段的描述符,结构基本是一致的,只有少数字段有差别。其中最重要的就是BASE字段,一共32位,保存的是当前段的首地址。 在Linux系统中,每个CPU对应一个GDT。一个GDT中有18个段描述符和14个未使用或保留项。其中用户和内核各有一个代码段和数据段,然后还包含一个TSS任务段来保存寄存器的状态。其他的段则包括局部线程存储,电源管理,即插即用等多个段。而Linux系统中,大多数用户态的程序都不使用LDT。
在IA-32中,逻辑地址是16位的段选择符+32位偏移地址,段寄存器不在保存段基址,而是保存段描述符的索引。
为了加速地址转换的过程,根据程序的局部性原理,我们可以讲当前段寄存器指向的段描述符缓存在特定的寄存器中。这里为6个段寄存器准备了6个用来缓存段描述符的非编程寄存器。这样就能加快地址转换的过程。而仅当段寄存器内容变化时,才有必要去访问内存中的GDT或LDT。
分段内存管理的优势在于内存共享和安全控制,而分页内存管理的优势在于提高内利用率。他们之间并不是相互对立的竞争关系,而是可以相互补充的。也就是可以把2种方式结合起来,也就是目前计算机中最普遍采用的段页式内存管理。段页式管理的核心就是对内存进行分段,对每个段进行分页。这样在拥有了分段的优势的同时,可以更加合理的使用内存的物理页。
对于段页式管理来说,我们需要通过段表来保存每一个段的信息,通过页表保存每个段中虚拟页的信息。在段页管理的系统中,CPU给出的不再是分页系统中的虚拟地址,而是给出的逻辑地址。(前面2篇有介绍逻辑地址和虚拟地址,简单的说逻辑地址是二维的,而虚拟地址是一维的,平坦的)。
上面的图简单的描述了在段页式内存管理的系统中,地址转换的过程。实际上就是我们前面介绍的分段和分页地址转换的结合。
这一篇文章主要介绍了IA-32系统中分段式内存管理是如何工作的。而目前主流的系统中都采用了段页相结合的内存管理方式,当然不同的系统具体实现起来是不同的。比如在Linux中,所有段首地址都是从0x0000000开始,所以逻辑地址和转换得到的线性地址完全是一样的。到这一篇,有关x86的内存管理方式以及介绍晚了。内存的分页和分段也决定了程序的编译,可执行文件的结构,程序的内存布局等等。这个将在后面介绍到。
《深入理解Linux内核第三版》
《现代操作系统》