物理内存虚拟内存以及段页表

  • 2022 年 11 月 10 日
  • 筆記

物理内存(物理地址)

这个是我们大家最能理解的,就是实实在在存在的内存空间。我们对内存的访问现在一般通过内存控制器。我们这里先要能够区别这里的内存空间并不是如外挂储存设备的nand/nor_flash这样设备中的存储空间,而是我们cpu直接能够按地址访问的空间。每一个字节有唯一的内存地址。物理上的地址总线的条数,决定着我们能够访问的大小。这应该好理解吧,因为当我们访问内存空间时,地址我们是通过地址总线一次传输过去的,比如我们有八位的地址总线,实际我们能够给出的地址只能是0-255,那每一个字节有唯一的地址,我们实际上也就最多只能访问256Byte的空间。简单来说,你可以把内存空间看作是一个大的网格,每个网格里装着1Byte,然后每个格子一个挨着一个,并且有唯一的位置信息。

 

虚拟内存(虚拟地址)

虚拟内存是针对物理内存的不足产生的,虚拟内存是物理内存的一种映射。要讲虚拟内存,不如我们讲讲它为什么会被引入,这样自然就清晰明了啦。

物理内存不足

当我们运行很多程序时,我们是将我们的程序放在内存空间执行的。也就是从硬盘中存储的可执行文件,把它通过执行器或者解释器放在了我们的内存空间中,官方一点说这叫做装载。假设一种情况,当运行的程序过大时,甚至大于我们的运行内存大小时,如果我们使用物理内存的方式,能不能实现将这段程序运行起来呢。是有可能可以的,因为运行程序不一定需要在同一时间全部加载到内存中去。比如2M大小的运行程序在1M大小的内存空间中,可能最开始程序的执行只依赖1M甚至不到1M的空间。我们完全可以只将即将运行和现在运行所必须的加载到内存中,待后面的内容需要运行时,我们再把后半部分的内容加载到内存中。那可能这跟虚拟内存又有什么关系呢?实际上,我们就是从可执行程序的角度提出了这一个概念。对于可执行程序而言,它是感知不到我们这样的操作的,在它的视角里,它跟拥有2M的内存空间是一样的,因为它完完全全运行在了内存空间中。简单来说,就好比一个小孩子(小明)需要人抱着,爸爸抱完,妈妈抱,但从小孩的视角来看,它只是知道它一直都有人抱着他而已。我们不想知道也不用程序去处理这样的发生在硬盘与内存之间的替换和加载,于是我们干脆给它说,你就是拥有2M的空间,所以我们引入了虚拟地址这个概念,程序使用我们的2M虚拟内存空间就可以了,它不用去关心这2M的虚拟地址是如何映射到我们的1M物理内存的,也不用知道何时发生替换的。(操作系统会处理好这件事情)

访问权限和保护

虚拟地址的引入带来的好处远不于此,而且还有其他场景下很重要的作用。我们通过刚刚上面看到了,通过虚拟地址的引入,对于程序而言我们实际上是不知道自己真实所访问物理空间地址的,但有趣的是我们绝对且实实在在地肯定是发生了对某个物理内存空间的访问的。所以这里相当于在物理地址与虚拟地址抽象出了一层,也就是我们操作系统,它负责将程序所访问的虚拟地址转换为某个物理地址,然后取得想要的东西再返回给我们的程序。通常情况下是这样的,但是当我们考虑一些特殊的时候,比如我们访问了一些操作系统也不知道的虚拟地址时,或者我们访问的虚拟地址所对应的物理地址已经被某段程序所使用时,麻烦就来了。如果我们直接使用物理地址,也无需操作系统,高度的自由就带来不可估量的后果了。所以这里虚拟地址,或者说被操作系统所妥善管理的虚拟地址就发挥作用了。操作系统清楚的知道它所给出的虚拟地址的情况,哪些是可被访问的,哪些是非法访问的,它就像一个检查官一样,它有权去拒绝或者转移我们无理的诉求。当不合法的虚拟地址并未被操作系统接纳并且翻译成物理地址访问时,我们只是想错的人,并不是犯错的人,因为我们的错误并未得到实施。所以这就是引入虚拟地址的好处,当然这个地方我想过,如果操作系统有一套针对物理地址的检查机制呢,那是不是其实也能做到这一点呢,这里我是这么想的,大家可能有更好的见解。我们都知道每个进程的虚拟地址空间是可以相同的,两个进程都可以访问同一个虚拟地址而不冲突,不互相影响,因为操作系统会把两个进程映射在两块不同的物理空间上。那比如我们同时运行50个进程,那当我们将程序从硬盘中加载到内存中时,有一个地址的重定位,即我们要确定程序加载到内存中时,各个部分的地址位置,虚拟地址的引入对这个的帮助是非常大的。因为我们不同进程在内存空间的虚拟地址分配可以是一致的。比如从哪里开始,每部分的位置。而多任务,多进程又是操作系统引入的至关重要的原因和核心功能。

段表

