link

一、MMU的产生

许多年以前,当人们还在使用DOS 或者更古老的操作系统的时候,计算机的内存还非常小,一般都是以K 为单位进行计算的,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以容纳当时的程序。

但随着图形界在的兴起,用户需求的不断增大,应用程序的规模也随之膨胀起来,终于一个难题出现在程序员的面前,那就是应用程序太大,以至于内存容纳不下该程序。

通常解决的办法是把程序分割成许多份称为覆盖块(overlay)的片段。

覆盖块0 首先运行,结束时他将调用另一个覆盖块。

虽然覆盖块的交换是由OS 完成的,但是必须先由程序员先进行分割,这是一个费时费力的工作,而且相当枯燥。

人们必须找于更好的办法从根本上解决这个问题。

不久人们找到了一个办法,这就是虚拟存储器(virtual memory)。

1、虚拟存储器(Virtual Memory)

虚拟存储存的基本思想是:

程序、数据、堆栈的总的大小可以超过物理存储器的大小,操作系统把当前使用的部分保留在内存中,而把其他未被使用的部分保存在磁盘上。

比如,对一个16MB 的程序 和一个内存只有4MB 的机器,OS 通过选择,可以决定各个时刻将哪4MB 的内容保留在内存中,并需要时在内存和磁盘间交换程序片段,这样就可以把这个16MB 的程序运行在一个具有4MB 内存机器上了。而这个16M的程序在运行前不必由程序员进行分割。

任何时候,计算机上都存在一个程序能够产生的地址集合,我们称之为地址范围。

这个范围的大小由CPU 的位数决定,

例如一个32位的CPU ,它的地址范围是0x0 ~ 0xFFFF FFFF (4G)

而对于一个64位的CPU ,它的地址范围为0x0 ~ 0xFFFF FFFF FFFF FFFF (64T)

这个范围就 是我们程序能够产生的地址范围,我们把这个地址范围称为虚拟地址空间,该空间中的某一个地址我们称之为虚拟地址。

与虚拟地址空间 和 虚拟地址相对应的则是物理地址空间 和 物理地址,

大多数时候,我们的系统所具备的物理地址空间只是虚拟地址空间的一个子集,

这时举一个最简单的例子直观的说明这两者,对于一台内存为256MB 的32Bit x86 主机来说,它的虚拟地址空间范围是0x0 ~ 0xFFFF FFFF (4G),而物理地址空间范围是0x0000 0000 ~ 0x0FFF FFFF ( 256MB )

在没有使用虚拟地址的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储被读写。而使用了虚拟存储的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到内存管理单元— MMU

MMU由一个或一组芯片组成,一般存在于协处理器中,其功能是把虚拟地址映射为物理地址。

  1. CPU 看到的是 Virtual Adress (程序中的逻辑地址)
  2. Caches 和 MMU 使用的是 MVA (实际的虚拟地址 MVA = (pid << 25) | VA)
  3. 实际物理设备使用的是 Physical Address (物理地址)

二、MMU的工作过程

大多数使用虚拟存储器的系统都使用一种称为分页(paging)。

虚拟地址空间划分为页(page)的单位,而相应的物理地址空间也被进行划分,单位是页框(frame)。

页和页框的大小必须相同。

接下来配全图片,以一个例子说明页与页框之间在MMU 的调度下是如何进行映射的:

在这个例子中,我们有一个可以生成16位地址的机器,它的虚拟地址范围从0x0000 ~ 0xFFFF(64k),而这台机器只有32K 的物理地址,因此它可以运行64K 的程序,但该程序不能一次性调入内存运行。

这台机器必须有一个达到可以存放64K 程序 的外部存储器(例如磁盘或Flash) 以保证程序片段在需要时可以被调用。

这个例子中,页的大小为64K ,页框大小与页相同(这点必须保证的,内存和外围存储器之间传输总是以页为单位),对 应64K 的虚拟地址和32K 的物理存储器,它们分别包含了16 个页 和 8 个页框。

执行下面这些指令:

MOVE REG,0// 将 0 号地址的值传递进寄存器 REG

