Linux 内存管理浅析

来源:互联网 发布:积分统计软件 编辑:程序博客网 时间:2024/06/05 09:43

(5). MMU(Memory Management Units)介绍

这一部分,和平台相关性非常大,因为每个CPU的MMU都有可能有自己的特性。这里我会以e6500平台为例子,介绍MMU的工作机制。

我们前面介绍了页面映射,其实就是在内存中建立了一个页表,当进程访问虚拟地址时,查询这个表,以获得实际的物理地址,那么这个表是由CPU(系统软件)来查询的吗?

如果CPU是工作在虚拟地址模式下,它实际上是没有办法直接访问到物理地址的。在这种情况下,也即意味着CPU不能够直接查询页表。这时候,必须要有MMU来执行这种页表查询操作。MMU也有自己的chahe,称之为TLB(Translation Look-aside Buffer),用来缓存PTE。

前面我说过,进程其实是一个抽象的空间,在这个空间中,程序编制者可以不用考虑实际的物理内存管理。每个进程有自己的虚拟地址空间,是进程私有的,CPU可以任意访问这个地址空间。而对于系统来说,我们需要考虑实际的物理地址空间,它是和具体设备相关(或者外部资源)的,内核需要在进程的虚拟地址和实际的物理地址之间建立一种映射关系。

我们可以这样说,用户进程需要解决的是特定的问题,而内核进程需要解决的是计算机内部的问题。什么是计算机内部的问题呢?这里我把像进程调度,内存管理,异常处理,外设管理这些问题归结为计算机自己需要解决的问题,或者是计算机自己的管理问题。

就像面向对象的编程一样,我们也可以把内核进程需要解决的问题,用面向对象的方式来理解。比如进程调度,那么内核可以把每个进程抽象成一个实体,这个实体具有自己的行为和记录这些行为的数据结构。对于内核进程调度模块来说,这些实体,就是其操作的对象。而 struct task_struct 就是这样一个抽象的类,它代表了一个进程在内核进程空间中的抽象。而 struct mm_struct 则描述了进程空间中的地址空间抽象。这里我再说下进程中的线程。其实上面的task_sturct结构体在linux中描述的是一个线程抽象。线程与进程的区别在于,进程是一个空间,线程是在这个空间中的事件。

为了理解这个概念,我们来看下现代几乎所有计算机都采用的架构(冯.诺依曼计算模型)。在这个架构中,CPU负责计算,通过从内存中获得指令和数据运行,在把计算结果保存到内存中,就是这么简单。那么我们理解的进程空间,就可以这样的。在这个空间中,有一个CPU和一个窗口(地址空间),CPU通过这个窗口获取指令和数据进行计算,再把结果通过这个窗口进行保存。至于这个窗口后面有什么,CPU是不关心的。如果CPU不去获得指令运行,那么这个进程空间我们说是不存在的,因为没有事件发生。就像我们这个宇宙,如果没有事件发生,那么我们的时间,空间也不存在一样。而在进程这个空间中发生的一系列事件组合,我们可以理解为就是一个线程。如果CPU执行了一些代码,发生的一系列事件,是有关联的,或者说编程者认为这些是逻辑上相关的,那么我们可以用一个线程来描述它们。在这个线程中发生的事件,是有因果关系的,或者说是顺序发生的。那么我们可以说在这个进程空间中的另外一个线程就是另外一些事件的组合。

之所以说这么多关于线程和进程的区别,是因为在进程或线程发生切换时,对于地址空间的切换是不同的。还是上面的例子,如果CPU需要执行一些与前面的事件没有关联的代码,我们就说这是一系列新事件的开始(或者是被打断执行的事件的继续)。那么CPU需要把当前运行的过程保存下来,这样就可以在以后某个时间继续运行。这里我们说发生了线程的切换。注意线程的切换,我们需要记录下当前线程中CPU的执行过程。这里我们并没有切换窗口,就是说我们没有赋予窗口新的含义,我们没有改变这样一个进程空间,这个进程空间还是原来那个空间。当我们赋予窗口新的含义时,那么这个进程空间也就发生了改变,这时我们说发生了进程切换。也就是说,我们映射了新的进程虚拟地址空间到外部的物理地址空间。虽然对于CPU来说,窗口还是看起来一样的。

MMU做的工作就是通过查询映射表完成这样一个映射过程。在这个过程中,我看可以看到还具有一个副作用,就是我们可以施加某些控制。我们可以在页表中放置一些标志位,用来表示当前物理页面,哪些是只读的,哪些是只有内核可以访问的等等(前面在介绍PTE时,已经做了相应的了解)。MMU在查询到页表,进行地址转换之前,会检查这些标志位,以确定是否需要转换地址。

下面我们看下e6500 CPU的MMU大致结构:

