binder(二)MMU浅析

什么是虚拟内存

在现代的操作系统中,当你对内存地址进行操作时其实操作的并不是物理内存地址,而是系统虚拟出来的一个虚拟内存地址。
虚拟地址与物理地址
通过简单的图例说明虚拟内存的概念。

操作系统的物理内存是固定的,我们这里指的是RAM。当操作系统运行多个进程时,每个进程如果直接访问物理内存,那是不是就会出现你进程P1访问地址A时如果进程P2也访问该地址,那就会造成进程之间的冲突。所以当代操作系统引入了虚拟内存的概念。

在创建P1和P2进程时,操作系统会分别告诉它们我整个内存都是你的,可是事实上操作系统给它们画了大饼,其实它们得到的是内存地址是虚拟出来的。当P1访问其内部的A地址时,P1.A指向物理地址A中,当P2访问其内部的A地址时它以为它访问的是物理地址A,但其实操作系统通过MMU将它指向了物理地址B。甚至当P1和P2各自访问其内部地址B时,其实它们公用了物理地址C。操作系统这种欺骗的手段就是虚拟内存。这些虚拟地址到物理地址之间的地址映射是MMU来完成的,对于P1和P2都只需知道它们占据了整块内存而无需关系它们的每一个地址都映射到物理内存的哪一个地址上。

所以虚拟内存的好处就是:解决各个进程之间的地址访问冲突问题,所有的地址映射都由MMU来处理,发没发生冲突、如何解决冲突等问题也由MMU来处理,各个进程都无需关心。

使用虚拟内存能更方便地实现内存和数据的共享。例如在进程加载系统库时总是先分配一块内存,将磁盘中的库文件加载到这个内存中。在直接使用物理内存时由于物理内存地址唯一,所以即使系统发现同一个库在系统中加载了多次也无能为力,因为每个进程指定的加载内存不一样。使用了虚拟内存之后这个问题就迎刃而解,系统只需将进程的虚拟内存地址指向库文件所在的物理内存地址即可,就如上图中的P1.B和P2.B同时指向了物理地址C。

什么是MMU

我们知道通过虚拟内存机制,每个进程都以为自己占用了全部内存,进程访问内存时,操作系统都会把进程提供的虚拟内存地址转换为物理地址,再去对应的物理地址上获取数据。CPU 中有一种硬件,内存管理单元 MMU(Memory Management Unit)专门用来将虚拟内存地址翻译为物理内存地址。CPU 还为页表寻址设置了缓存策略,由于程序的局部性,其缓存命中率能达到 98%。

操作系统都是以字节Byte为单位,MMU是使用一张映射表来存储虚拟地址与物理地址之间的映射关系,那每存储一个条目都需要至少8个字节(32位虚拟地址–>32位物理地址),在32位且只有4G运存的设备上就至少需要32G的空间来存储这张对照表,显然这是不现实的,所以操作系统引入了页的概念。

MMU将虚拟内存划分成以 为单位,相应的物理内存空间也会以页框 为单位进行划分。页和页框的大小必须相同。系统启动时会将物理内存以4K为单位划分成许多页框,在分配虚拟内存时也是以4K为单位划分成许多页,这样映射表中的每一个条目都只需存储页索引和其地址针对页索引的偏移量即可,还是在32位且4G运存的设备上只需要8M的空间来存储映射表,大大减少了内存占用。

SWAP

系统为进程分配内存空间并不是一下子就将进程的所有空间分配给它,而是以页为单位进行动态分配(懒分配),用到的时候才会进行真正的内存分配。当进程访问某个页中的虚拟地址时,MMU去映射对应的物理地址发现当前并未给该页分配物理内存空间,那么系统会执行一个缺页中断然后去分配物理内存空间,分配完成之后再将虚拟内存空间指向该物理内存空间。

问题:假设你有一个4(RAM)+128(ROM)的手机,那么王者荣耀一个应用就1个多G,那么是不是运行4个这样大的游戏直接就把手机撑爆了?
因为虚拟内存要通过缺页中断为进程的虚拟内存空间分配真正的物理内存空间,但是现实中物理内存空间一定是有限的,例如你的设备总共只有4G的运存,此时4G内存被占满了怎么办,Linux提出了SWAP概念,其原理是在分配物理内存空间出现可用内存不足的时候,将暂时不用的内存数据先转移到磁盘中去(前提是你的磁盘足够放,例如这里你的磁盘有128G肯定够)让当前需要分配空间的进程先使用,等到被存储到磁盘中的数据再次要被使用时再通过将其他数据转移到磁盘来腾出空间将这些数据再移回内存中。通过这种“交换”技术,使系统可以运行更多的进程使用更多的内存。这就是为什么在虚拟内存解释的时候说系统会欺骗每个进程说它们占有了所有的内存,因为如果必要的时候理论上是可以通过将其他当前不使用的空间数据迁移到磁盘来腾出空间给紧急进程使用的。
当然上述过程使用了一些缓存技术来达到让寻址更加快速的目的。

MMU的工作过程

虚拟地址映射到真正的物理地址的过程是MMU工作的核心
页和页框映射关系图
在这个例子中我们有一台可以生成16位地址的机器,它的虚拟地址范围从0x0000~0xFFFF(64K),而这台机器只有32K的物理地址,因此他可以运行64K的程序,但该程序不能一次性调入内存运行。这台机器必须有一个达到可以存放64K程序的外部存储器(例如磁盘或是FLASH)以保证程序片段在需要时可以被调用。在这个例子中,页的大小为4K,页框大小与页相同(这点是必须保证的,内存和外围存储器之间的传输总是以页为单位的),对应64K的虚拟地址和32K的物理存储器,他们分别包含了16个页和8个页框。