虚拟地址 0 将被送往MMU,MMU看到该虚拟地址落在页0 范围内(页0 范围是0 到 4095),从上图我们可以看出页0 所对应的(映射)的页框为 2(页框2的地址范围是8192 到 12287)。

因此,MMU 将该虚拟地址转化为物理地址 8192, 并把地址8192送到地址总结上。

内存对MMU 的映射一无所知,它只看到一个对地址8192的读请求并执行它,MMU 从而将8192 到 12287换虚拟地址解析为对应的物理地址 0 到 4096 。

MOVE REG , 20500
被转换为---->  MOVE REG, 12308

因为虚拟地址20500 在虚页5(虚拟地址范围是20480 到 24575)距开头20个字节处,虚页5映射到页框3(页框3的地址范围是12288 到 16383),于是被映射到物理地址12288 + 20 = 12308

MOV REG , 32780

虚拟地址32780 落在页 8 的范围内,从上图我们看出,页8并没有被 有效的进行映射(该页被打上X),这时又会发生什么呢?

MMU 注意到这个页没有被映射,于是通知CPU 发生一个缺页故障(page fault),这种情况下,操作系统必须处理这个页故障,它必须从8个物理页框中找到一个很少被使用的页框,并把该页框的内容写入外围存储器(这个动作被称为page copy),随后把需要引用的页(本例 是页8)映射到刚才被释放的页框中(这个动作被称为修改映射关系),然后重新执行产生故障的指令(MOV REG, 32780).

假定操作系统,决定释放页框1, 以使以后任何对虚拟地址4K 到 8K 的访问都引起故障而使操作系统做出适当的动作。

其次它把虚页8 对应的页框号由X 变为1, 因此得新执行MOV REG, 32780,MMU 将32780 映射为 4180。

我们已经知道,大多数使用虚拟存储器的系统都使用一种称为分页(paging)的技术,就象我们刚才所举的例子,虚拟地址空间被分为大小相同的一组页,每个页有一个用来标示它的页号(这个页号一般是它在该组中的索引,这点和C/C++中的数组相似)。

在上面的例子中0~4K的页号为0,4~8K的页号为1,8~12K的页号为2,以此类推。

而虚拟地址(注意:是一个确定的地址,不是一个空间)被MMU分为2个部分,第一部分是页号索引(page Index),第二部分则是相对该页首地址的偏移量(offset).

我们还是以刚才那个16位机器结合下图进行一个实例说明,该实例中,虚拟地址8196被送进MMU,MMU把它映射成物理地址。16位的CPU总共能产生的地址范围是0~64K,按每页4K的大小计算,该空间必须被分成16个页。而我们的虚拟地址第一部分所能够表达的范围也必须等于16(这样才能索引到该页组中的每一个页),也就是说这个部分至少需要4个bit。

该地址的页号索引为0010(二进制码),即索引的页为页2,第二部分为000000000100(二进制),偏移量为4。

页2中的页框号为6(页2映射在页框6,见上图),我们看到页框6的物理地址是24~28K。于是MMU计算出虚拟地址8196应该被映射成物理地址24580(页框首地址+偏移量=24576+4=24580)。

同样的,若我们对虚拟地址1026进行读取,1026的二进制码为0000010000000010page index="0000"=0,offset=010000000010=1026

页号为0,该页映射的页框号为2,页框2的物理地址范围是8192~12287,故MMU将虚拟地址1026映射为物理地址9218(页框首地址+偏移量=8192+1026=9218)。

以上就是MMU的工作过程。

三、虚拟内存管理

现代操作系统普遍采用虚拟内存管理(Virtual Memory Management)机制,这需要处理器中的MMU(Memory Mangement Unit,内存管理单元)提供支持。

首先引入两个概念,虚拟地址和物理地址。

  • 如果处理器没有MMU,或者有MMU 但没有启用,CPU执行单元发出的内存地址将直接传到芯片引脚上,被物理内存芯片接收,这称为物理地址。
  • 如果处理器启用了MMU,CPU执行单元发出的内存地址将被 MMU 截获,从CPU到MMU 的地址称为虚拟地址,而MMU 将这个地址翻译成另一个地址,发到CPU芯片的外部地址引脚上,也就是将VA映射成了PA 了。

