Linux 内存管理浅析 - 页面映射管理之地址空间

二. 页面映射管理


1. 页表管理

我们知道CPU需要访问指令和数据,第一步就是将指令或数据地址放到地址总线上,由内存控制器负责将指令或数据从内存中读出,然后放到数据总线上,CPU从数据总线上获取指令或数据。由于CPU访问的是虚拟地址,虚拟地址必须经过转换成实际的地址才能送到地址总线上。这个地址转换工作,在现代CPU里有个专门的硬件模块负责,就是MMU(Memory Management Unit)。

在介绍MMU转换工作之前,我们先了解一下地址空间的概念。


(1). 地址空间

地址空间是一个抽象的概念。

我们说具有不同的地址空间的时候,是指在不同的地址空间的地址没有逻辑上的关联。举个例子,如果地址0x0000与地址0x0003在相同的地址空间,则它们具有某种关系,它们代表了不同的数据存放位置,具有唯一性。如果这两个地址相同,我们则可以认为存放的数据是相同的。如果这两个地址在不同的地址空间,则它们是没有关联的。不管它们是否具有相同的地址。在不同地址空间的地址没有可比性。

这里我们可以分为虚拟地址空间和实际的物理地址空间。
虚拟的地址空间代表了进程运行的空间,是CPU可以访问的地址,在这个空间里,程序的执行不需要考虑实际设备在哪儿。对于程序来说,这是系统提供给它的一段地址,在这段地址里,它可以访问任何它想访问的地方。但是这只是一段地址,如果没有映射到实际设备地址,是没有任何意义的。不同的进程具有不同的用户虚拟地址空间。

我们可以想象成程序的执行就是CPU与外部设备的交互过程。对于外部设备,我们有必要把它们统一编址在同一个地址空间,这样,对这些设备的访问,我们才能保证唯一性。这个地址空间,我们称之为物理地址空间。是设备的统一编址。

由于虚拟地址空间不代表任何实际的设备,则它的大小取决于CPU可以访问的地址范围。对于32bit CPU来说,地址范围就是0 - 2^32,64bit 则是0 - 2^64。不同的虚拟地址范围对我们的内存管理的行为具有不同的影响。

下面这张图显示了地址空间与页表的关系。由于e6500是64bit的CPU,它的虚拟地址空间非常大。后面用户地址空间管理部分我们会看到具体虚拟地址的划分,这里只需要了解到每一个进程的用户态地址是从0到0x0x0000400000000000(64TB,46bit)的。


Linux 内存管理浅析 - 页面映射管理之地址空间


每个程序被加载执行后,都具有相同的地址空间。CPU执行不同的程序进程,对于CPU来说,是不区分是哪个进程的地址空间的。不同的进程的地址空间的地址转换成实际物理地址空间地址,需要由MMU来完成。

在上图中,当CPU送出虚拟地址时,MMU首先在TLB中查找,如果找到了相应页表,则直接进行转换,将相应的实际地址送到地址总线。这里就有个问题,如果切换到了别的进程,那查找的地址还有效吗?实际做法是,每个进程都有自己的页表结构,当发生进程切换时,相应进程的页表结构地址也会更新到某个寄存器中,TLB中相应被切换出去进程的页表被设置无效。MMU在TLB中查找不到当前进程相应页表,则会根据寄存器中保存的当前进程的页表结构地址去在内存中查找,这里我们称发生了TLB miss。内存中查找到相应页表,会更新到TLB,下次查找就可以直接在TLB中查找到,加速页表转换速度。

从上面我们也可以看到内核的虚拟地址空间是进程间共享的,也即不管进程是否发生切换,内核地址空间的页表是不变的,实际上这部分页表会在TLB中特殊保存,后面MMU管理部分我们会看到。


再来说说进程和线程。

