四.虚拟高速缓存
1.虚拟高速缓存的概念
2.别名和歧义的概念
歧义:(ambiguity)
在高速缓存建立映射表之后,如果虚拟地址和物理地址的映射关系发生变化,而从虚拟高速缓存来看,虚拟地址不变,所以依然会命中而直接返回,从而导致读取到的是旧的数据.
别名:(alias)
别名指的是不同的虚拟地址映射到同一个物理地址,在虚拟缓存中会有多份对应关系,在写操作之后,物理地址上的数据进行了更新,但是因为不同虚拟地址对应同一个物理地址,在缓存中命中之后获取到的有可能是旧的数据.
3. 管理虚拟高速缓存
context_switch
虚拟缓存在进程切换的时候,是需要flush cache的,因为进程切换时前后进程中的虚拟地址有可能相同,却有不同的物理映射,不flush cache,则hit之后,获取的是前一个进程的数据。
ARM926EJ-S的文档中有如下说明,说明是否由软件冲刷是处理器相关的
page -- 4.1
The caches arevirtual index, virtual tag, addressed using the Modified Virtual
Address (MVA). Thisenables the avoidance of cache cleaning and/or invalidating
on context switch.
具体到代码中是cpu_do_switch_mm函数,可以根据不同的处理器看,后面分析
fork
采用COW的fork,如果fork之后没发生切换,父进程继续执行,此时通过write-back将数据从cache写回到主存之后,由于COW,会建立一个副本作为父进程的页面,从而发生子进程和父进程数据不一致的情况。不带键值的cache设计中,在fork执行前作flush操作,保证数据一致性.
execve
exec丢弃当前进程的地址空间,但是由于虚拟地址的问题,新老进程可能使用了同样的虚拟地址,映射的是不同的物理地址,导致新程序会接收到一些老程序的数据,所以在获取数据前,要使cache无效,主存储器有效,在使用进程键的cache设计中,因为exec会完全丢弃老进程的地址空间,同时使用的是和原进程相同的进程键(减少查找进程键的时间),所以刷新操作必须执行.
具体代码中比较多应用,比如copy_strings中,flush_kernel_dcache_page,flush_arg_page,具体到汇编的分析前面已举例。
exit
exit会丢弃一个进程的地址空间,内核应该确保丢弃任何高速缓存的数据,从而保证在下一个进程运行时不会出现歧义,因为exit的最后一步是执行现场切换,现场切换会进行冲洗告诉缓存的操作,所以在exit中不需要添加冲洗操作,是否需要flush和体系cache设计有关,在exit_mmap中,最后会调用到flush_cache_mm.
brk /sbrk
brk/sbrk用于增大和缩小进程的bss段,增大bss段不会带来高速缓存的问题,因为是新分配的虚拟存储空间。而缩小bss段,必须防止进程访问相应于刚刚释放掉的虚拟存储区的高速缓存数据,所以必须是缩小bss部分对应的高速缓存无效。
具体到代码中,
对sbrk的操作也由brk实现,如果brk区域变小,则做对应的unmap操作,在unmap操作中有flush操作.
/* Always allow shrinking brk. */
if (brk <= mm->brk) {
if (!do_munmap(mm,newbrk, oldbrk-newbrk))
gotoset_brk;
goto out;
}
共享存储器和映射文件
对于不同进程使用不同的虚拟地址来共享相同的共享存储段,需要考虑带来高速缓存的别名问题,但是由于每次现场切换都已经进行冲洗高速缓存的操作,所以不需手动消除。
但是如果是一个进程内不同的虚拟地址映射同意物理页面,则需要考虑别名问题,一方面内核可以通过只访问一次的方式去处理,也可以高速缓存的组织方式去解决。(比如每次附加的起始地址都索引到相同的高速缓冲行)
输入输出
使用DMA传输需要注意使高速缓存无效,所以在上面说到unmap时会有flush dcache的操作。
用户-内核数据的歧义
在内核模式和用户模式执行期间都会使用高速缓存,所以必须保证用户不能访问任何被高速缓存的内核数据,也必须保证任何被高速缓存的用户数据不会被误认为内核数据。比较简单的处理方式是,用户进程不能访问被高速缓存的内核数据(也不能修改它),但是在行替换期间可以写回到内核数据,这样就不用显式的高速缓存冲洗操作来防止用户-内核的歧义,否则需要显式冲洗数据。
说明:linux内核中已经有swapper守护进程来作页面的换入和换出工作,在完成这些工作之后一定会进行进程切换,如果是原来不带键值的cache设计中,则会在切换时flush,保证主存储器的内容最新,因此也可以安全的执行DMA,再带键值的进程在执行此操作时,则需要在交换页面之后进行flush的操作,查看代码,swap.c中put_page有进行相关操作.
4.带有键值的cache设计
context_switch
ARM926EJ-S的文档中有如下说明
page -- 4.1
The caches arevirtual index, virtual tag, addressed using the Modified Virtual
Address (MVA). Thisenables the avoidance of cache cleaning and/or invalidating
on context switch.
如何通过Modified Virtual Address (MVA)来避免context switch时的cache冲洗呢?事实上,之所以需要在进程切换的时候冲洗cache,原因就在于前后进程在同一个虚拟地址对应的可能是不同的物理页面,而cache是不感知的。
在高速缓存中添加一个进程键唯一地确定一行高速缓存属于一个特殊的进程,这样就能减少必须冲洗操作的次数,理想情况下,给每一个进程分配一个唯一的键,则VA和键就能组成唯一的标识符,组成MVA,从而在进程切换时无须冲洗.
这里进程键和pid不是同一个概念,进程键是和寄存器相关的,只要是每个进程的唯一标识就行,与pid无关.
使用key的方法还是有一点问题的,切换进程之后,高速缓存中只包含老进程数据的虚拟地址,切换期间,MMU已经载入新进程的地址空间映射关系,所以不能转换老进程的地址。用写直通能消除此问题。如果使用将MVA传递给MMU的方式,则MMU需要管理多个地址空间的映射关系,这样就能使用写回高速缓存机制。
ARM926EJ-S的手册中对MMU的addresstranslation描述如下,
The VA generated bythe CPU core is converted to a Modified Virtual Address (MVA)
by the FCSE usingthe value held in CP15 c13. The MMU translates MVAs into
physical addressesto access external memory, and also performs access permission
checking.
fork
在使用键值的情况下,如果是N组单行的设计cache颠簸是必然的,这个问题在前面有过说明.父子进程的键值不一样,在进程切换之后(使用键值不再刷新cache),单行必然会无法命中(因为键值不同,即使虚拟地址一致,并且COW),此时使用N组多行的设计就能避免此问题.
下图是一个双路组相联父子进程共享读取地址0x100000的例子,
exec
使用进程键同样要刷新cache,新老进程使用的是同一个进程键,由于execve丢弃所有的地址空间,所以flush之后不会产生歧义.
exit
在使用不带键值的cache时,因为任务切换会进行flush操作,所以不需要在exit的最后手动冲洗,而使用键值的设计,现场切换没有flush操作,需要相关操作(和体系有关)
具体在exit_mmap中,最后会调用到flush_cache_mm.
brk/sbrk
带有键值的cache设计和不使用的一致,在sbrk时需要flush.
共享存储和映射文件
不带键值的cache设计在现场交换时会进行flush操作,而在带键值的cache上就显得比较麻烦,首先N路组相联必然是一个能稍微解决此问题的办法,和前面说的fork一致,其次使用内核只能访问一次的方式处理效率较低,而最好的方法是通过键值加虚地址的组合仍然能索引到相同的高速缓冲行的设计。但是这也无可避免的体现到,带键值的cache设计的缺点,共享的数据在高速缓存中不能共享.
下图是通过键值加虚地址的组合仍然能索引到相同的高速缓冲行的示例.
从ashmem.c的代码来看,一方面使用N路组相联稍微解决cache flush问题,同时使用mutex_lock实现互斥操作,另外还提供了ioctl的操作,应用可以传递ASHMEM_CACHE_FLUSH_RANGE来实现flush操作.
输入输出
带键值的cache这里和不带键值的cache设计保持一致.
用户-内核数据的歧义
和不带键值的设计基本保持一致.
TLB
TLB本身仅仅是一个虚拟高速缓存而已,通过使用虚拟高速缓存可以找到转换所用的入口地址,它也同样以这个地址来进行索引和标记。TLB所高速缓存的数据是物理页号和页访问权限。TLB内发生一次命中的时候,MMU能够从TLB的数据立刻计算出物理地址和有效的权限。在出现一次缺失的时候,有些实现会读取保存在主存储器内的页面,以获得相关的映射关系,然后将新的映射关系自动高速缓存到TLB中供以后使用.
Tlb和cache/mmu的关系可以见下图(摘自wiki)
和cache类似,如果只使用虚拟地址来标记TLB数据,在context_swith时必须flush TLB,防止发生歧义,而一些处理器的TLB带键值设计的则不需要flush,只要在sbrk的时候或者另一个进程要重用键的时候.在DMA传输时,是不需要作flushTLB的.
看看ARM926EJ-S中,TLB的说明,
The MMU contains asingle unified TLB used for both data accesses and instruction
fetches. The TLB isdivided into two parts:
• an eight-entryfully-associative part used exclusively for holding locked down
TLB entries
• a set-associativepart for all other entries, 2 way x 32 entry.
When an entry hasbeen written into the lockdown part of the TLB, it can only be
removed by beingoverwritten explicitly, or by an MVA-based TLB invalidate
operation, wherethe MVA matches the locked down entry.
从上面的说明来看,这是带键值的TLB,因为在进程切换时,不需要flush TLB
SMP 和 TLB
在SMP体系下TLB的刷新设计到广播操作,详细可以见ipi_flush_tlb_kernel_range函数。
从下图可以很容易明白为啥SMP下需要做同步的tlb flush操作
5.带有物理标记的虚拟高速缓存
另外一种不用key的增强方式是,高速缓存保存的是物理地址,暂不学习.
五.物理高速缓存
前面说到的都是虚拟高速缓存,一般就是我们平时说的L1 cache, L2 cache一般就是这里说的物理高速缓存.
在crash_note.c中,函数crash_notes_save_this_cpu有一个示例应用,
下面是物理高速缓存的组织结构,
这个设计的好处在于不会出现歧义也不会出现别名,共享数据的进程使用相同的物理页面,使得它们索引到相同的高速缓冲行或者组,并且会匹配标记从而发生一次命中,最后一般不需要冲洗高速缓存.
物理高速缓存的在现场切换,fork,exec,exit,brk/sbrk,共享存储,映射文件,用户-内核数据的歧义等方面均不需要冲洗操作.
输入输出和总线监听
高速缓存能看到所有的总线交易,所以它能监听所有IO设备执行的总线活动,从而可以检查交易中的物理地址是否驻留在高速缓存中,如果发生不一致就进行更新.不管是write-back还是write-through策略,都能通过总线监听的方式得到一致更新.
带有次级物理高速缓存的主虚拟高速缓存组织结构如下,
六.高速缓存的管理
一个高速缓存的整体性能是由3个因素决定的:高速缓存的物理设计,在系统上运行的程序的局部引用特性,以及操作系统管理高速缓存的效果。
高速缓存的物理设计在定下来之后就无法改变,而对于应用程序的局部引用特性操作系统无法控制,而操作系统管理高速缓存的方式则比较复杂.
操作系统管理高速缓存主要有三种技术,分别是地址空间布局,延迟高速缓存无效,和对齐高速缓存的数据结构.
地址空间布局:
地址空间的布局主要针对一个进程中的3个主要区域(text,data/bss,stack),一个比较好的布局是能减少cache冲洗次数,最好的办法是能动态地址绑定.
静态地址布局如下,
动态布局如下,
在linux代码中是如何实现的呢?
比如有如下设计,
config COMPAT_BRK
bool "Disable heap randomization"
default y
help
Randomizing heap placement makes heap exploits harder, but it
also breaks ancient binaries (including anything libc5 based).
This option changes the bootup default to heap randomization
disabled, and can be overridden at runtime by setting
/proc/sys/kernel/randomize_va_space to 2.
On non-ancient distros (post-2000 ones) N is usually a safe choice.
android的init.rc中, write/proc/sys/kernel/randomize_va_space 2
物理索引高速缓存(pagecolor)
page color的理论比较复杂,目前基本的了解是在申请page时通过取模做编号,不同的编号对应不同的color的概念,这样在管理page时,能最大限度的利用到cache.(以后分析学习)
延迟高速缓存无效:
延迟高速缓存无效和推后执行有点类似,在不需要flush操作时,先进行其他操作,比如在带有key的虚拟高速缓存中,exit之后可以不用flush操作,因为由于进程键的关系,cache必然无法命中,在后面会重新flushcache.
对齐高速缓存的数据结构:
在设计数据结构时最后能根据cacheline来进行数据结构的对齐,这样便于命中和也能减少flush的次数.
PS: 再下篇分析代码