Cache
随着CPU频率的提高, CPU对数据的计算速度一直在提高, 而CPU直接从内存中读取数据的时间要比 计算时间慢了50-100倍, 可以想象一下,CPU没有下一个数据,只能等待,等到SDRAM主存的数据送到才能进行下一步的计算。因此 为了提升系统的性能,高效的流水线 还需要加上 高速缓存. 高速缓存 能够提高CPU对内存访问的速度,提高至少50倍.
Cache是一种容量小,速度快的存储阵列。它位于SDRAM和处理器CPU RISC核心之间,保存着最近一段时间处理器涉及到的SDRAM的部分代码内容。因为CPU直接从SDRAM拿数据比较慢,而为了加快CPU数据的传送,因此该cache的主要目的就是,减小SDRAM因为低速和处理器内核造成存储器访问瓶颈的问题。(cache管理可不是简单的单纯的进行SDRAM数据拷贝)
Cache还经常与写缓冲器(write buffer)一起使用。Write buffer是一个很小的先进先出(FIFO)存储器。Write buffer的作用主要就是将高速cache从SDRAM较慢的写操作中解放出来,为cache中写到主存中的数据提供缓冲,有时候CPU也会利用到writebuffer,直接丢一部分数据到该FIFO,由它写入到SDRAM中。
注意到cache和write buffer都是设计在ARM内核中的,因此都有附加的硬件控制器,设定好之后,可以自动的处理处理器CPU和SDRAM之间的数据传送。
因为cache存储器只是提供了主存中非常少的一部分数据,在执行过程中,cache会很快填满,因此cache控制器会不停的交换SDRAM和cache的内容,在特定的代码空间中,交换的数据是随机的,因此不同的程序,执行所需的时间会有细微的差别!(从上面可以理解为ARM处理器中的cache其实功能相当于是程序中的一个子函数,将SDRAM中的某个特定功能的函数代码数据取出,放到该cache缓存中完成,这样的话,在cache控制器下,有部分重复的代码是不需要移出移入的,因此会节省很多时间! 因此,难点就是如何设计出一个有效的cache控制器,来帮助cpu跑的更加的有效率,详细的cache设计在后面介绍!)
正常的流程:
CPU处理核心ßàcacheß--àFIFO(writebuffer)ß--àSDRAM主存
CPU核可利用内部的存储器(TCM)直接访问SDRAM,但是如果是大块代码区的访问一定要通过cache来实现,如果只是字节、字等的读取,CPU也可以向SDRAM直接获取,影响不是很大,因为单条ARM指令的处理长度就是32位长,不会影响到流水线。
一级存储器:如SDRAM等,cpu有直接的片上接口进行连接的存储器
因此,给一级存储器作为高速缓存的叫一级缓存,如上说的i-cache和d-cache等,L1 cache
二级存储器(辅助存储器):也可以用来缓存硬盘、移动存储等,低速的大容量设备
因此,给二级存储器做高速缓存的叫二级缓存,L2 cache,在大型的系统中,通常需要有嵌套的多级高速缓存,L1 cache离CPU最近。如果L1 cache未命中时,不是直接去内存找,而是查找L2 cache!
(从ARM7开始,ARM CPU内部又增加了MMU,内存管理单元,用来支持虚拟内存页表
逻辑cache : ARM7-ARM9-ARM10都是逻辑cache结构, CPU----cache----MMU----SDRAM
物理cache:ARM11采用的是物理cache结构, CPU----MMU---cache----SDRAM
)
我们这里只是使用哈佛结构的ARM9处理器,即将指令和数据分离,因此有i-cache和d-cache两个cache
Cache的结构设计
Cache包含 cache控制器和cache存储器。
Cache存储器是一个专用的存储器阵列,每一块的访问存储单元,我们称之为 “cache行”
Cache控制器是利用处理器再访问存储器时所提供的地址的不同段,来选择cache存储器的不同部分.使用了空间和时间局部性原理,也就是说如果访问的连续地址是同一处或相近,那么该类数据做保留,其他数据先给FIFO存放给SDRAM,这样来控制16k大小的Cache存储器。
92年以前的CPU多数采用直接映射的高速缓存:
当CPU读写存储器请求被送到SDRAM控制器之前,cache控制器会截获,根据读取相关的地址信息来判断是否是连续的程序的代码(将其想象成一段段繁杂的汇编程序),然后cache控制器会利用标签、索引域等信息,对该地址和cache存储器内的标签等信息进行比较判断,再当前活动的cache行中,如果判断出是相近的地址或数据,那么称之为cache命中(hit),不用重新将SDRAM的数据拷贝给cache存储器中,否则称为cache失效(Miss) ,这个情况下,cahce控制器会将SDRAM主存中拷贝整个的cache行到cache存储器中,为处理器核提供相应的代码或数据,也即,cache行填充(cache line fill).但是这样的设计有个缺点就是,一旦碰到while(1){funcA(), funcB()}的情况下,cache就会不停的翻来覆去的替换!
解决的方法就是:采用2个或者是4个cache存储器相联,来确保多次命中,也就是说象上面的这样的情况,可以分别将FuncA放在一个cache,而FuncB放在另外一个cache中.
当高速缓存满了的时候,我们要丢弃部分高速缓存的内容,一般采用“最近最少填充的”这样的算法来选择要替换的cache行,如果发现高速缓存的数据比之内存中的数据要新,那么我们就需要将这些数据写回到内存中,OK,这里就用到了writebuffer(FIFO)!
随着时钟的速度的提高,往往需要将cache进行分级嵌套,L1 cache未命中,那么查找L2 cache….
确定高速缓存的大小和配置
1.在主存中,开辟一些内存,方便用它来填充高速缓存,一个很好的窍门,就是在高速缓存初始化之前,先预留系统内存的低32K用于该目的(这也就是为什么我们要将运行的Image地址放在0x80008000处开始运行,在head.S初始化cache之前就有预留的主存空间)
在有MMU的ARM系列中,大多数的CPU按照4K的页大小来转换地址,这就是说,虚拟地址的低12位不需要转换,只要你的高速缓存是4K或者更小,那么索引MMU的虚拟地址和物理地址是一样的,没有任何问题,否则可能会导致cache重影.
协处理器15(CP15)的一些处理器是专门用来配置和控制带cache的ARM内核的。
其中:
c7 和c9是主寄存器,控制cache的设置和操作
c7: 是只写的,清除或清理cache,flush cache和clean cache
c9: 定义将被替换的丢弃者指针的基地址,该基地址决定了锁定在cache中的代码和数据的行数
清除clean cache的意思是清除cache中存储的全部数据。
清除整个cache mcr P15,0,Rd,c7,c7,0 --其中Rd的值是0
清除数据cache mcr p15,0,rd,c7,c6,0――rd=0
清除指令cachemcr p15,0,rd,c7,c5,0
清理flush cache的意思是回写操作,将被改写过的cache行强制写到主存,并把cache行中的污染位清零。这个操作只用在 回写策略的D-cache上!
Mcr p15,0, rd,c7,c10,z-清理D-cache
Mcr p15,0,rd,c7,c14,z --清除d-cache并清理cache
ARM926ej-s的清理通过test-clean测试清理
通过循环测试清理D-cache mcr p15,0,r15,c7,c10,3
通过循环测试清理并清除D-cache mcr p15,0,r15,c7,c14,3
Mrc p15,0,r12,c2,c0,0 ---读取所有 cp15的寄存器到r12中
<详细的结构说明,请参考《ARM嵌入式系统开发:软件设计与优化》的12.2.1章节>
MMU
MMU,即操作系统对内存存储管理单元,主要完成以下几个功能:
l 隐藏和保护:程序只能到达操作系统允许的内存区。并且,可以单独的指定每个页面为可写或者写保护;操作系统甚至可以停止一个意外覆盖自己代码的程序。
l 分配连续的存储空间: 有了MMU,操作系统可以从物理上分散的页面来构造连续的程序空间,允许我们从一个简单的固定大小页的缓冲池中分配存储。
l 扩展地址范围: 有时候有些CPU不能直接访问它们全部的物理存储器范围,但是又需要很大的地址范围,因此如果你需要更大范围的存储器映射,那么必须要经过MMU来进行。
l 使存储器映射适应你的程序: 有MMU,你的程序可以使用适合自己的地址。在大的操作系统里,可能同一个程序有许多的拷贝同时运行,让这些拷贝都用同样的程序地址要容易的多。
l 按需调页: 在大程序上有用,在按需调页的系统中,没有用到的程序块从不需要读进内存。
l 重定位:程序入口点和预先声明的数据的地址在程序编译/构建时是固定的
Linux存储器管理程序的工作实际上就是 给每个程序(进程)都提供自己的存储空间。
一个进程组中的多个线程可以共享一个地址空间。如果MMU管理不出错,每个进程的命运独立于其它进程(操作系统也保护自己):这样一个进程崩溃,但是不会影响到整个系统。
进程通过malloc()等系统函数,向操作系统申请获取空间。
进程运行的实际空间总是物理空间地址,让操作系统在我们从一个进程环境切换到另一个的时候,急急忙忙的去拼凑虚拟地址页到物理地址映射空间是可能的,但是这样效率太低,因此,我们给每个活动线程的存储器映射一个地址ID/ASID,该ID号通常是一个8位的数据,当需要切换不同的空间时候,只要寻找当前的ASID表即可,这样硬件可以容纳不同的空间而不混淆
关于地址映射:由于不同的程序需要的地址空间大小也不同,如果程序想要多少,那硬件给多少,那么存储器可能很快就会变成一些碎片,因此,所有实际的系统都是以页――固定大小的存储块――作为单位的映射内存。太小的页可能需要很大的转换表,并且程序运行的虚拟地址转换到物理地址需要太多的时间,太大的页,浪费,并且效率不高。最终,我们定义为linux占据绝大多数的结果,那就是4K大小为一页。0x1000=4096
从这里我们看出,物理地址的低12位 11-------0 ,是一页,而将物理地址的高位31—12称之为物理帧号,也称PFN(physical frame number), 而对应的将操作系统的分配的地址的高位31--12,也称虚拟页号,我们称作VPN(virtual page number)。低位即页内的地址不需要转换,我们关心的只是VPN和PFN之间的转换,通常满足程序需要的空间,存储器管理硬件只需要把虚拟页号地址的转换成物理地址的高位即可。
如何高效的进行转换,那么这就是TLB(转换旁路缓冲器)的由来!
想象一下,如果直接一对一的对应物理地址映射建立一个虚拟页表,那么以32位的程序为例:
2的32次,也就是说需要1024*1024*1024*4 这个代表的是32位的字地址,那么8位字节地址就是1024*1024*1024*4*4 也就是说,如果以4k大小为一页,那么该数据表项,至少需要4M的空间,太大了点!
再看一下程序硬件工作情况:
1. 系统内存分配的虚拟地址分为两个部分,最低位4k大小,不作转换,直接传递
2. 高位进行转换,也即VPN,它和上面说的ASID,也就是当前线程活动的地址空间ID标识号拼接一起形成一个唯一的页地址!(分配程序大于4k的存储空间,也很好理解,操作系统分配的虚拟内存,是31----12高位,只需要向左左移2位,那么根据虚拟地址的偏移量部分所占的位数,分配的空间就是4*4k=16k大小,这个也就是简单的理解为重定位寄存器 )
3. 我们在TLB中查看是否有该页的转换项。如果有,那么就给出高位的物理地址位,因此我们就得到了要用的地址。TLB只是一个专用的存储器,可以用各种方法来匹配地址。MMU中可以特定某些标志位来告诉它忽略某些项的ASID,或者屏蔽某些位,允许更大范围的TLB进行虚拟地址的映射。
4. MMU中还有一些额外的标志位 和 PFN(物理帧号)放在一起来控制访问权限,例如:只读不可写。做保护
5. 如果TLB中没有匹配的项,也就是没有地址转换记录时,CPU会产生一个TLB重填异常,那么系统必须(用驻留主存的页表信息)找出或者创建适当的页表项,来加载进TLB,然后再次运行转换过程。(这个就是Image运行前需要空出64k=0x8000大小的驻留内存的原理,在mmu初始化之前)
从上可知,TLB/MMU是一个硬件,MMU主要的功能就是管理好TLB表项的相关操作,操作系统每个进程自己的ID,来结合VPN,从而查找TLB,为该进程分配一个独立的私有存储空间 ,因此简化了各个程序的设计!可以说带有MMU的ARM处理器核 可以有效的支持多任务的程序运行环境.(由此,你可以想象为什么系统需要分配内存,释放内存,系统资源等等的概念,如何在编程的时候提高效率?)