全部博文(58)
分类: LINUX
2009-01-09 15:11:54
在内存分段系统中,一个程序的逻辑地址通过分段机制自动地映射(变换)到中间层的4GB(232B)线性地址空间中。程序每次对内存的引用都是对内存段中内存的引用。当程序引用一个内存地址时,通过把相应的段基址加到程序员看得见的逻辑地址上就形成了一个对应的线性地址。此时若没有启用分页机制,则该线性地址就被送到CPU的外部地址总线上,用于直接寻址对应的物理内存。如图5-6所示。
图5-6 虚拟地址(逻辑地址)到物理地址的变换过程
CPU进行地址变换(映射)的主要目的是为了解决虚拟内存空间到物理内存空间的映射问题。虚拟内存空间的含义是指一种利用二级或外部存储空间,使程序能不受实际物理内存量限制而使用内存的一种方法。通常虚拟内存空间要比实际物理内存量大得多。
那么虚拟存储管理是怎样实现的呢?原理与上述列车运行的比喻类似。首先,当一个程序需要使用一块不存在的内存时(即在内存页表项中已标出相应内存页面不在内存中),CPU就需要一种方法来得知这个情况。这是通过80386的页错误异常中断来实现的。当一个进程引用一个不存在页面中的内存地址时,就会触发CPU产生页出错异常中断,并把引起中断的线性地址放到CR2控制寄存器中。因此处理该中断的过程就可以知道发生页异常的确切地址,从而可以把进程要求的页面从二级存储空间(如硬盘上)加载到物理内存中。如果此时物理内存已经被全部占用,那么可以借助二级存储空间的一部分作为交换缓冲区(Swapper)把内存中暂时不使用的页面交换到二级缓冲区中,然后把要求的页面调入内存中。这也就是内存管理的缺页加载机制,在Linux 0.12内核中是在程序mm/memory.c中实现。
Intel CPU使用段(Segment)的概念来对程序进行寻址。每个段定义了内存中的某个区域以及访问的优先级等信息。假定大家知晓实模式下内存寻址原理,现在我们根据CPU在实模式和保护模式下寻址方式的不同,用比较的方法来简单说明32位保护模式运行机制下内存寻址的主要特点。
在实模式下,寻址一个内存地址主要是使用段和偏移值,段值被存放在段寄存器中(如ds),并且段的长度被固定为64KB。段内偏移地址存放在任意一个可用于寻址的寄存器中(如si)。因此,根据段寄存器和偏移寄存器中的值,就可以算出实际指向的内存地址,如图5-7a所示。
而在保护模式运行方式下,段寄存器中存放的不再是被寻址段的基地址,而是一个段描述符表(Segment Descriptor Table)中某一描述符项在表中的索引值。索引值指定的段描述符项中含有需要寻址的内存段的基地址、段的长度值和段的访问特权级别等信息。寻址的内存位置是由该段描述符项中指定的段基地址值与一个段内偏移值组合而成。段的长度可变,由描述符中的内容指定。可见,和实模式下的寻址相比,段寄存器值换成了段描述符表中相应段描述符的索引值以及段表选择位和特权级,称为段选择符(Segment Selector),但偏移值还是使用了原实模式下的概念。这样,在保护模式下寻址一个内存地址就需要比实模式下多一个环节,即需要使用段描述符表。这是由于在保护模式下访问一个内存段需要的信息比较多,而一个16位的段寄存器放不下这么多内容。示意图如图5-7b所示。注意,如果你不在一个段描述符中定义一个内存线性地址空间区域,那么该地址区域就完全不能被寻址,CPU将拒绝访问该地址区域。
5-7 实模式与保护模式下寻址方式的比较
a) 实模式下寻址 b) 保护模式下寻址
每个描述符占用8字节,其中含有所描述段在线性地址空间中的起始地址(基址)、段的长度、段的类型(如代码段和数据段)、段的特权级和其他一些信息。一个段可以定义的最大长度是4GB。
保存描述符项的描述符表有3种类型,每种用于不同目的。全局描述符表(Global Descriptor Table,GDT)是主要的基本描述符表,该表可被所有程序用于引用访问一个内存段。中断描述符表(Interrupt Descriptor Table,IDT)保存了定义中断或异常处理过程的段描述符。IDT表直接替代了8086系统中的中断向量表。为了能在80x86保护模式下正常运行,必须为CPU定义一个GDT表和一个IDT表。最后一种类型的表是局部描述符表(Local Descriptor Table,LDT)。该表应用于多任务系统中。通常每个任务使用一个LDT表。作为对GDT表的扩充,每个LDT表为对应任务提供了更多的可用描述符项,因而也为每个任务提供了可寻址内存空间的范围。这些表可以保存在线性地址空间的任何地方。为了让CPU能定位GDT表、IDT表和当前的LDT表,需要为CPU分别设置GDTR、IDTR和LDTR三个特殊寄存器。这些寄存器中将存储对应表的32位线性基地址和表的限长字节值。表限长值是表的长度值减1。
当CPU要寻址一个段时,就会使用16位的段寄存器中的选择符来定位一个段描述符。在80x86 CPU中,段寄存器中的值右移3位即是描述符表中一个描述符的索引值。13位的索引值最多可定位8192(0~8191)个描述符项。选择符中位2(TI)用来指定使用哪个表。若该位是0则选择符指定的是GDT表中的描述符,否则是LDT表中的描述符。
每个程序都可由若干个内存段组成。程序的逻辑地址(或称为虚拟地址)即是用于寻址这些段和段中具体地址位置。在Linux 0.12中,程序逻辑地址到线性地址的变换过程使用了CPU的全局段描述符表GDT和局部段描述符表LDT。由GDT映射的地址空间称为全局地址空间,由LDT映射的地址空间则称为局部地址空间,而这两者构成了虚拟地址的空间。具体的使用方式如图5-8所示。
5-8 Linux系统中虚拟地址空间分配图
图中画出了具有两个任务时的情况。可以看出,每个任务的局部描述符表LDT本身也是由GDT中描述符定义的一个内存段,在该段中存放着对应任务的代码段和数据段描述符,因此LDT段很短,其段限长通常只要大于24字节即可。同样,每个任务的任务状态段TSS也是由GDT中描述符定义的一个内存段,其段限长也只要满足能够存放一个TSS数据结构就够了。
对于中断描述符表IDT,它保存在内核代码段中。由于在Linux 0.12内核中,内核和各任务的代码段和数据段都分别被映射到线性地址空间中相同基址处,且段限长也一样,因此内核的代码段和数据段是重叠的,各任务的代码段和数据段分别也是重叠的,如5-10和图5-11所示。任务状态段(Task State Segment,TSS)用于在任务切换时CPU自动保存或恢复相关任务的当前执行上下文(CPU当前状态)。例如对于切换出的任务,CPU就把其寄存器等信息保存在该任务的TSS段中,同时CPU使用新切换进任务的TSS段中的信息来设置各寄存器,以恢复该任务的执行环境,参见图4-37。在Linux 0.12中,每个任务的TSS段内容被保存在该任务的任务数据结构中。另外,Linux 0.12内核中没有使用到GDT表中第4个描述符(图中syscall描述符项)。从如下所示的include/linux/sched.h文件中第201行上的原英文注释可以猜想到,Linus当时设计内核时曾经想把系统调用的代码放在这个独立的段中。