背景
虚拟地址代表的是进程可以看到的地址,物理地址代表的是内存是一个绝对的位置。一个进程看到的虚拟地址在一个时间段里会被映射到内存上的一个物理地址。
为什么需要映射?
额外映射的需要我们维护虚拟地址和物理地址的关系,这个确实是一件很麻烦的事情,那么为什么我们需要这样'多此一举'呢?我认为重要是有两个点非常关键。
- 进程之间的隔离 (Provide Process Isolation)
- 简化编程 (Increase Programming Flexibility)
进程之间的隔离
这个我觉得是一个安全和稳定性问题上的考虑吧,如果每一个进程可以直接访问和修改一个被分配给其他进程的内存空间(这不是一个安全行为如果一个恶意程序去访问一个敏感位置的内存),这个势必导致一个进程有风险去影响其他进程的运行甚至导致系统崩溃(系统核心进程被干扰了)。为了避免这些情况,干脆只给进程分配独立内存空间(其他进程不会分配到同样的内存空间),这样一个进程运行的时候使用的内存空间都不会被其他进程使用同时这个进程也不会影响其他进程。
操作系统提供了进程之间通信的机制(IPC, Inter-Process Communication)去满足需要进程之间流通数据的需求。
简化编程
为什么会说这个有帮助我们编程呢?可以想象一下如果没有映射关系的话,我们想获取一个内存空间要怎么办呢。一个很好的例子是如果要分配一个很长的连续地址给我的进程,我们可能要自己处理去寻找一块连续的空间给我的进程, 有时候甚至找不到足够的连续空间给我们。我们还有很多空间可以分配啊只是没有连续的,难道我们要一直等待空间释放到有足够的连续空间给我们吗?当然不是了,我们只能把很长的连续空间分配切分成多个短一点的连续的空间分配。只是这样可能我们不能只记住一个基址(base address), 所有连续空间的基址都要记住而且还得记住他们的顺序。
上述的事情如果每一个开发者写代码的时候如果要自己处理是很繁琐的事情,映射很完美的处理了这个问题。我们编程的时候会要一块连续的空间,我们会获取到一块连续的虚拟空间,我们只需要获取这个空间的基址就好了,通过这个推出来后面空间的虚拟地址,每一个虚拟地址都被映射到了内存上的一个物理地址,至于这个物理地址是不是连续的,这个开发者不需要考虑了。
物理地址和虚拟地址到底是什么?
物理地址是什么?
物理地址是内存中一个byte的地址。内存的空间也引入了分页机制(Paging)去切割全部的空间成一个个小的连续的内存空间也就是页框(Page Frame)。所以物理地址也分成了页框索引(Frame Index)和页内偏移量(Offset)。物理地址帮助快速定位一个页框中的一个字节。
物理地址有多大?
物理地址的上限主要取决于处理器的地址总线宽度和内存容量。 对于32bits的系统地址被处理器的地址总线宽度限制了,处理器能寻址最大到2^32。所以最多可以有2^32 个bytes = 4G,因为每一个byte都得有自己的地址。 对于64bits的系统通常有了足够的地址分配给更多的bytes, 2^64 = 16EB,远远大于我们需要的内存数量所以通常处理器会限制总线宽度到48就是2^48 bytes = 256TB 或者 52就是2^52 bytes = 4PB就完全足够了。但是我们一般没有这么多的内存所以物理地址被我们的内存容量限制了。
虚拟地址是什么?
虚拟地址是进程可以看到的地址,它不是真正存在在硬件上的。同时进程可以看到的虚拟地址都是独立的,相同的虚拟地址会在不同的进程里面使用但是这些虚拟地址会被映射到不同的物理地址。进程在使用一个虚拟地址上做一些操作的时候,虚拟地址会被映射到一个物理地址,所有的操作都是使用这个物理地址的。
虚拟地址有多大?
虚拟地址的大小取决于处理器的地址总线宽度。32bits的系统也是只能有2^32个地址然后64bits的系统有远远超过需求的2^64的地址。通常来说,实际的实现通常仅支持 48 位 或 57 位的虚拟地址(在Page Table部分会解释一下为什么是57)。
系统如何维护映射关系的呢?
页表(Page Table)
每个进程都有独立的虚拟空间所以系统需要为每一个进程维护映射关系。页表就是操作系统维护来处理一个进程的映射关系的数据结构。
页表项(PTE, Page Table Entry)
页表项是页表里面储存的内容,长度有32/64 bits。如果在64bits的系统里面,数量就是 4k / 8bytes = 512个,所以我们需要9个bits去表示页表项目的索引。 页表项的内部有有12位的控制符(Contribute bits)。然后就是最重要的物理页框的索引。最后剩余的空间会空着。
多层页表 (Multilevel Page Table)
多层页表是页表的一种类型,它是为了更好的管理关于一个进程的全部的映射关系。
我们可以从图上看到,我们需要在每层找到对应的页表项,然后利用这个页表项去跳转到下层的对应的页表。虚拟地址会帮助我们定位每一层的页表具体位置不需要遍历全部的页表。最后定位获得的是物理页框(Frame),配合虚拟地址里面的偏移量(Offset)就是最后的地址。
四级页表 (four-level page table structure)
一种常用的多页表结构,每一层都有自己的名字
- PML4(Page Map Level 4)
- PDPT(Page Directory Pointer Table,页目录指针表)
- PD(Page Directory,页目录表)
- PT(Page Table,页表)
利用四级页表我们也就需要跳转3次就到了最下层的页表了
为什么需要多层页表不是一个大页表?
为了储存一个进程的全部映射关系,我们可能需要成千上万的页框去储存它,如果我们想通过索引快速定位需要的映射关系,我们需要一开始就分配连续的一段空间,很明显这不是一个好想法。多层页面很好的解决了这个问题,让空间动态的分配给映射关系同时又不会拖累查询映射关系的速度。
虚拟地址是如何通过多层页表来找到映射的物理地址的?
虚拟地址切分成多段,前面的每一段代表着在每一层页表的索引来帮助我们快速定位到对应页表项,每一段的长度是9bits可以提供512个地址给一个页表里面的全部512个页面项。
拿四级页表举例子, 我们有一个虚拟地址 index 1 | index 2 | index 3 | index 4 | offset, 分成了五个部分,前面四个index是每层的页表的索引,后面的是最后物理地址的偏移量。前面的四个index需要36bits(4 * 9 = 36)储存,后面的偏移量需要12个bits。
首先我们找到PML4也就是最上面层的页表,然后index 1来定位到页面项指向下面一层的页表也就是PDPT。我们继续使用index 2在PDPT寻找对应的页面项对应下层的PD。按照这个逻辑我们可以找到PT里面的页面项对应着物理页框的位置。最后加上偏移量就是这个物理页框上中一个字节的物理位置。
如果更多层的页表我们就需要更多段index来定位,比如5层的话,虚拟地址得多加9bits也就是一共57bits。
如何添加一个映射关系?
一个非常关键的点是一个进程刚刚开始的时候,虚拟地址和物理地址是还没有存在映射关系,系统不会初始化把全部的虚拟地址一一对应提前映射好因为提前分配物理空间是浪费的其次虚拟空间太大了映射不完。
如果进程尝试使用一个虚拟地址,就是按照查找映射关系一样一层层寻找,如果发现不存在,那就马上创建页表项来添加这个映射关系。所以映射关系是动态添加的,提高效率也减少浪费的内存空间。
总结归纳
- 虚拟地址是页为单位,物理地址是页框为单位,他们的大小都是4K, 所以定位他们中的一个字节需要12bits的偏移量来寻址。
- 物理地址通常是48bits,12bits表达了偏移量,剩下的36bits来定位全部的页框。
- 一个页面项是64个bits,所以一个页表里面有512个页项目,需要9bits来表示它的索引。
- 一个页面项包含了12bits的控制项,然后就是包含了36bits的页框位置,剩下空间保留。