前言
在多任务的处理器上,往往运行着许多的用户进程,这些进程之间相互隔离,它们都有自己的虚拟存储空间。要实现这样的虚拟存储空间,需要可以进行地址重分配以及虚拟地址到物理地址的转换。
MMU就是实现这种功能的硬件部件,它负责将数据和代码的虚拟地址转换为物理地址。程序里面使用的都是虚拟地址,所以CPU发出的地址是虚拟地址;但是外设访问使用的是物理地址,所以CPU发出的虚拟地址需要经过MMU转换为物理地址后,才能送到地址总线上去做访问操作。地址转换的过程由MMU硬件自动执行,对应用程序来说是透明的,应用程序感受不到MMU的存在。除此之外,MMU还负责管理各个存储区域的访问权限,存储序以及缓存策略等等。
在多任务的嵌入式系统里面,整个物理存储空间可能被分成很多块(比如主存,FLASH,外设寄存器)。这些存储块被分配不同的访问权限以及存储属性,并且这些存储块的物理地址可以是不连续的。我们可以设置MMU,让它将虚拟地址映射到这些不连续的物理地址上,这样我们写程序的时候就可以使用连续的虚拟地址,并不用关心实际的存储系统是怎么分布的。应用程序的编写,编译和链接都是用的虚拟地址,程序运行在虚拟存储空间;实际的硬件系统使用的是物理地址。换句话说,程序员和编译工具链使用的是虚拟地址;硬件系统使用的是物理地址。
MMU一般由操作系统来设置,用来实现虚拟存储空间到物理存储空间的转换。当MMU没有启用的时候,虚拟地址被直接当作物理地址使用。如果一个虚拟地址无法被MMU转换,MMU会产生一个abort异常送到对应的CPU并提供引起问题的信息。
虚拟存储
MMU可以让我们为应用进程设置不同的虚拟地址映射,每个应用进程都有自己的虚拟存储映射,对不同的应用程序,同一个虚拟地址可以映射为不同的物理地址,这是通过使用转换表(Translation Tables,后面使用TTs)实现的。TTs是由软件在内存里面创建的一段树形表数据结构,MMU硬件通过爬TTs的方式完成地址转换。
每个转换表都有一个转换表入口(Translation Table Entry,后面使用TTE),这些Entries通过虚拟地址组织起来,称为(Translation Table Entries,后面使用TTEs表示)。在ARM架构里面,一个TTE包含将虚拟页转换为物理页所需的全部信息,并且提供这个页的访问权限和存储属性。
如下是MMU在虚拟存储系统中的位置示意图:
当MMU功能开启后,CPU发出的所有访存操作都要经过它。CPU核发出的地址是虚拟地址,虚拟地址送入MMU转换后得到物理地址,然后物理地址送入存储系统的下一级(这里是Caches,前面已经讲过了缓存的内容)。本质上来说,MMU通过将虚拟地址的高位部分替换为其它值来得到对应的物理地址,这个地址一般都是该虚拟地址对应的物理存储块的基地址。
MMU包含两个主要的部件,一个是爬表单元(Table Walk Uint,后面用TWU表示),它用于从内存里面读取TTs。另一个是转换表后备缓存(Translation Lookaside Buffers,后面用TLBs表示),用于缓存最近执行过的页地址转换,这部分放到转换表入口之后讲,会更容易理解。
配置和启用MMU
在启用MMU之前,必须先将转换表TTs写到内存里面,然后设置TTBR寄存器指向TTs。如下是开启MMU的指令序列:
MRC p15, 0, R1, c1, C0, 0 ;Read control register
ORR R1, #0x1 ;Set M bit
MCR p15, 0,R1,C1, C0,0 ;Write control register and enable MMU
需要注意的是如果MMU的开启改变了当前代码运行存储区域的地址映射,可能需要使用Barriers来确保操作正确。
页大小
page的大小通常是由操作系统来设置的,但是了解如何选择页大小是有好处的。更小的页可以更精细地控制存储块(包括访问权限,缓存策略),减少页内存储空间的浪费。比如当我们在程序里面分配7KB的内存空间,分配两个4KB的页造成的浪费要比分配一个64K的页要小很多。然而,更大的页可以提高MMU的页地址转换命中率,因为每个TLB缓存索引的存储区域变得更大。
一级地址转换(First level address translation)
这部分内容讨论使用ARM核上的一级转换表(level 1 translation table)将虚拟地址转换为物理地址的过程。所谓的一级地址转换,是指只用一级转换表经过一次转换就可以得到物理地址。一级转换表又称为主转换表(master translation table),它将整个的32位的4G虚拟地址空间划分为大小相等的4096个存储区域,每个区域大小为1MB。故一级转换表有4096个32位的入口,每个入口包含的可以是一个二级转换表地址,也可以是一个1M区域的基地址(还可能是其它内容),一级转换时对应的入口是一个1M区域转换入口,入口包含这1M存储空间对应的物理基地址。对于32位的虚拟地址,高12位用来表示转换表的入口索引,低20位为1M区域里面的偏移量。这样,我们先通过入口索引找到对应的入口,然后从入口里得到1M区域的物理基地址,加上20位的偏移量,就是一个虚拟地址对应的物理地址了。举个简单的例子,如下图:
假设我们把一级转换表放在0x12300000的内存地址上,CPU发出0x00100012的虚拟地址。这个虚拟地址的index部分位0x001,则表明其对应的转换表入口为第1个,入口对应的内存地址为(Translation Table Base Address)0x12300000 + (index)0x001 * (entry size)4 = 0x12300004。假设该地址保存的数据为0x081XXXX(入口的高12位表示这1M区域的物理基地址),则得到物理地址为0x08100000 + (offset)0x00012 = 0x08100012。
如下图是一级转换表入口的四种格式:
这4种入口格式对应了4种不同类型的入口:
- 一个转换1M大小区域的入口,将虚拟地址空间中的一个1M大小的存储空间映射到指定物理地址的大小为1M的物理存储空间。
- 一个指向2级转换表的入口,可以通过使用2级转换表将1M的存储区域进一步划分为更小的page。
- 一个转换16MB超级区域的入口,这种入口是一种特殊的1M转换入口,它占用16个入口的存储空间,但是可以减少这片存储区域在TLBs里面的入口数量。
- 可以生成abort异常的错误入口,通常这意味着给定的虚拟地址未被映射(也即不可使用)。这个异常要么是指令预取异常,要么是数据异常,具体取决于访问类型。
入口的低2位[1:0]表明了其是错误入口,转换表入口还是区域转换入口。第[18]位用于进一步区分是普通的区域转换入口还是超级区域转换入口。
对于16M的超级区域,其虚拟基地址和物理基地址都必须对齐到16M。因为每个1级转换表入口只对应1M的区域,故其需要使用16个连续的且相同的入口来表示。
如下是使用一级转换表将一个虚拟地址转换为物理地址的大致过程:
虚拟地址里面的index部分加上TTBR寄存器里面的值,得到一级转换表里面对应的入口。入口里面的Section Base Address表示该虚拟地址对应的物理存储区域的物理基地址。物理基地址加上Section index部分(同虚拟地址里面的Page index部分)后,即得到物理地址。具体的物理地址计算过程如下图:
一级转换表转换虚拟地址为物理地址的过程可以简单理解为:将虚拟地址的高12为替换为其它的值,具体替换为什么值取决于对应的一级转换表入口里面的Section Base Address部分的值。
入口里面的其它位的含义在这里稍作解释:AP(Access Permissions)表示访问权限,C(Cacheable)和B(Bufferable)表示缓存和缓冲的类型,这些内容在后面的存储属性部分会学习。
二级转换表(Level 2 translation tables)
二级转换表有256个4字节的入口,需要占用1KB的内存空间,并且二级转换表需要对齐到1KB的地址处。我们可以使用二级转换表进一步将1MB的存储区域划分为更小的页来进行管理,一个二级转换表入口将1MB/256=4KB的大小的虚拟存储区域映射到相同大小的物理存储区域。这个4K的区域称为一页(PAGE),二级转换表入口可以给出4K大小的页和64K大小的页的物理基地址,共有3种不同的二级转换表入口格式,它们对应3种不同的二级转换表入口,使用低两位[1:0]来进行区分,如下图:
- 代表4KB页(Small page)的入口。
- 代表64KB页(Large page)的入口。
- 代表错误页(表示该页未被映射,不可使用)的入口,对该页的使用会引发abort异常。
跟一级转换表入口类似,二级转换表的入口页包含页的物理基地址,以及其它的控制该页存储属性的标志位:比如类型扩展 (TEX),共享(S),访问权限(AP,APX)等等。B和C跟TEX搭配起来使用,可以控制页的缓存策略。nG位表明页是否为全局的(是否可由多个进程共同使用)。
二级转换的过程如下图所示:
使用二级转换的时候,虚拟地址的高12位[31:20]用于表示一级转换表入口的index,随后的8位[19:12]用于表示二级转换表入口的index,剩下的12位[11:0]用于表示4K页内的偏移量。物理地址的具体计算方法如下图所示:
先从虚拟地址里面取出一级转换表入口index,加上TTBR寄存器,找到对应的一级转换表入口。此时该一级转换表入口的类型应为一个指向二级转换表的入口,里面保存了二级转换表的起始地址。再从虚拟地址里面取出二级转换表入口index,加上刚才得到的二级转换表起始地址,得到对应的二级转换表入口。该二级转换表入口的类型应为一个4K的页入口,里面包含该虚拟地址映射到的4K物理存储区域的起始物理地址。该起始物理地址加上虚拟地址中的偏移部分,就是该虚拟地址对应的物理地址。
二级转换表转换虚拟地址为物理地址的过程可以简单理解为:将虚拟地址的高20为替换为其它的值,具体替换为什么值取决于对应的二级转换表入口里面的Small Page Base Address部分的值。
存储属性(Memory Attributes)
前面已经多次提到转换表入口除了指定虚拟地址对应的物理存储区域的起始地址,还可以设置该存储区域的存储属性,这部分就来具体学习了解存储属性部分的内容。
-
存储访问权限(Memory Access Permissions)
转换表入口里面的AP和APX指定了对应存储区域的访问权限,无对应权限的访问行为会引发abort异常,引发异常的地址和原因被保存在CP15的寄存器中(the fault address and fault status registers)。在abort异常的handler中,我们可以采取一些措施来对访问异常进行补救,比如修改转换表,然后返回重新尝试访问。也可以直接将产生访问异常的应用视为有问题的应用,将其终止。下表是存储访问权限标志组合:
-
存储类型( Memory types)
ARM架构定义了3种互斥的存储类型:严格顺序(Strongly-ordered)类型,设备(Device)类型,常规(Normal)类型,所有的存储区域都必须设置为其中一种存储类型。下表汇总了3种存储类型的存储区域具备的属性:
下表是存储类型与转换表入口里面的标志位对应关系:
可以看到,只有Normal类型的存储区域是可缓存的。 -
不可执行(Execute Never)
当XN位被设置,表示不可从该存储区域取指令。如果尝试从该存储区域取指令,将产生取指异常(prefetch abort)。通常,需要为Device类型的存储区域设置XN位来防止意外地从这些存储区域取指令。 -
域(Domains)
ARM架构有一项非比寻常的特性:可以给不同的存储区域打上一个域ID,形成一个域,共有16个可用的硬编码域ID。对于每个域ID,CP15的c3里面的域访问控制寄存器Domain Access Control Register (DACR)都提供了一套两位的权限控制位,这些控制位可以将每个域标识为不可访问(no-access),管理员模式(manager mode)或者用户模式(client mode)。对于标识为不可访问的域,任何对域内存储区域的访问都会引发异常,不管转换表入口里面设置的存储访问权限是什么。对于标识为管理员模式的域,域内的存储区域可完全被访问,不管转换表入口里面设置的存储访问权限是什么。对于标识为用户模式的域,域内的存储区域按照转换表入口里面设置的存储访问权限来访问。
要注意的是,ARMv7里面已经摒弃了域的使用,后面该特性也会被移除。
转换表后备缓存( The Translation Lookaside Buffer)
TLBs用于缓存最近执行过的地址转换信息。对于一个访存操作,MMU首先在TLBs里面检查是否缓存了对应的转换信息。如果有对应的缓存则称为TLB命中,可以立即转换得到物理地址,不用再到内存里面去找转换表;否则称为未命中,需要通过TWU到内存里面去读取转换表,然后转换得到物理地址,这个新的转换会被缓存到TLBs中。
TLB的结构在不同的ARM处理器实现上可能不相同,如下是一个典型的TLB结构示意图:
TLBs缓存的是虚拟地址到物理地址的转换信息(更具体地说,是虚拟区域/虚拟页地址到物理区域/物理页地址的转换信息),那么其缓存条目中固然要包含虚拟页地址和物理页地址;除此之外,还会包含对应区域/页的存储属性。如上图所示,VA表示虚拟地址,PA表示物理地址,Attributes表示存储属性。可以发现,PA+Attributes部分其实就是转换表入口里的内容,也就是说TLBs缓存的其实是最近用过的转换表入口,只不过还需要为其增加Tag来用于查找缓存的转换表入口。ASID的作用先不用管,下一部分就会讲到。
一个TLBs缓存条目称为TLB入口,TLBs同其它缓存一样,有自己的替换策略,但是对用户来说是透明的,我们不用关心。ARM架构要求TLBs里面只能缓存有效的转换表入口。
TLBs同样需要解决缓存一致性的问题,当操作系统改变TTEs后,TLBs的某个条目里面可能缓存了过时的转换信息,操作系统必须要对该条目进行无效化。这可以通过操作CP15协处理器来完成,linux内核封装了几个对应的函数,比如flush_tlb_all()用于清空所有的TLBs缓存条目,flush_tlb_range()用于清空指定范围的TLBs缓存条目。一般来说,设备驱动里面都不会用到这些函数。
转换表在多任务操作系统里面的使用
对于大多数使用Cortex-A系列处理器的系统,都会同时运行多个应用或任务,每个任务都可以在内存里面有自己的转换表。通常来说,存储系统的大部分存储区域经过设计和组织之后,这些区域的虚拟存储空间到物理存储空间的转换就是固定的,转换表入口不会改变。这些固定的存储区域通常用于存储操作系统的代码和数据,以及用于存放用户任务使用的转换表。
任务的转换表是属于任务自己的,可能随着任务的新建和消亡动态变化。当一个任务启动的时候,操作系统会给它分配一个转换表,同时用于任务代码和数据的虚拟地址转换。如果任务里面需要使用额外的存储区域(比如使用malloc分配额外的存储空间),内核可以修改转换表,增加新的转换入口。当任务结束运行时,内核会移除跟应用关联的转换表,对应的存储空间可以再分配给其它的应用使用。这样,内存里面便可以驻留多个任务的信息,实现多任务同时运行。在任务切换的时候,内核也会将转换表切换为下一个任务的转换表,这样就不会影响到休眠任务的内存数据。也就是说各个任务使用各自的转换表,在物理内存里面操作和维护自己占用的存储空间,实现各自之间代码和数据的隔离。
前面在讲解二级转换表入口的时候,提到了一个标志位nG(非全局)。如果入口设置了该标志位,表示这个页为非全局的 ,它关联到一个特定的应用。此时,MMU做地址转换的时候,需要同时使用虚拟地址和ASID(Address Space ID)。ASID是由操作系统给各个任务分配的一个介于0~255之间的数字,当前任务的ASID值通过CP15的c13写到ASID寄存器里面。当设置了nG位的转换表入口被更新到TLB缓存里面的时候,ASID值也会被保存到TLB缓存条目里面,以便用于做地址转换。对于该TLB缓存条目,只有当前ASID(ASID寄存器)与条目里的ASID一致的时候(这表明这个条目缓存的是当前应用使用的虚拟地址映射信息),才会进行匹配。因此,在TLBs缓存里面,可以存在多个虚拟地址相同,但是设置了nG位并且ASID不同的缓存条目(虽然它们的虚拟地址相同,但是它们属于不同的应用)。在TLBs缓存里面,对于同一个虚拟地址,应该是不能存在多个非nG转换入口的缓存条目。因为这样的话,这个虚拟地址可以在缓存里面找到多个映射关系,可能导致错误的转换,这一点在ARM的文档中并没有直接提及。下面是一个简单的示意图:
假设任务A,B,C都是链接到虚拟地址0去运行的,因此它们都要将对应的转换表入口设置位nG。这样,在TLBs缓存里面,就可以同时存在3个虚拟地址为0x000的条目,它们的ASID分别设置为A,B,C各自的ASID。对于同一个虚拟地址0,可以通过当前运行的任务ASID来找到对应的缓存条目,直接得到正确的物理地址。
对于多任务的操作系统,各个任务都有自己的转换表,这在管理上存在潜在的困难,因为内存里面会存在多份一级转换表(L1 translation table)的拷贝,每个拷贝的大小都占用16KB((4G/1M)*4/1024)的内存空间。但是,通常来说,这些拷贝的转换表入口绝大部分都是相同的,只有应用相关的部分不同。另一个问题是,如果转换表的某个全局入口需要修改,所有的拷贝都要进行修改操作。
为了减小这些问题带来的影响,ARM架构使用了两个转换表基址寄存器:CP15包含两个转换表基址寄存器TTBR0和TTBR1,这两个寄存器搭配转换表控制寄存器(TTB Control Register)一起使用。通过往TTB寄存器写入N值(0-7),可以告诉MMU它需要检测虚拟地址的高N位来决定使用TTBR0还是TTBR1。默认情况下N被设置位0,此时直接使用TTBR0;当设置为1-7的时候,就检测虚拟地址的高N位。如果这些位全为0,则使用TTBR0,否则使用TTBR1。
这里举个例子:比如将N设置为7,则4G虚拟地址空间中低32M的虚拟地址转换全都使用TTBR0,32M以上的虚拟地址全部使用TTBR1寄存器。这样,映射固定的存储区域(比如分配给操作系统使用的存储区域)可以使用TTBR1寄存器,应用使用的存储区域就使用TTBR0寄存器。这样,各个应用的转换表就可以压缩为32个入口,只占用128字节。
使用这些特性的时候,操作系统需要在做上下文切换的时候改变TTBR0和ASID的值。因为这是两条独立的指令,非原子操作,需要避免非原子访问引发的问题,操作系统编写人员应参考《ARM Architecture Reference Manual》熟悉推荐的指令调用过程。