这里写图片描述


e6500 MMU具有下面几个特性:

  • 支持64bit的虚拟地址和最大40bit的物理地址转换。

  • 具有两级TLB。前面说过,MMU通过将PTE缓存到TLB中,以加速查找过程。类似于CPU cache,MMU也具有L1和L2两级TLB。L1是L2的缓存,L2是L1的后备存储。当在L1中没有查询到相应的PTE时,会在L2中进行再一次查找,如果找到了,则会更新到L1中。同时指令TLB和数据TLB在L1中是分开独立的。

  • e6500具有4个core共8个硬件线程。每一个core有两个线程,这两个线程会共享一部分硬件,其中MMU部分的L2 TLB和L1 指令TLB是共享的。这两个线程有独自的L1 数据TLB。这个会对后面我们介绍的MMU工作过程会有影响。

  • 支持可变size页表映射。L1和L2 TLB都各具有固定4K页面映射的TLB和可变size映射的TLB。 在MMU查询的过程中,对于这两个TLB是并行查询的。特别的,当我们在后面部分说起TLB0和TLB1的时候,指的是L2 TLB。

  • L1中 的TLB只能MMU自己操作。L2的TLB0 和 TLB1,软件能够通过MAS寄存器进行控制。后面我会简单叙述软件控制过程。TLB1比较特殊,它的entry具有indirect位和IPROT位,同时只有软件能够操作,具体作用后面会介绍。而TLB0 MMU能够操作。

  • 对L2 TLB0和TLB1的软件操作,e6500提供了MAS(MMUs assist registers)寄存器,软件只能将需要控制的内容先写入到MAS寄存器中,然后通过相应的指令,完成对TLB的操作。具体的TLB指令,我不想在这里叙述,只会在后面需要描述具体过程时,如果碰到,我会稍微介绍下。这些指令可以查阅具体的文档。


TLB(Translation Look-aside Buffer)就是MMU的缓存,它和CPU缓存具有同样的结构。我们只需要知道在TLB中,数据都是按行存放的,一行称之为一个entry。一个entry包含了若干bit位,下面是它的bit位含义描述:

这里写图片描述

我们可以看到,这里的一些标志位,和PTE页表中的标志位是对应的。

  • V,这个标志位表明当前entry是否有效,MMU查询会忽略无效的entry。

  • TS,TGS。TS对应于虚拟地址中的AS,TGS对应于虚拟地址中的GS,以进行某些页面访问控制。需要这些标志位匹配,才能称之为TLB命中。因为实际上内核没有用到,这里我不详细介绍。

  • WIMGE。这个其实和MMU关系不大,影响的是CPU缓存的行为。因为我们不能对CPU缓存进行直接操作,所以我们通过设置这些标志位,实现对CPU缓存行为的控制。具体可以查看相应文档,了解这些标志位的含义。

  • TID。这个比较重要,对应于虚拟地址中的PID。前面我说了,L2 TLB和L1的指令TLB是两个CPU硬件线程共享的。这两个CPU硬件线程可以并行的执行两个软件线程,如果这两个软件线程不在同一个进程空间,那么在TLB中,我们怎么区分这两个进程地址空间呢?答案就是,在虚拟地址中,CPU会设置一个当前进程地址空间的PID。通过比较PID与TLB entry中的TID,我们能够区分这两个不同的进程地址空间。

  • SIZE。这个是映射的物理页面大小。对于TLB0来说,就是固定的4KB。对于TLB1,我们可以配置4KB-1TB。

  • UX,UR,UW,SX,SR,SW。这六个标志位,是页面访问的权限控制位。在TLB查询过程中,不会比较这些标志位。在命中一个entry后,MMU才会检查这些标志位,以确定当前进程是否有权访问这个物理页面。CPU分为特权模式和用户模式。S代表CPU在特权模式下是否可以访问,而U则表示用户模式下是否可以访问。一般的,内核工作在特权模式,而用户进程工作在用户模式。X代表代码执行,控制当前页面是否能被指令获取单元访问。R和W分别代表数据的读和写,控制是否能够被load和store指令访问。这些标志位定义在:arch\powerpc\include\asm\pte-book3e.h

  • EPN。虚拟地址的页面号。虚拟地址我们知道,其实是没有页面编号的。但由于它会映射到某个物理页面,我们也就认为这一段映射的地址是页面对齐的。就是说,虚拟地址的12bit(4KB页面)低位,我们可以屏蔽掉。这样屏蔽掉12bit低位的虚拟地址,就是EPN。

  • RPN。实际物理地址页面号。对应于PTE中的物理页面编号PFN。

  • IPROT。是entry无效保护位。只在TLB1中有效。后面我们会看到,在进程切换时,会把被切换进程的TLB entry都使能无效,这样在下一个进程访问地址空间时,不至于发生冲突。内核的进程空间地址,有一部分是线性映射的,这一部分地址我们要确保在任何时候都不能被从TLB中切换出去。如果不小心无效了这个地址空间映射的entry,整个系统就完蛋了。所以,我们可以把这样的entry设置IPROT位,以确保entry一直有效。前面说过,TLB1只有软件能够控制,因此我们只有把关键的地址映射放到TLB1中,才能确保不被硬件MMU替换掉。

  • IND。只在TLB1中有效。在e6500 MMU中,如果在TLB中没有找到对应的PTE,则会进一步在TLB1中查找PMD。这个标志位就是表示当前entry是一个PMD,它里面保存的是PTE的地址。MMU会根据这个地址自己去内存中查找PTE,并把PTE的内容更新到TLB0中。这里我们称之为发生了部分TLB miss。而这个miss的处理过程,是由硬件MMU来完成的。


