前言
现代操作系统普遍采用虚拟内存管理(Virtual Memory Management)机制,这需要处理器中的MMU(Memory Management Unit,内存管理单元)提供支持。
MMU(Memory Management Unit) :内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权。
内存管理单元MMU(memory management unit)的主要功能是虚拟地址(virtual memory addresses)到物理地址(physical addresses)的转换。除此之外,它还可以实现内存保护(memory protection)、缓存控制(cache control)、总线仲裁(bus arbitration)以及存储体切换(bank switching)。
工作机制
CPU将要请求的虚拟地址传给MMU,然后MMU先在高速缓存TLB(Translation Lookaside Buffer)查找转换关系,如果找到了相应的物理地址则直接访问;如果找不到则在地址转换表(Translation Table)里查找计算。
虚拟地址
现代的内存管理单元是以页的方式来分区虚拟地址空间(the range of addresses used by the processor)的。页的大小是2的n次方,通常为几KB。所以虚拟地址就被分为了两个部分:virtual page number和offset。
页表项(page table entry)
上面从虚拟页号在页表里找到的存放物理页表号的条目就是页表项(PTE)。PTE一般占1个字长,里面不仅包含了physical page number,还包含了重写标志位(dirty bit)、访问控制位(accessed bit)、允许读写的进程类型(user/supervisor mode)、是否可以被cached以及映射类型(PTE最后两位)。
映射
映射方式
映射方式有两种,段映射和页映射。段映射只用到一级页表,页映射用到一级页表和二级页表。
映射粒度
段映射的映射粒度有两种,1M section和16M supersection;页映射的映射粒度有4K small page、64K large page和过时的1K tiny page。
假设页表只有一级
对于一个有MMU的CPU而言,MMU开启后,CPU是这样寻址的:CPU任何时候,一切时候,发 出的地址都是虚拟地址,这个虚拟地址发给MMU后.MMU通过页表来在页表里面查出来这个虚 拟地址对应的物理地址是什么,从而去访问外面的内存条。MMU里面的页表地址寄存器,记录 了页表本身的存放位置。
现在我们假设每一页的大小是4KB,而且假设页表只有一级,这个页表长成下面这个样子,页表的每一行是32个bit。
总结CPU访问虚拟地址的流程如下:
依次按顺序判断:是否命中(命中:想要的数据在内存中)、是否满足RWX权限、是否满足 User/Kernel权限,只要一项不满足,MMU会给CPU发出paqefault,CPU自动跳到fault的代码 去处理fault。全满足,那么MMU就去访问内存条上对应的地址。
另外,如果页表只有1级,每4KB的虚拟地址空间就需要页表里面的一行(32bit),那么CPU要覆盖到整个4GB的内存,就需要这个页表的大小是:4MB。
所以,这个页表的大小是4MB,覆盖了整个0-4GB的虚拟地址空间,任何一个虚拟地址,都可 以用地址的高20位(由于一页是4KB,低12位就是叶内偏移了),作为页表这个表的行号去读 对应的页表项。
这个查水表的过程,由MMU硬件自动完成。
现在我们假设在Linux里面有2个进程,一个是QQ,一个是Firefox,他们的页表分别如下:
当CPU在执行QQ的时候,Linux会把QQ的页表的物理地址255MB,填入MMU的页表地址寄存 器,于是这个时候,QQ的页表生效。根据页表内容,CPU如果访问4KB这个虚拟地址的话, MMU访问内存条的6MB物理地址:CPU如果访问8KB这个虚拟地址的话,MMU访问内存条的 8MB物理地址:CPU如果访问3GB这个虚拟地址的话,MMU访问内存条的0MB物理地址;
当CPU在执行Firefox的时候,Linux会把Firefox的页表的物理地址280MB,填入MMU的页表地址 寄存器,于是这个时候,Firefox的页表生效,QQ的页表淡出江湖。根据页表内容,CPU如果访 问4KB这个虚拟地址的话,MMU访问内存条的100MB物理地址:CPU如果访问8KB这个虚拟地 址的话,MMU访问内存条的200MB物理地址:CPU如果访问3GB这个虚拟地址的话,MMU访 问内存条的0MB物理地址。
上面我们发现一个共同点,QQ和Firefox去访问3GB虚拟地址的时候,最终MMU访问的都是 0MB这个物理地址,具体原因非常简单,QQ和Firefox,这2张页表里面,3GB/4KB这一行,里面填的是完全一样的东东。
多级页表:真实的存在
上面我们发现,如果采用一级页表的话,每个进程都需要1个4MB的页表,这个空间浪费还是很 大,干是我们可以采用二级或者三级页表。举例如下,假设我们用地址的高10位作为一级页表 的索引,中间10位作为2级页表的索引。CPU访问虚拟地址16,这个地址如果分解为10/10/12位 的话,就是这个样子:
那么MMU会用0这个下标去访问一级页表(一级页表的地址填入MMU的页表地址寄存器)的第 0行,第0行的内容写的是2MB(此外不再是最终的物理地址,而是二级页表的物理地址).证明二 级页表的地址在2MB,于是MMU自动去以中间的10位作为下标,去查询位置在2MB的二级页 表.在2级页表里面,最终查到第0页(地址范围0x00000000~0x00000FFF)这个虚拟地址的物理地 址是1GB,于是MMU去访问内存条的1GB+16这个物理地址。
据以上分析,1级页表占据的内存是2的10次方,再乘以4,即4KB。而每个二级页表,也是2的10次方,再乘以4,即4KB。分级机制的主要好处是,二级页表不是一定存在了,比如一级页表 的第2行不命中,也即如下地址都无效的话:
那么这一行对应的二级页表,就整个都不需要了,于是就省掉了这段区间4KB二级页表的内存占用。页表当然还有是三级甚至更多。
至于有多级页表的时候,其实MMU也只需要知道一级页表的基地址即可。每次切换进程的时 候,把一级页表的地址重新填入MMU,把新的进程的页表激活即可。 在Android开发中,Linux中MMU内存管理也是重要部分;Android学习是永无止境的,技术不断更新。我们不能停滞不前有关更多的技术学习可以参考《Android核心技术手册》里面内容丰富适用于的大部分初中级程序员学习进阶。
文末:MMU的产生
许多年以前,当人们还在使用DOS或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以K为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以容纳当时的程序。但随着图形界面的兴起还用用户需求的不断增大,应用程序的规模也随之膨胀起来,终于一个难题出现在程序员的面前,那就是应用程序太大以至于内存容纳不下该程序,通常解决的办法是把程序分割成许多称为覆盖块(overlay)的片段。
覆盖块0首先运行,结束时他将调用另一个覆盖块。虽然覆盖块的交换是由OS完成的,但是必须先由程序员把程序先进行分割,这是一个费时费力的工作,而且相当枯燥。人们必须找到更好的办法从根本上解决这个问题。不久人们找到了一个办法,这就是虚拟存储器(virtual memory).虚拟存储器的基本思想是程序,数据,堆栈的总的大小可以超过物理存储器的大小,操作系统把当前使用的部分保留在内存中,而把其他未被使用的部分保存在磁盘上比如对一个16MB的程序和一个内存只有4MB的机器,OS通过选择,可以决定各个时刻将哪4M的内容保留在内存中,并在需要时在内存和磁盘间交换程序片段,这样就可以把这个16M的程序运行在一个只具有4M内存机器上了。而这个16M的程序在运行前不必由程序员进行分割。