我们先根据上图解释一下分页后要用到的几个术语,在上面我们已经接触了页框,上图中蓝色部分是物理空间,其中每一格表示一个物理页框。橙色部分是虚拟空间,每一格表示一个页,它由两部分组成,分别是Frame Index(页框索引)和位p(present 存在位),Frame Index的意义很明显,它指出本页是往哪个物理页框进行映射的,位p的意义则是指出本页的映射是否有效,如上图,当某个页并没有被映射时(或称映射无效,Frame Index部分为X),该位为0,映射有效则该位为1。

我们执行下面这些指令(本例子的指令不针对任何特定机型,都是伪指令)

例1:

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

虚拟地址0将被送往MMU,MMU看到该虚地址落在页0范围内(页0范围是0到4095),从上图我们看到页0所对应(映射)的页框为2(页框2的地址范围是8192到12287),因此MMU将该虚拟地址转化为物理地址8192,并把地址8192送到地址总线上。内存对MMU的映射一无所知,它只看到一个对地址8192的读请求并执行它。MMU从而把0到4096的虚拟地址映射到8192到12287的物理地址。

例2:

MOVE REG,8192
被转换为
MOVE REG,24576

因为虚拟地址8192在页2中,而页2被映射到页框6(物理地址从24576到28671)

例3:

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

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

通过适当的设置MMU,可以把16个虚页隐射到8个页框中的任何一个,但是这个方法并没有有效的解决虚拟地址空间比物理地址空间大的问题。从上图中我们可以看到,我们只有8个页框(物理地址),但我们有16个页(虚拟地址),所以我们只能把16个页中的8个进行有效的映射。我们看看例4会发生什么情况:

例4:

MOV REG,32780

虚拟地址32780落在页8的范围内,从上图总我们看到页8没有被有效的进行映射(该页被打上X),这是又会发生什么?MMU注意到这个页没有被映射,于是通知CPU发生一个缺页故障(page fault).这种情况下操作系统必须处理这个页故障,它必须从8个物理页框中找到1个当前很少被使用的页框并把该页框的内容写入外围存储器(这个动作被称为page copy),随后把需要引用的页(例4中是页8)映射到刚才释放的页框中(这个动作称为修改映射关系),然后从新执行产生故障的指令(MOV REG,32780)。假设操作系统决定释放页框1,那么它将把虚页8装入物理地址的4-8K,并做两处修改:首先把标记虚页1未被映射(原来虚页1是被影射到页框1的),以使以后任何对虚拟地址4K到8K的访问都引起页故障而使操作系统做出适当的动作(这个动作正是我们现在在讨论的),其次他把虚页8对应的页框号由X变为1,因此重新执行MOV REG,32780时,MMU将把32780映射为4108。

我们大致了解了MMU在我们的机器中扮演了什么角色以及它基本的工作内容是什么,下面我们将举例子说明它究竟是如何工作的(注意,本例中的MMU并无针对某种特定的机型,它是所有MMU工作的一个抽象)。

首先明确一点,MMU的主要工作只有一个,就是把虚拟地址映射到物理地址。
我们已经知道,大多数使用虚拟存储器的系统都使用一种称为分页(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。一个页的大小是4K(4096),也就是说偏移部分必须使用12个bit来表示(2^12=4096,这样才能访问到一个页中的所有地址),8192的二进制码如下图所示:
页索引号与偏移量关系图该地址的页号索引为0010(二进制码),既索引的页为页2,第二部分为000000000100(二进制),偏移量为4。页2中的页框号为6(页2映射在页框6,见上图),我们看到页框6的物理地址是24~28K。
于是MMU计算出虚拟地址8196应该被映射成物理地址24580(页框首地址+偏移量=24576+4=24580)。同样的,若我们对虚拟地址1026进行读取,1026的二进制码为0000010000000010,page index=“0000”=0,offset=010000000010=1026。页号为0,该页映射的页框号为2,页框2的物理地址范围是8192~12287,故MMU将虚拟地址1026映射为物理地址9218(页框首地址+偏移量=8192+1026=9218)。

MMU 的 TLB

由上面的例子可知,在 MMU 工作的时候,软件也需要进行配合,软件需要准备一张表,来告诉 MMU 当前的地址映射的关系(即,虚拟地址和物理地址的对应关系)。而这张表存储在内存中(代码的数据结构),每次 MMU 工作的时候,都去遍历这个表里面的关系,然后找到对应的映射,这个过程叫做 table walk。这样会严重影响系统效率。于是乎,MMU 中增加了 Cache,这个 Cache 叫做 TLB。

为了减少存储器访问的平均消耗, 转换表遍历结果被高速缓存在一个或多个叫作 Translation Lookaside Buffers(TLBs)的结构中。通常在ARM 的实现中每个内存接口有一个TLB。当存储器中的转换表被改变或选中了不同的转换表(通过写CP15 的寄存器,先前高速缓存的转换表遍历结果将不再有效。MMU 结构提供了刷新TLB 的操作。MMU 结构也允许特定的转换表遍历结果被锁定在一个TLB 中,这就保证了对相关的存储器区域的访问绝不会导致转换表遍历,这也对那些把指令和数据锁定在高速缓存中的实时代码有相同的好处。

当ARM 要访问存储器时,MMU 先查找 TLB 中的虚拟地址表,如果没有命中,则,还是要去走 table walk 的流程。即,如果TLB 中没有虚拟地址的入口,则转换表遍历硬件从存在主存储器中的转换表中获取转换和访问权限。一旦取到,这些信息将被放在 TLB 中,它会放在一个没有使用的入口处或覆盖一个已有的入口。

参考文章:
什么是MMU
关于Linux 虚拟内存,你真的理解到位了嘛?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值