4. MMU

现代操作系统普遍采用虚拟内存管理(Virtual Memory Management)机制,这需要MMU(Memory Management Unit,内存管理单元)的支持。有些嵌入式处理器没有MMU,则不能运行依赖于虚拟内存管理的操作系统。本节简要介绍MMU的作用和操作系统的虚拟内存管理机制。

首先引入两个概念,虚拟地址和物理地址。如果处理器没有MMU,或者有MMU但没有启用,CPU执行单元发出的内存地址将直接传到芯片引脚上,被内存芯片(以下称为物理内存,以便与虚拟内存区分)接收,这称为物理地址(Physical Address,以下简称PA),如下图所示。

图 17.4. 物理地址

物理地址

如果处理器启用了MMU,CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址,如下图所示。

图 17.5. 虚拟地址

虚拟地址

注意,对于32位的CPU,从CPU执行单元这边看地址线是32条(图中只是示意性地画了4条地址线),可寻址空间是4GB,但是通常嵌入式处理器的地址引脚不会有这么多条地址线,因为引脚是芯片上十分有限而宝贵的资源,而且也不太可能用到4GB这么大的物理内存。事实上,在启用MMU的情况下虚拟地址空间和物理地址空间是完全独立的,物理地址空间既可以小于也可以大于虚拟地址空间,例如有些32位的服务器可以配置大于4GB的物理内存。我们说32位的CPU,是指CPU寄存器是32位的,数据总线是32位的,虚拟地址空间是32位的,而物理地址空间则不一定是32位的。物理地址的范围是多少,取决于处理器引脚上有多少条地址线,也取决于这些地址线上实际连接了多大的内存芯片。

MMU将虚拟地址映射到物理地址是以页(Page)为单位的,对于32位CPU通常一页为4KB。例如,MMU可以通过一个映射项将虚拟地址的一页0xb7001000~0xb7001fff映射到物理地址的一页0x2000~0x2fff,物理内存中的页称为物理页面或页帧(Page Frame)。至于虚拟内存的哪个页面映射到物理内存的哪个页帧,这是通过页表(Page Table)来描述的,页表保存在物理内存中,MMU会查找页表来确定一个虚拟地址应该映射到什么物理地址。总结一下这个过程:

  1. 在操作系统初始化或者分配、释放内存时,会执行一些指令在物理内存中填写页表,然后用指令设置MMU,告诉MMU页表在物理内存中的什么位置。

  2. 设置好之后,CPU每次执行访问内存的指令都会自动引发MMU做查表和地址转换的操作,地址转换操作完全由硬件完成,不需要用指令控制MMU去做。

我们在程序中使用的变量和函数都有各自的地址,程序被编译后,这些地址就成了指令中的地址,指令中的地址被CPU解释执行,就成了CPU执行单元发出的内存地址,所以在启用MMU的情况下,程序中使用的地址都是虚拟地址。一个操作系统中同时运行着很多进程,通常桌面上的每个窗口都是一个进程,Shell是一个进程,在Shell下敲命令运行的程序又是一个新的进程,此外还有很多系统服务和后台进程在默默无闻地工作着。由于有了虚拟内存管理机制,各进程不必担心自己使用的地址范围会不会和别的进程冲突,比如两个进程都使用了虚拟地址0x0804 8000,操作系统可以设置MMU的映射项把它们映射到不同的物理地址,它们通过同样的虚拟地址访问不同的物理页面,就不会冲突了。虚拟内存管理机制还会在后面进一步讨论。

MMU除了做地址转换之外,还提供内存保护机制。各种体系结构都有用户模式(User Mode)和特权模式(Privileged Mode)之分,操作系统可以设定每个内存页面的访问权限,有些页面不允许访问,有些页面只有在CPU处于特权模式时才允许访问,有些页面在用户模式和特权模式都可以访问,允许访问的权限又分为可读、可写和可执行三种。这样设定好之后,当CPU要访问一个VA时,MMU会检查CPU当前处于用户模式还是特权模式,访问内存的目的是读数据、写数据还是取指令,如果和操作系统设定的页面权限相符,就允许访问,把它转换成PA,否则不允许访问,产生一个异常(Exception)。异常的处理过程和中断类似,只不过中断是由外部设备产生的,而异常是由CPU内部产生的,中断产生的原因和CPU当前执行的指令无关,而异常的产生就是由于CPU当前执行的指令出了问题,例如访问内存的指令被MMU检查出权限错误,除法指令的除数为0等。

中断”和“异常”这两个名词用得也比较混乱,不同的体系结构有不同的定义,有时候中断和异常不加区分,有时候异常包括中断,有时候中断包括异常。在本书中按上述定义使用这两个名词,中断的产生与指令的执行是异步(Asynchronous)的,异常的产生与指令的执行是同步(Synchronous)的。

图 17.6. 处理器模式

处理器模式

通常操作系统把虚拟地址空间划分为用户空间和内核空间,例如x86平台的虚拟地址空间是0x0000 0000~0xffff ffff,大致上前3GB(0x0000 0000~0xbfff ffff)是用户空间,后1GB(0xc000 0000~0xffff ffff)是内核空间。用户程序在用户模式下执行,不能访问内核中的数据,也不能跳转到内核代码中执行。这样可以保护内核,如果一个进程访问了非法地址,顶多这一个进程崩溃,而不会影响到内核和其它进程。CPU在产生中断或异常时会自动切换模式,由用户模式切换到特权模式,因此跳转到内核代码中执行中断或异常服务程序就被允许了。事实上,所有内核代码的执行都是从中断或异常服务程序开始的,整个内核就是由各种中断处理和异常处理程序组成。

我们已经遇到过很多次的段错误是这样产生的:

  1. 用户程序要访问的一个VA,经MMU检查无权访问。

  2. MMU产生一个异常,CPU从用户模式切换到特权模式,跳转到内核代码中执行异常服务程序。

  3. 内核把这个异常解释为段错误,把引发异常的进程终止掉。

访问权限也是在页表中设置的,可以设定哪些页面属于用户空间,哪些页面属于内核空间,哪些页面可读,哪些页面可写,哪些页面的数据可以当作指令执行等等。MMU在做地址转换时顺便检查访问权限。