前面我们说过,对于地址转换,我们能够施加某些控制。通过比较PTE页表上的某些标志位,我们能够确定某些权限控制。那么是什么与PTE页表上标志位比较呢?在进行地址转换时,很明显,虚拟地址会送到MMU进行转换。很自然的,我们如果在虚拟地址里增加一些bit位作为标志位,那么MMU就能在查询PTE时,同时比较这些标志位了。

在e6500中,CPU扩展虚拟地址的bit位,用来存放某些标志位,这些标志是通过一些CPU内部的特殊寄存器(MSR)获取的。形成的新的地址,在e6500中,称之为虚拟地址(VA)。而原来的虚拟地址,称之为有效地址(EA)。EA在e6500中是64 bit,而VA则有81 bit。实际MMU进行TLB查询的地址是VA。下面这幅图显示了这两种地址的差别。


这里写图片描述


前面介绍了TLB中相应的标志位与虚拟地址标志位的关系,这里不再重复。下面介绍下MMU的查找过程。

  • MMU首先根据VA查找L1 TLB中entry,如果找到,称为TLB命中。将相应RPN和页面内部地址送到地址总线,完成地址转换。

  • MMU如果在L1中没有找到,则会进一步查找L2 TLB,如果查找到,则会将其更新到L1 TLB中,完成地址转换。注意这里,如果是在TLB0中查找到的,则更新到L1 4KB TLB中,如果是在TLB1查找到的则会更新到L1 VS TLB中。地址转换和更新其实是同步进行的。如果L1 TLB中没有足够entry容纳新的PTE,则会发生L1 TLB entry替换,具体的替换策略是称之为LRU的方法。LRU很多文档都有介绍,这里不再赘述。

  • 一旦L2中也没有查找到对应的entry,那么会进一步在TLB1中查找设置了IND标志位的entry是否匹配。如果查找到,那么我们称之为发生了部分TLB miss。MMU会根据这个entry中的PTE地址,在内存中查找相应的PTE,如果查找到,则更新PTE到TLB0中。注意TLB0是固定4KB页面映射的,那么,如果我们配置了不同size的页面映射,并且发生了部分 TLB miss,那么MMU是没有办法将其更新到TLB1中的。后面介绍huge page时,我们还会讨论这个问题。

  • 如果没有在IND entry中查找到PTE地址,那么MMU会抛出一个TLB miss异常,需要软件来完成查找过程。这个后面会详述。

  • 如果在IND entry中查找到PTE地址,但从内存中读取PTE的内容无效,这说明当前访问的虚拟地址并没有映射到一个有效的物理页面。MMU会抛出一个data_storage或instruction_storage异常,也就是发生了我们平常所说的缺页异常。这部分我会在用户空间进程地址管理部分详细介绍。

注意上面MMU查找TLB entry,会根据entry中size大小,取VA地址中相应的bit位与entry中的EPN进行比较。如果size是4KB,那么就取VA地址高52bit进行比较进行匹配查找。

从上面过程看出,e6500 MMU 具有两步TLB查找过程,如果没有直接找到对应PTE,那么会查找上一级页表(我们这里是PMD,保存在IND entry中)。通过两级页表的查询,对TLB存储限制和查询时间做出了平衡。其他平台CPU也大都采用了这样一种设计。

上面过程说到了软件部分需要处理的TLB miss异常。我们可以看到,软件不需要将PTE直接更新到TLB0或TLB1中。软件只需要将PMD更新到TLB1中并设置IND位就可以了。MMU会自动将PTE更新到TLB0中。但这里有个前提,就是自动更新的PTE我们不能设置成其他size的页面映射,必须是固定4KB大小的PTE。

下面这张图大致描述了MMU 查询TLB过程。

这里写图片描述


下一节我们看下页面映射软件处理部分。

原创粉丝点击