本章我们从硬件底层开始,首先研究TLB机制以及如何设置。在此基础上分别研究裸机程序和操作系统下内存管理机制。
1 TLB/MMU硬件
TLB是把程序地址或者虚拟地址转换成物理地址的硬件电路。地址转换是实现安全OS的安全特性的关键。
基于MIPS架构的CPU,转换页表项的大小是4K,我们称之为页(page)。虚拟地址的低12位是在物理内存上的偏移量,换句话说,虚拟地址的低12位等于物理地址的低12位。其实,MIPS架构的CPU完全支持访问更大的页(大于4K),这对于特殊场景下的应用很有用。尤其是现在AI人工智能这么火,许多应用直接把算法和模型数据存放到内存上,需要使用上百G、甚至更大的内存。这时,就需要访问更大的页。但是,本文不讨论更大的页表转换。
转换表中的每一项包含一个VPN(虚拟地址页编号)和一个PFN(物理页帧编号)。当程序给出一个虚拟地址后,和TLB中的每一个VPN进行比较,如果匹配,就给出对应的PFN。具体的比较复杂,由硬件电路实现。所以,通常TLB只有16到64项。
转换表中的每一项除了VPN之外,还包含一些标志位。这些标志位允许OS控制实际的物理地址的属性,比如只读(read-only),或指定数据可以被缓存,也就是存储到Cache中。
现代MIPS架构CPU为了效率,在上面的基础上进行扩展,将每个TLB项包含两个独立的物理页帧,由两个连续的虚拟地址页进行映射。也就是说,每一个TLB项,包含1个VPN和2个PFN,因为虚拟地址是连续的,所以VPN自动加1访问下一个物理内存页。
图6-1是一个兼容MIPS32/64规范的TLB项定义。它们的控制使用协处理器0(CP0)寄存器实现。图中的标签就是寄存器的名称。
在实际的使用过程中,TLB太小了,肯定不能包含所有虚拟地址到物理地址的映射。所以,软件只是把TLB作为最近常用地址转换的一个缓存。当需要的地址转换不在TLB中时,产生异常,异常处理程序计算并加载正确的地址转换关系。这样做,效率肯定包含所有的地址映射效率高,但这是一个综合平衡的结果。
2 TLB/MMU寄存器
上面描述了TLB的工作机制,那么想要控制它实现我们想要的地址转换,就必须有控制它的寄存器和相应的指令。相关寄存器位于协处理器0(CP0)中。下面我们会一一描述这些寄存器:
- EntryHi:在协处理器0中的编号为
10
。 - EntryLo0-1:在协处理器0中的编号为
2/3
。 - PageMask:在协处理器0中的编号为
5
。
1-3项描述的这些寄存器一起构成了一个TLB项所需要的一切。所有对TLB的读写都要经过它们。其中,EntryHi存有VPN和ASID。ASID域具有双重职责,因为它还记录了当前进程的地址空间标识符。64位CPU中,EntryHi扩展到64位,但是对于32位软件仍然保持32位布局不变。兼容MIPS32/64规范的CPU每一项映射2个连续的VPN到2个不同的PFN上,物理内存页的PFN和存取权限标志由EntryLo0和EntryLo1两个寄存器单独指定。PageMask可以用来创建映射大于4K的页。 - Index:在协处理器0中的编号为
0
。
对TLB项的索引。操作指令靠这个寄存器寻址TLB项。 - Random:在协处理器0中的编号为
1
。
产生伪随机数(实际上是一个自由运行的计数器)。表示tlbwr
指令写新TLB项时随机指定的位置。当在异常处理中,重新填充TLB时,随机替换TLB表项使用。可以节省时间。 - Context:在协处理器0中的编号为
4
。 - XContext:在协处理器0中的编号为
20
。
6-7两项描述的寄存器是辅助寄存器,用来加速TLB重填异常处理程序的处理过程。高位可读写,低位取自未能命中的地址中的VPN。通俗的说,就是标记内存映射表在内存中的位置和映射关系的。具体看后面的介绍。
2.1 TLB关键域描述1
下面对关键的域进行描述:
- VPN2(虚拟页编号)
程序地址或虚拟地址的高位(低位0-13略去)。其中,位0-12属于页内偏移,但是位12并不参与查找。每一页映射大小为4K的页,位13自动在两个可能的输出值之间进行选择。refill
异常发生后,将自动设置此域,以匹配无法转译的程序地址或虚拟地址。如果想要不同的TLB项或尝试TLB探测时,必须手动进行设定。
MIPS32/64规范中允许EntryHi的最大虚拟地址区域,达到64位,然而当前的通用CPU只能实现40位。如果你想要知道可用的虚拟地址多大,可以将EntryHi寄存器全写1,然后重新读回,还为1的位就是有效位。
VPN2中超过CPU实际使用的高位地址必须全写0或者1,和R域的最高有效位要匹配。也就是说,核心态使用地址高位必须全为1,否则全为0。
如果使用的是32位指令集,这一切自动发生,不需要我们管理。因为这种工作模式下,所有的寄存器包含的值都是一个32位数的64位有符号扩展。你可以把它理解成就是一个32位寄存器。
从图6-2中,可以看出还有一些位填充为0:这些位并不是没用,有些CPU可以配置支持1KB大小的页,这样VPN2的位需要向下扩展2位。 - ASID(地址空间标识符)
这一部分的作用同ARM架构的ASID作用是一样的。可以保证应用程序的地址空间互相隔离,不受彼此的影响。
异常不会影响该域,所以refill异常之后,该域对当前的进程仍然是有效的。支持多进程的操作系统使用该域表示当前有效的地址空间。但是在使用指令tlbr
指令检查TLB项的时候必须十分小心:该操作会重写整个EntryHi寄存器,所以执行之后,必须恢复正确的当前ASID的值。 - R(64位版本才有)
R值 | 区域名 | 描述 |
0 | xuseg | 用户态可访问的虚拟存储器的低地址区 |
1 | xsseg | 管理态可访问的空间(管理态是可选的) |
2 | xkphys | Core专用的大物理内存窗口 |
3 | xkseg | 核心态空间 |
- PageMask:
寄存器允许设置TLB域来映射更大的页。具体可以允许的页大小如下表所示:
24-21 | 20-17 | 16-13 | 页大小 |
0000 | 0000 | 0000 | 4KB |
0000 | 0000 | 0011 | 16KB |
0000 | 0000 | 1111 | 64KB |
0000 | 0011 | 1111 | 256KB |
0000 | 1111 | 1111 | 1MB |
0011 | 1111 | 1111 | 4MB |
1111 | 1111 | 1111 | 16MB |
- 如果你的CPU支持1KB大小的页,在PageMask底部还要有两个额外的位,对它的设置,遵循同样的模式。
2.2 TLB关键域描述2-EntryLo0-1
图6-3分别展示了64位和32位版本的EntryLo寄存器:
- PFN
保存物理地址的高位,和VPN2中的高位是映射关系。MIPS32架构的CPU外部物理内存的接口限制到2^32字节的范围,但是EntryLo潜在支持多达2^38字节的物理范围(26位的PFN,支持2^26个物理页,每个大小4K)。 - C
包含3位,最初是为多处理器系统的Cache一致性设计的,设置一致性属性,有些手册称之为CCA
。OS往往知道哪些内存页不需要在多个Cache之间实现一致性,比如,只有一个CPU核使用的内存页,再比如只读的内存页,它们可以经过Cache访问,但是不需要考虑一致性。所以,关闭Cache的snooping和交互可以让系统更有效率。
但有时候,嵌入式系统也会使用该域,用来选择Cache的工作方式,比如标记某个具体的页为write-though
式管理,也就是说,访问标记为这种管理方式的页,所有的写操作都同时直接写入主内存和Cache中。
常用的值为:2,不用Cache(uncached);3,可用Cache,但是不要一致性检查(Cacheable noncoherent)。 - D(脏位)
写内存使能标志位。1,写使能允许;0,如果尝试写,产生陷阱。 - V(合法标志位)
如果为0,尝试访问该地址都导致异常。用来设置某个物理地址不可访问。 - G(全局标志)
置1时,TLB项只匹配VPN域内容,不管ASID域的内容是否匹配EntryHi中的值。这样提供了一种机制,可以实现所有进程共享某个地址空间。
需要注意的是,虽然有两个EntryLo寄存器,但是只有一个G位,如果EntryLo0的G位和EntryLo1的不同,那就会坏事。 - 未使用的PFN位和填充为0的位
无论填写0或1,硬件统统忽略。
2.3 选择TLB项的寄存器
- Index寄存器
取值范围0~总项数-1。具体读一项的时候,手动设置Index;如果使用tlbp搜索某个TLB项时,Index会自动增加。Index不需要使用很多位,目前为止,MIPS架构的CPU没有超过128项。bit31有特殊意义,当检测到未匹配项的时候由tlbp置位。使用bit31,看起来像负数,很容易测试。 - Random寄存器
保存到TLB中的索引,CPU每执行一次指令就计数一次(向下递减计数)。写数据指令tlbwr使用这个值作为写入TLB中的位置。可以用来实现随机替换策略。
正常使用时,不需要读写Random寄存器。硬件复位时,将Random设置为最大值-TLB项的最高编号,每个时钟周期递减计数,递减到最小值后又回绕到最大值,周而复始。 - Wired寄存器
有时候,我们可能需要一些永久转换的地址项,基于MIPS架构的OS文档中一般称为wired。设置了Wired寄存器后,凡是索引值小于该值的表项不受随机替换的影响。写入Wired寄存器时,Random寄存器被复位到TLB顶部。
2.4 页表存取辅助寄存器-Context和XContext寄存器
当给出的虚拟地址不在TLB表中时,CPU发生异常,未能转换的虚拟地址已经在BadVAddr寄存器中了。虚拟地址中对应VPN域的位也会被写入到EntryHi(VPN2)中,从而为未命中
的地址建立新的TLB项。
为了进一步加速这种异常的处理过程,Context或XContext寄存器用来记录保存在内存中的页表指针。通过它,可以快速查找定义的虚拟内存映射表。
MIPS32架构的CPU只有Context寄存器,可以帮助填充32位的虚拟地址。MIPS64架构的CPU增加了XContext寄存器,用来扩展虚拟地址空间(达到40位。如图6-4所示:
XContext寄存器是MIPS64架构唯一没有精确定义各个域边界的寄存器:XContext(BadVPN2)域在支持超过40位虚拟地址空间的CPU上自动向上增长,并且将R和PTEBase域向左推移(要想保证放得下,必须自动缩小后者)。各个域的具体参考如下:
- Context(PTEBase):
用于页表管理。保存想要保存的内存页表的基地址。该基地址的低22位为0,也就是以4M为边界。虽然在物理内存或者未映射的内存上提供对齐很低效,但是这样设计的目的是把该表存储到kseg2映射区域内。 - Context(BadVPN2)/XContext(BadVPN2):
跟随在TLB未命中异常之后,这个域被自动填入BadVAddr寄存器中的高位值,也就是VPN域。这儿的数字2,表示连续的虚拟地址页对应独立的两个物理内存页。
BadVPN2的值从第4位开始,是因为PTEBase表中的项都是16字节大小的表项。如果,我们地址是32位,且不需要那么多的软件状态标志位,则页表的项可以使用8字节。这就是Linux没有按照约定使用Context寄存器的原因。 - XContext(PTEBase):
物理内存比较大时用的页表基址寄存器。如果页表非常大,可以存储在巨大内核使用的地址空间内(xkseg区域)。 - XContext(R):
标志TLB未命中发生的地址空间。具体的值可以参考如下:
R值 | 区域名 | 描述 |
0 | xuseg | 用户态可访问的虚拟内存的低地址区 |
1 | xsseg | 管理态可访问的空间(管理态是可选的) |
2 | 对应未映射的地址段,未使用 | |
3 | xkseg | 内核态映射空间(包含老kseg2) |
再次提醒:Linux并不这样使用Context/XContext寄存器。
3 TLB/MMU指令
相关指令:
- tlbr和tlbwi
读写TLB表项,也就是在TLB项和EntryHi和EntryLo0-1寄存器之间搬运数据。使用Index寄存器中的值作为索引,
正常情况下,这两个指令应该不常用。如果你确实修改了某个TLB项,记得恢复ASID域的值,因为该域的值也会被覆盖掉。 - tlbwr
拷贝EntryHi、EntryLo和PageMask寄存器的内容到由Random寄存器随机索引的某个TLB项中。当你选择随机替换策略时,这可以节省你自己产生随机数的时间。实践中,tlbwr经常被TLB重填异常处理程序调用,写一个新的TLB项,而tlbwi指令可以在任何地方使用。 - tlbp
遍历TLB表。搜索TLB表,查看是否有与EntryHi寄存器中的VPN和ASID相匹配的项。如果有,把对应项的索引写入到Index寄存器中;如果没有,则设置Index寄存器的bit31,这个值看上去是一个负值,更好判断。
需要注意的是,tlbp不会从TLB中读取数据,必须在后面执行指令tlbr读取数据。
在大部分的CPU中,TLB地址转换都被纳入流水线的操作流程中,以便提高效率。这时,TLB的这些指令操作不能完全适配标准的管道流水线。所以,在使用了上面这些指令后,立马使用相关虚拟地址的指令可能会产生危险,这个问题我们之前的文章分析过。为了避免这个问题,通常在kseg0非转换区域进行TLB的维护工作。
4 TLB编程
TLB表的设置过程是:将想要的值写入到EntryHi和EntryLo寄存器中,然后使用tlbwr或tlbwi指令拷贝到相应的TLB表中。
处理TLB重填异常的时候,硬件自动将虚拟地址的VPN和ASID域写入到EntryHi寄存器中。
一定注意,不要创建两个相同的虚拟地址映射关系。如果TLB包含重复的项,尝试转换这个地址的时候,会潜在地破坏CPU芯片。一些CPU为了在这种情况保护自身,会关闭TLB硬件单元,并设置相应的SR(TS)标志位。此时,TLB只能复位才能工作。
系统软件一般不会读取TLB表项。但是,如果确实需要读取它们,则使用tlbp遍历匹配到需要的虚拟地址对应的TLB项,把对应的索引值写入到Index寄存器。然后使用tlbr指令读取TLB项相应的值到EntryHi和EntryLo0-1寄存器中。使用过程中,不要忘记保存和恢复EntryHi寄存器,因为ASID域非常重要。
有时候在阅读相关CPU文档的时候,可能会看到带有”ITLB”和”DTLB”字样的指令,它们分别执行指令和数据地址的转换。它们主要执行L1级Cache中地址的转译工作,这些操作完全由硬件进行管理,软件无需干预。当你写入到主TLB表中某一项时,它们会自动失效。
4.1 重填过程
如果程序试图访问任何需要转译的地址(通常是用户态使用的地址空间kuseg和内核态使用的kseg2段),如果TLB表中没有对应的转换映射,CPU就会发出一个TLB重填异常。
我们知道TLB一般很小,而应用程序所需要的地址空间都很大,无法一次在TLB表完全展现。所以,内核OS一般都在内存中维护着一些页表,它们保存着虚拟地址到物理地址的映射关系,我们称这些表为虚拟内存映射表。把TLB作为一个内存映射表的一个缓存。为了提高效率,这些页表中的数据项直接就是按照TLB表项的内容进行排列组合的数据;为了更快访问这些页表,把这些页表的位置和结构保存到Context或XContext寄存器中,作为访问这些页表的指针。
MIPS架构系统一般在kseg0段运行OS代码,这段地址不需要地址转换。所以,TLB未命中一般发生在用户态程序中。为了加速异常处理程序的执行,提供了几个特殊的硬件特性。首先,重填异常处理程序位于内存的低地址区,不会被其它异常使用;其次,使用一些小技巧保证虚拟内存映射表存储于内核虚拟地址空间上(kseg2或64位中对应的内核虚拟地址空间中),这样,这些页表所在的物理内存就不需要映射到用户态虚拟地址空间上了;最后,Context或XContext寄存器可以直接从内存中的虚拟地址映射表中访问正确的映射关系了。
当然了,这些过程都不是强制的。在小型的嵌入式系统上,TLB完全可以映射到固定的物理内存或者进行很少的转换,这时候,TLB就不需要作为一个缓存而存在了。
甚至有些大型OS也不使用上面这种处理方式,比如Linux。因为它与Linux对于虚拟内存的管理策略不同。因为Linux内核的地址映射对所有进程都相同。后面我们再专门分析,基于MIPS架构的Linux内存管理方式。
4.2 使用ASID
ASID设计的目的就是将内存区域进行安全划分,保证不同进程的地址空间安全。使用方法就是设置TLB项中的ASID域为对应的值,并且EntryLo0-1(G)标志位为0,就可以只访问EntryHi(ASID)匹配的项了。ASID占用8位,允许同时映射多达256个不同的地址空间,而不用在进程切换的时候清除TLB。如果ASID用尽,需要把不需要的进程从TLB中清除。抛弃其所有的映射,就可以把ASID重新指定给其它进程了。
4.3 Random和Wired寄存器
一般情况下,TLB使用随机替换原则。所以,为了效率MIPS架构CPU提供了一个Random寄存器来简化实现。
但是,有时候确实需要一些TLB项常驻TLB表中。MIPS架构提供Wired寄存器实现这个需求。写入Wired寄存器一个值,0~wired-1范围的值Random寄存器就不会再产生。它仍然递减,到了wired寄存器的值时,就会返回到最大值。通过这种方式将TLB索引在0到wired-1中的项永久保留在TLB表中。
5 对硬件友好的页表和重填机制
类Unix的OS为MIPS架构提供了一种特殊的地址转换机制。把所有的地址空间划分为一个线性数组,使用VPN索引,与EntryLo寄存器的位域匹配。这样成对的TLB项需要16个字节保存,2*64位。
这种处理方式减少了重填异常处理程序的负荷,但是带来了其它问题。因为每8K的用户空间地址占用一个16字节的表项,整个2GB的用户空间就占用4MB大小的页表,这是一个相当大的内存空间。我们知道,用户空间的地址一般是在底部填充代码和数据,顶部是堆栈(向下增长),这样中间有一个巨大的空隙。MIPS架构借鉴了DEC的VAX体系结构的启发,把页表存入内核态的虚拟地址空间(kseg2或xkseg)。这样的话,
- 节省了物理空间:
中间不用的空闲不需要为其提供物理内存分配。 - 这种使用线性数组映射所有用户虚拟地址的方法,提供了一种在进程切换时,不需要遍历所有虚拟地址空间就可以切换虚拟地址空间的简单机制。进程切换时,改变ASID值,kseg2地址空间内指向页表的指针自动就会重映射到正确的页表上。是不是很巧妙???
MIPS架构通过Context寄存器(64架构使用扩展寄存器XContext)支持这种线性页表。
如果页表是以4M为边界,使用页表的起始地址的高位填充Context寄存器中的PTEBase域,然后,跟随在重填异常之后,Context寄存器就会自动包含重填
需要的页表中的项的地址。
但是,这种方案有一个问题,就是TLB重填异常处理程序本身可能产生TLB重填异常,因为kseg2中存储的映射页表并不在TLB中。但是,硬件对这个问题进行了修复。如果嵌套TLB异常发生,此时,CPU已经处于异常模式了。在MIPS架构的CPU中,异常模式中的TLB重填被定位到通用异常入口点,在那里进行检查并处理。
更多介绍请继续往下看。
5.1 TLB未命中处理程序
TLB未命中
异常发生时,如果状态寄存器SR中的EXL标志位没有被置位,总是会跳转到CPU特定的入口点,开始执行。
下面是一个MIPS32架构的CPU或者MIPS64架构的CPU被当作32位的CPU,处理TLB未命中的处理程序。
.set noreorder .set noat TLBmiss32: mfc0 k1, C0_CONTEXT # (1) lw k0, 0(k1) # (2) lw k1, 8(k1) # (3) mtc0 k0, C0_ENTRYLO0 # (4) mtc0 k1, C0_ENTRYLO1 # (5) ehb # (6) tlbwr # (7) eret # (8) .set at .set reorder
分析:
- (1)行
通常情况下,k0和k1通用寄存器是为底层异常处理程序保留的寄存器。所以,可以直接使用这两个寄存器。 - (2-5)行
把Context执行的页表映射关系写入到EntryLo0和EntryLo1寄存器中,Context的内容发生异常时自动加载。如图6-4所示,MIPS32/64架构的Context寄存器为成对的物理地址映射保留了16字节的空间(每个物理页的映射需要8字节),尽管MIPS32的EntryLo0和EntryLo1只是32位寄存器。这是为了和64位架构兼容而进行的设计。
在这儿,为什么交错执行lw/mtc0指令序列?这是为了效率。我们之前已经多次说过load指令会有一个延时槽,这儿是对延时槽的最大化利用。
如果kseg2区间的地址转换不在页表中,发生嵌套异常怎么办?后面再讲解。 - (6)行
执行遇险屏障(其它架构比如ARM和x86,一般称为内存屏障指令)。如果直接调用tlbwr指令,因为MIPS32架构无法保证此时EntryLo1寄存器的内容已经准备好被使用。所以,加上一条执行遇险屏障,保证数据的安全使用。 - (7)
随机替换,将EntryLo0和EntryLo1寄存器的内容写入到TLB项中。 - (8)
异常返回指令。从异常返回到EPC寄存器中的地址位置并且清除SR(EXL)标志位。
如果在TLB重填异常处理程序中,访问页表的地址时发生miss情况怎么办?(页表的地址位于kseg2空间中,并不在页表中保存)。前面我们提到过,这种情况返回到通用异常处理程序入口点。Cause寄存器和地址异常相关的寄存器(BadAddr,EntryHi,甚至Context和Xcontext)都会被定位到访问页表时的TLB未命中异常相关的信息上。但是EPC寄存器的值仍然指向最初造成TLB未命中的指令处。
这样的话,通用异常程序修复kseg2中的页表未命中问题(也就是将页表的地址合法化),然后,就返回到用户程序。因为我们没有修复任何与第一次地址miss相关的信息,所以,此时用户程序会再次发生地址miss。但是,页表的地址miss问题已经修复,不会再产生二次嵌套地址异常。这时候,TLB异常处理程序就会执行上面的代码,加载页表中的页表映射关系到TLB中。
5.2 XTLB未命中处理
MIPS64架构的CPU有2个特殊的入口点。其中一个,和MIPS32架构CPU共享,用来处理32位地址空间的转换;另一个入口点为64位架构提供,供其寻址更大的地址空间。
状态寄存器中的3个标志位:UX、SX、和KX,它们负责在转换失败时,根据CPU的特权等级选择要使用的异常处理程序。
当相关的状态位(用户模式的SR(UX)标志位)被置位时,TLB未命中异常使用一个不同的向量,那应该是一个加载巨大地址空间转换表的例程。处理程序的代码和32版本的差不多,除了使用64位宽的寄存器和用XContext寄存器代替Context之外。
.set noreorder .set noat TLBmissR4K: dmfc0 k1, C0_XCONTEXT ld k0, 0(k1) ld k1, 8(k1) dmtc0 k0, C0_ENTRYLO0 dmtc0 k1, C0_ENTRYLO1 ehb tlbwr eret .set at .set reorder
需要主要的是,此时的页表结构比较庞大,需要保存在巨大的xkseg地址空间中。
上面的方式不是完全必须的,基于MIPS架构的Linux版本就没有使用这种方式。Linux内核多级页表管理虚拟内存的方式,我们会专门写一篇文章介绍。
6 MIPS架构中TLB的使用场景
如果你要运行的系统是全功能的操作系统,比如说Linux,对TLB的使用不需要你的关注。但是,对于实时OS,你可能想知道TLB是否有用。因为MIPS架构的TLB提供了一种通用目的地址转换服务,你可以根据应用灵活运用它。
TLB机制,允许在page的粒度上,转换任何虚拟地址到物理地址。如果在TLB表中的映射可以容纳所需的所有转换,那么就不需要支持TLB重填异常或单独在内存中保存一个页表。
TLB也允许你定义一些地址是临时的,或者永久不可用的,从而对这些位置的访问导致一个异常来运行操作系统的某些服务例程。通过使用ASID,可以在用户空间实现多任务间的地址空间安全。你还可以对内存进行写保护。
应用可能有许多,下面举几个代表性的例子:
- 访问不方便的物理地址空间:
正常情况下,MIPS架构的硬件寄存器位于物理地址范围0~512MB时比较方便,可以通过kseg1地址空间内的某个指针对其进行访问。但是,有时候硬件无法在这个区域,可以把高物理内存的空间映射到一个方便的地址空间,比如kseg2。与此相关的TLB转换标志必须保证不经过Cache访问这个区域。 - 异常处理程序的内存访问:
默认情况下,保留k0和k1寄存器给异常处理程序,用来进行上下文的保存。但是,如果你不想使用k0和k1呢?这就会带来麻烦,因为MIPS架构的CPU,除了32个通用寄存器之外,没有任何地方可以用来保存。
所以,这种情况下,你可以使用TLB映射一个或多个物理页作为读写内存,使用zero寄存器作为基址寄存器,如果是正的偏移量,就访问kuseg区域的前32KB,如果是负的偏移量,就访问kseg2的后32KB。如果不使用TLB,这就无法实现。 - 在没有虚拟内存的系统中,用来实现可扩展的堆和栈:
即使在没有虚拟内存的系统中,扩展堆栈并监视其使用情况也是很有用的。在这种情况下,需要使用TLB映射堆和栈的地址,使用TLB-miss事件决定是否分配更多内存或者判断应用程序是否失去控制。 - 仿真硬件:
如果某个硬件有时候存在,有时候不存在。通过将寄存器映射到某个区域上,访问这个地址就可以直接访问硬件,如果硬件不存在,调用软件处理程序。
TLB核心的思想就是,通过转换适配,将其变为一个通用的资源,使得硬件开发人员更简单。
7 实时操作系统中的内存管理思想
前面的讨论我们主要针对的是非实时操作系统,比如类Unix-OS操作系统。但是,对于嵌入式OS来说,大部分情况下要简单的多。比如说风河公司的VxWorks等,基本上都是运行在单个地址空间,且提供多线程的能力。彼此之间,没有任务间的保护,所有的功能都被实现在一个大的应用程序中。
对于多种多样的嵌入式系统,是否使用复杂的操作系统(比如说,Linux),目前没有一个统一的标准。如果使用,你可以获得更丰富的编程环境,任务间的保护,更加简洁的接口等。但是同时,也失去了CPU一些执行效率,且需要更大的物理内存;还要牺牲一些实时性。所以,对于机顶盒,DVD播放器和网络路由器等使用Linux比较合适,而像其它一些可靠性、实时性要求比较高的一些场合需要使用实时操作系统,甚至是裸机程序。
当然了,Linux是开源的,这本身就是一种优势。你可以修改源代码,实现自己一些特定的功能。
但是,对于我们开发者来说,可能会面对各种情况。所以,深入硬件实现机制,在此基础之上,灵活运用各种硬件,选择或实现合适的软件是非常重要的。尤其是面对一个新的内存管理系统。需要做的第一件事情就是,搞明白内存映射,包括软件视角的虚拟地址映射和硬件视角的物理地址映射。正是因为选择了相对简单的虚拟地址映射方式,才使得Unix系统内存管理系统相对描述起来简单。但是,嵌入式系统情况非常复杂,有的根本就没有MMU,有的某些地址不需要映射(比如kseg0和kseg1)。这就需要具体问题具体分析了。