逻辑地址就是我们普通的段+偏移的表现方式,而线性地址就是段+偏移之后算出来的一个地址,前者可以认为是二维的地址,而后者可以理解是一维的。线性地址和虚拟地址的概念相接近,不知道其根本的区别。而物理地址就是实际在地址总线上传输的地址,也就是物理内存访问的真正地址。
如上图,Linux在内存管理上,把逻辑地址通过分段机制变化成线性地址,线性地址也就是4G(32位系统)的程序地址。线性地址再通过分页机制转化成物理地址,最后CPU去访问物理地址。
去年写个一篇关于IA32内存寻址的文章,现在再重温下。下面是一张很好的内存寻址图
采用分段机制的好处就是方便了程序员的编码,把整个地址分成不同的数据段,代码段,数据段,堆栈等等。每个段都是动态调整的,在程序重定向的时候。那么对于每一个段的基本信息:段的起始地址,段的长度,段的访问权限等,都会保存在段描述表中(上面的GDT),GDT是存在物理内存中的。也就说我们普通的程序执行一条指令的时候(比如C语言中的&操作),我们操作的是一个逻辑地址,逻辑地址必须通过段机制转化成线性地址,而根据什么转化呢?就是GDT!GDT会告诉你在线性地址哪里到哪里是你的数据段,哪里到哪里是代码段。那么我们怎么找到GDT呢?GDT是在内存里的,这就用到寄存器GDTR了,GDTR会告诉你GDT在内存的起始位置,然后就可以去问GDT我要访问的逻辑地址所对应的线性地址是什么。再深入点,上面说的了GDT里面是描述每个段的基本信息的,其中就是每个段在线性地址里对应的起始位置。
GDT里面存的是各个段的索引,叫做段选择子(Seg.Selector),现代计算机为了减少对内存访问的次数,就把段选择子存于我们平时说的CS寄存器,DS寄存器等,这样,一个逻辑地址到线性地址的转换就大大地加快了。
现在完整地说一遍上面分段的整个流程,对于一个逻辑地址(段:偏移),首先通过GDTR找到GDT,GDT里面是段选择子,直接从段寄存器获得,通过段选择子找到该段在内存里的基地址,然后加上逻辑地址的偏移部分,这就完整地得到了一个线性地址。
前面一篇文章讲到逻辑地址到线性地址的转换,也就是分段机制的实现.分段机制虽然能够让程序员方便地管理程序的各个段,并且可以动态调整段的大小,而且对内存的碎片率也大大减小.但是还是没有解决程序大小比物理内存还大的问题,这时候就出现了分页机制,分页机制的出现最大的体现就是虚拟内存,虚拟内存的实现可以让大的程序装入内存.分页机制的实现还导致了部分程序装入,按需分配内存,代码共享等.
根据上图,逻辑地址通过分段机制变成线性地址,线性地址通过分页机制变成物理地址。那么怎么奖线性地址转换成物理地址呢?首先,对于没有启用分页机制的系统(通过一个寄存器的某一个比特判断)。则直接把线性地址映射到物理地址,物理地址传入到地址总线上。若是启用了分页机制,CPU会把线性地址进行拆分。
这里引入先页的概念,OS把虚拟内存切割,每部分的大小都是一样的(一般可以根据硬件指定),每部分就叫做页(Page),每张页都对应物理内存中的页框(Page Frame,其实就是依次个对应的关系)。
但是线性地址中某个标号为N的页,在物理内存中标号可以是M(N!=M)。也就说可以每个线性地址页都在物理内存中有相对应的页框。而页和页框的对应关系是存在页表里面的,这里可以这样理解,页是一个函系统的输入,页框是一个系统的输出,而页表就是这个系统.但是一张页表要处理整整4G(32位)的大小,光光这张页表就需要很大了.所以就引入二级页表的概念,也可以说成是页目录,页目录的原理和前面页表的作用一样,只不过页目录这个系统的输入是页号,而输出是页表的地址.
好了,理解了上面的基本概念之后,我们就开始线性地址到物理地址的转换.对于一个线性地址,前面的10bits当作页目录项,找到页目录项后就找到对应的页表起始地址,根据线性地址的中间后面10bits(页表的偏移)+页表起始地址,找到物理内存中的页框地址,内存中的页框地址+线性地址低12位就是实际的物理地址.
同样的,上述过程在访问最终的物理内存之前,要进行两次内存访问,两次查表,效率肯定就低了.现代计算机会把页表和页目录也用硬件进行缓存,当然是一部分常用的项.