四 地址空间
到目前为止,我们了解了虚拟内存的原理和分页原理,知道通过一种映射方式,可以实现分页内存以及进一步的虚拟内存的管理。但是,映射具体是怎么实现的,怎么工作的,目前还没有介绍。不过,不着急,在探索答案之前,我们需要先理清一些概念,也就是为解决映射问题,需要先了解的地址空间概念。这些概念,我们在前面提到映射关系时,部分已有提及。这里,我们再进一步理顺:何为地址空间,有哪些地址空间,这些空间都代表了什么?
地址空间是站在不同角度看内存得出的。若站在物理内存连线这个最底层角度,那么只有一个地址空间,这就是实际物理内存地址空间。内存多大,这个空间就多大,整个空间是平坦型的,没有层次结构。
如果引入管理结构,站在进程角度,我们可以提炼出一个进程的逻辑层面的地址空间,叫进程地址空间。这个空间实际是虚拟出来的,人为指定的。
如果站在CPU内存管理单元角度,可以看到一个线性地址空间。这个空间把进程分散使用的页面和虚拟到外部存储(比如磁盘)的空间串起来,整合为整块的内存。
对内存地址空间,其地址是绝对的,固定的,就是说内存中的每一个字节,其地址是固定不变的。CPU访问这个空间时,放到地址总线上的值就是固定的地址值。而对于线性地址空间,每一个有固定地址的页面,不同时刻装载的可能是不同程序的内容,或者是同一程序的不同片段,这基于缺页中断导致。程序不能直接使用上述两个地址。对于物理内存地址空间,同一个地址不同时刻,运行程序不同,装载内容不同,所以程序不能直接用。对于线性地址空间,程序段偏移不同,实际利用的空间位置也会不同,所以编译器不会假设这两个地址空间,对程序的各个段做出布局。相反,编译器会假设程序使用的是一个完整的逻辑地址空间,每次都是从0开始,以此为程序内部各个段布局做出安排,而不管底层操作系统如何做。程序看到的只是自己的逻辑地址空间,每个进程都这样认为。它们被操作系统关起来,只开一个窗户。每次访问外部内存空间时,通过窗户,把逻辑地址送出去即可,不需要知道窗户外地址对应到了物理内存哪里,也不需要知道到底有多少空间可用。
线性地址空间做了这种物理地址空间与逻辑地址空间的中介,这是操作系统所直接面对的,如下图:
一种更加全局的示意图如下:
下面,我们再倒过来,从逻辑地址开始,总结一下。
1 逻辑地址。是由程序编译时产生的地址空间。当我们在代码中通过指针访问变量和内存时,这些指针,最终都会被编译器转换为逻辑地址空间的一员。
2 线性地址。是逻辑地址跟物理地址之间的中介。操作系统对物理内存分页管理,逻辑地址总要对应到分页后的具体页面,这其实是将逻辑地址映射到线性地址的一个区段。大部分情况下,说逻辑地址,是基于分段内存管理,特别是基于Intel保护模式的内存管理。举个例子,在Linux 早期0.11版本内核中,给每个程序限定了64MB的内存空间。因此程序的逻辑地址范围是从0x0000000到0x4000000。又因为32位系统一般支持4GB线性地址空间,所以,总的可以载入64个进程。当然这是理想情况,实际中部分内存要给操作系统及管理内耗,并不是完全给用户进程的。
在不使用分段机制的系统中,逻辑地址一般是等于线性地址的。这时候逻辑地址在线性地址空间的偏移就是0。那么问题来了,每个程序的4G线性地址空间(对于32位系统)又是如何区分的呢?这是基于进程的页表。后面会专门介绍,这里先有个感性的认识。
3 物理地址。是指出现在CPU外部地址总线上的寻址物理内存单元的地址,也是地址空间变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项组合成物理地址。
动态关联上述内存的分配使用过程如下图所述:
有了上述地址空间,下一步要做的就是空间之间的正确转换。这一操作在操作系统中叫映射。映射有点类似坐标变换的感觉。一个东西,从不同角度看去,看到的样子可能是不一样的。有时候,某个样子反而方便我们理解事物本身。线性代数里的变化,似乎干的就是这件事。
另外,通过映射这个概念借用,我们可以知道,这种方式并不改变事物的本质,具体到虚拟内存,就是内存并不因为映射的存在而变多变少。内存还是那些内存,但是我们使用它们的方式,有些方面简单了(比如编译);而有些方面复杂了(比如操作系统)。
最后,还要注意一下,理论上讲,不同的逻辑地址可以映射到同一个线性地址上,不同的线性地址也可以映射到同一个物理地址上,所以是多对一的关系。反过来讲,不同的物理地址可以对应到同一个线性地址,不同的线性地址也可以对应到同一个逻辑地址。从大的时空观角度来看,这种映射是多对多的映射。总之,映射到什么位置并不重要,重要的是从一个空间可以转换到另一个空间。