进程我们可以理解为就是一个空间,在这个空间中不同的时刻会发生不同的事件。
线程就是其中一系列事件的组合,而事件则是行为的结果。CPU访问内存是一个行为,这个行为本身是可以重复的,但它会在不同的时刻产生不同的结果,也即发生了不同的事件。行为我们可以理解为是我们的代码,而事件则是代码执行的结果。在这里,代码的执行者是CPU,也就是事件的产生者。从系统的角度来看,不同的程序具有不同的行为,当CPU去执行这些不同的程序时,会产生不同的事件组合。这样我们就可以把他们抽象出来,分为不同的实体进行管理,这就是线程。

我们再来看行为的执行。当CPU执行某个程序时,必然需要用到某些资源,产生运算结果等。在CPU执行的每一步,我们必须要想办法把前面执行的结果保存下来,否则CPU永远也不知道以前做过什么,那后面也就可能永远原地打转。线程的栈我们用来记录线程的执行过程,不同的线程具有不同的栈。

不同的线程可能发生在相同的空间,这个空间包含了程序这样的行为,也包含了程序的执行者(CPU,MMU,内存控制器等硬件)、程序执行的过程及结果、所用到的内存空间及资源。所有这些,在不同的时刻,产生了不同的事件,反映了程序编制者的意图。我们通过进程这个实体来描述这样一个空间。

那么不同的进程具有不同的空间,它们不是可能包含了相同的资源吗?如CPU和内存。那不同的空间怎么能够互相不干扰呢?答案就是分时复用和地址隔离。像CPU和寄存器这样的资源,不同的进程是分时占用的,在同一时刻,只有一个进程能够
使用。当然,如果同一个进程内部有不同的线程需要使用CPU这样的资源,它们也同样需要分时复用。对于像内存这样的资源,通常是分配不同的地址给不同的进程来复用的。

如果按照地址空间来划分,则进程可以分为用户进程和内核进程。用户进程工作在用户进程地址空间内,如我们在上面图片看到的那样,不同的用户进程具有自己的地址空间。用户进程处理的是程序编制者需要解决的具体问题,他与计算机本身的管理是无关的。而内核进程则只有一个,其工作在内核地址空间中。内核进程的任务就是负责用户进程的切换、内存的分配、资源的分配及外设的管理等,它处理的是计算机自身的问题。


  • 虚拟地址空间与物理地址空间的划分及映射关系
    下面这幅图是e6500 CPU的虚拟地址空间的划分及其与物理地址空间的部分映射关系。之所以说是部分映射,是因为物理地址空间我只画出了物理内存部分。另外我只画出了内核虚拟地址空间地址划分。具体物理地址的划分取决于不同的平台配置。对于一般的配置,物理内存的地址通常是从物理地址0开始的。取决于不同的内存模型,内存地址可能是不连续的,即中间有一段地址,不是内存使用的。这部分在物理内存管理部分我们会详细的了解到。用户进程虚拟地址空间划分,我们会在后面用户进程地址空间管理部分看到。

    内核代码:arch\powerpc\include\asm\page.h 定义了内核虚拟地址划分。

Linux 内存管理浅析 - 页面映射管理之地址空间

由于64bit地址空间非常巨大,内核的虚拟地址空间划分同32bit是有不同的。后面我们在物理内存分配管理部分会详细介绍。

这里我说下linear map这一段虚拟地址。这段地址从名字可看出,是线性映射区。就是说这一段地址和内存物理地址是对应的。从图中可看出,由于64bit地址空间足够容纳所有物理内存地址,所以这一段虚拟地址映射到了所有物理内存地址空间。

除了线性映射区外,内核还提供了vmalloc这样的内存分配机制。注意vmalloc只是用于内核进程分配内存所用,它和用户进程地址空间分配没有关系。入前所述,线性映射区内存虚拟地址与物理内存地址一一对应。这就对虚拟地址的分配有了限制。这是因为我们分配一段虚拟地址的时候,必须要考虑对应的物理内存是否连续可用,如果不可用,则内存分配将失败。我们知道,系统运行一段时间之后,会出现所谓的内存碎片,很有可能没有大段连续的物理内存可用。这个时候,vmalloc机制,提供了一个方法,使得连续的虚拟地址不再需要对应到连续的物理内存地址。这和用户进程的地址空间映射是一样的。它们采用的方法是页面映射。