段表,我谈一点自己的理解,段表其实是对内存空间出于逻辑上的一个切分和管理。比如我们各个段,代码段,数据段,等等,这些划分其实跟内存分布没有任何的联系,它们的大小也不同,但是每个段都有各自所不同的属性,这对于内存访问是重要的。比如我们不希望改变我们代码段的东西,我们可以将其设置为只读的。段表的管理,其实较为简单,因为我们管理的段的数量是有限的。采用查询段表的形式,我们的逻辑地址(段描述信息+虚拟地址)中的前几位,是查表的索引,也就是所谓的段选择符,后面的位(一般后32位)为具体的段内偏移,通过段表+段选择符,我们能够找到该段的段描述符,通过段描述符我们能够得到一些该段的基本信息,其中就包括了段的访问特权级,段的基地址(该段第一个字节开始的位置),我们通过段基地址+偏移就可以得到该逻辑地址所对应的线性地址/虚拟地址。所谓的线性地址,再经过我们的页表处理转换后便能访问到我们真实的物理地址,所以这里也能看出来,我们的页表主要的作用是把线性地址管理起来,从而使之正确安全地能够访问到物理地址。

页表以及多级页表

页表管理除去访问保护等功能外,最重要的作用就是完成线性地址到物理地址的转换。最简单的思路肯定是一个线性地址对应一个物理地址,就像一个简单的一维数组,你给出索引,对应一个值。但是实际上,这个访问关系会随着数据量的扩张而变的夸张。因为我们地址的数太多了,比如32G,所以维护这样的一个数据结构是不现实的。我们采用页表,即我们把内存划分的更大一点,4KB而不是原来的1B(内存中一个唯一地址对应1B数据)作为一个内存块,也就是一页,我们的大小就迅速缩小了4K倍,在这样的方式下我们采用页基地址+偏移的方式去找到某个具体的地址,这个地址存放着我们的该线性地址所对应的物理地址。这是个好思路而且也能看出前人是真的聪明啊,但是即使这样,还是有很大的问题,32位的线性地址,一页4K,那页内偏移肯定需要12位,那还剩下20位就是我们所谓的页号,也就是某一页的索引。那也就是2的20次方个成员大小为4B(因为每一个索引对应着一个页地址,地址都是32位(默认是这样的情况,有些系统肯定也不只32位,但是不影响我们分析这个问题)也就是4B的嘛)的数组,那也就是4M的数据。我们每一个进程都是需要有自己的一个页表。(因为不同的进程实际是运行在不同的物理地址的,仔细想想你就会明白不可能多个进程共用一个页表)通常情况下,在系统中运行的进程数量是很多的,所以这样的方式也并不是很可行,当我们有200个进程时,我们需要花800M的内存空间去维护一个这样的东西西,amazing……)。于是有了多级页表,多级页表,即我们需要再多一次的访问才能取得页地址,即我们把二十位拆分为两个十位,第一个十位作为页表目录号,我们以它作为索引,建立一个目录表,它的每一项指向一个页表的基地址,第二个十位作为页号,以它作为索引建立一个页表,指明该页在该页表中的偏移,用来找到页表中具体的页基地址。我个人感觉有点把一维数组变成二维数组的感觉。但是这里我们发现我们从维护一个4M的一维数据结构,变成两个4k(2的十次乘以4B)一维的数据结构,这样的方式,虽然多了一次访存查表,但是使我们整体的思路变的可行了,我们一个进程若需要维护8K的数据结构,那1000个进程这样的情况下,所占用的内存资源也就是最早方案1个进程运行的两倍而已。(单独从地址转换这个角度来讲)。这里有一些时间换空间的感觉,但是想想还是很划算的。

TLB

这里我一直对它的概念就是像Cache,不过他加速或者优化的不是访存过程,而是查表。这里详细点说,我们cpu访问物理地址之前,需要先得到物理地址,而物理地址的获得就是我们这所有讨论的目的,它就安静地躺在某一页的某一个偏移的位置处。TLB则是缓存了我们最近访问的虚拟页对应的物理页的位置,当我们访问某个线性地址/虚拟地址时,我们会先对比我们所要访问的虚拟地址与TLB所缓存的虚拟地址的某些位是否一致,若一致则命中,就能直接从TLB中拿到该物理页地址,再加上偏移我们就能直接得到所要的物理地址,而无需访存,反之,未命中时,cpu也会从内存中的得到具体的物理地址,更新TLB表项。这里我们不做深入分析,我们就从访存的角度来看一下整个过程,首先CPU用的是虚拟地址,MMU需要把虚拟地址转换为物理地址,这里会如果采用单页表,也就是我们提到的类似于一维数组的情况,会发生一次访存(原因是该页表在内存中,而且我认为这个页表的物理地址我们是知道的而且必须是物理地址,不然我们怎么找到这个表的地址(那不就俄罗斯套娃了嘛),果然X86的CR3寄存器存的就是这个东西),如果我们采用多级页表,那就是上面我们提到 的两个小的表,那就是两次访存,如果TLB命中了那就是没有访存发生。当我们拿到了物理地址后,我们才会对物理内存进行访问,这里也是一次访存,这个地方又会引入Cache进行优化,类似于TLB查表,它会检查该物理地址是否最近访问过,然后………。

 

总结:

感觉还是很有收获,自己想着自己去做这些时的思路,再去参考现在这些系统实现的思路发现豁然开朗,也不禁感叹前人的智慧,但是更多的是觉得,当我们面临这些难题时,我们能不能自己先去想我们会怎么去解决呢,比如页表,多级页表,TLB,Cache,它们每一个的引入都是解决了我们计算机运行时的一个又一个问题。转变思路,接受理解别人的想法是很难的事情。因为自己都没有首先考虑过自己面临这些问题时自己的分析和答案。如果我们能自己先去思考,再去看他人,必定才能站的更高,看得更远!