如果是32位处理器, 则内存地址总线是32位的,与CPU 执行单元相连,而经过MMU转换后的外地址总线则不一定是32位。

也就是说,虚拟地址空间与物理地址空间是独立的,32位处理器的虚拟地址空间是4GB,而物理地址空间既可以大于也可以小于4G。

MMU 将 VA映射到PA是以页(page)为单位的,32位处理器的页尺寸通常是4KB。

例如:

MMU 可以通过一个映射项将VA 的一页0xB7001000 - 0xB7001FFFF映射到PA 的一页0x2000 ~ 0x2FFF

如果CPU 执行单元要访问虚拟地址0xB7001008,则实际访问到的物理地址是0x2008。

物理内存中的页称为物理页帧(page frame),虚拟内存的哪个页面映射到物理内存的哪个页帧是通过页表(Page Table)来描述的,页表保存在物理内存中,MMU 会查找页表来确定一个VA 应该映射到什么PA.

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

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

我们在程序中使用的变量和函数都有各自的地址,程序被编译后,这些地址就成了指令中的地址,指令中的地址被 CPU解释执行,就成了CPU的执行单元发出的内存地址,所以在启用MMU 的情况下,程序中使用的地址都是虚拟地址,都会引发MMU 做查表和地址转主换操作。

那为什么要设计这么复杂的内存管理机制呢? 多了一层VA 到 PA 的转换到底换来什么好处?

MMU 除了做地址转换之外,还提供内存保护机制,各种体系结构都有用户模式(User Mode)和特权模式(Privileged Mode)之分,操作系统可以在页表中设置每个内存页面的访问权限,

有些页面不允许访问,

有些页面只有在CPU 处于特权模式时才允许访问,

有些页面在用户模工和特权模式都可以访问,访问权限又分为可读、可写 和可执行三种。

这样设定好之后,当CPU 要访问一个VA 时,MMU都会检查CPU 当前处于用户模式还是特权模式,访问内存的目的是读数据、写数据、还是取指令,如果和操作系统设定的页面权限相符,就允许访问,把它转换成PA ;如果不允许访问,就产生一个异常(Exception)。

异常处理过程和中断类似,不同的是中断由外部设备产生而异常由CPU 内部产生,中断产生的原因和CPU 当前执行的指令无关,而异常的产生就是由于CPU 当前执行的指令出了问题,例如,访问内存的指令被 MMU 检查出权限错误,除法指令的除数为0 都会产生异常。

用户空间和内核空间

通常操作系统把虚拟地址划分为用户空间和内核空间,例如 X86平台的Linux 系统虚拟地址空间是0x00000000 - 0xFFFFFFFF,前3GB(0x00000000 - 0xBFFFFFFF)是用户空间,后1GB(0xC0000000 - 0xFFFFFFFF)是内核空间。

用户程序加载到用户空间,在用户模式下执行,不能访问内核中的数据,也不能跳转到内核代码中执行。

这样可以保护内核,如果一个进程访问了非法地址,顶多这一个进程崩溃,而不会影响到内核和整个系统的稳定性。

CPU 在产生中断和异常时不仅会跳转到中断或异常服务程序,还会自动切换模式,从用户模式切换到特权模式,因此从中断或异常服务程序可以跳转到内核代码中执行。

事实上,整 个内核就是由各种中断和异常处理程序组成的。

总结下:

在正常情况下 ,处理器在用户模式执行用户程序,在中断或异常情况下处理器切换到特权模式执行内核程序,处理完中断或异常之后再返回用户模式继续执行用户程序。

段错误

段错误是这样产生的:用户程序要访问一个VA, 经MMU 检查无权访问,MMU 产生一个异常,CPU 从用户模式切换到特权模式,跳转到内核代码中执行异常服务程序。内核把这个异常解释为段错误,把相发异常的进程终止掉。

推荐:

视频:剖析Linux内核MMU机制

C/C++Linux服务器开发/高级架构师 系统学习公开课地址

欢迎朋友们加入C/C++Linux服务器开发/高级架构师群: 960994558 群内提供免费的C/C++Linux服务器开发/高级架构师学习资料资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)!

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