1. 软件管理TLB
前面我们介绍的TLB管理和TLB故障的处理都完全由MMU硬件完成,只有一个页面不在内存时才会陷入操作系统。
而实际上,在现代的一些RISC机中,包括MIPS、Alpha,HP PA,几乎全部的这种页面管理工作都是由软件完成的。在这些机器中,TLB条目是由操作系统显式地装入,在TLB没有命中时,MMU不是到页表中找到并装入需要的页面信息,而是产生一个TLB故障把问题交给操作系统。操作系统必须找到页面,从TLB中淘汰一个条目,装入一个新的条目,然后重新启动产生异常(或故障)的指令。当然,所有这些都必须用很少指令完成,因为TLB不命中的频率远比页面异常大得多。
令人惊奇的是,如果TLB的尺寸取一个合理的较大值(比如64个条目)以减少不命中的频率,那么软件管理的TLB效率可能相当高。这里主要的收益是一个简单得多的MMU(最后介绍),它在CPU芯片上为高速缓存和其它能提高性能的部件让出了相当大的面积。
人们已经使用了很多方法来提高使用软件管理TLB机器的性能,有一个方法既能减少TLB的不命中率又能减少在TLB不命中确实发生时的开销。为了减少TLB的不命中率,操作系统有时可以用它的直觉来指出那些页面可能将被使用并把他们预装入TLB中。例如,当一个客户进程向位于同一台机器的服务器进程发出一个RPC请求时,服务器很可能即将运行。知道了这一点,在客户进程因执行RPC陷入时,系统就可以找到服务器的代码、数据、堆栈的页面,并在TLB中提前为他们建立映射,以避免TLB故障的发生。
无论是硬件还是软件,处理TLB不命中的一般方法是对页表执行索引操作找出所引用的页面。用软件执行这个搜索的一个问题是保存页表的页面面本身可能就不在TLB中,这将在处理过程中再一次引发一个TLB异常,这种异常可以通过保持一个大的(比如4K)TLB条目的软件高速缓存而得到减少,这个高速缓存保持在固定位置,它的页面总是保持在TLB中,操作系统通过首先检查软件高速缓存可以大大减少TLB不命中的次数。
2. 刷新机制
用软件来管理TLB和其他缓存的一个重要的要求就是保持TLB和其他缓存中的内容的同步性,这样必须考虑在一定条件下刷新内容。
在Linux中刷新机制(包括TLB的刷新,缓存的刷新等等)主要要用来完成以下几个工作;
(1) 保证在任何时刻内存管理硬件所看到的进程的内核映射和内核页表一致;
(2) 如果负责内存管理的内核代码对用户进程页面进行了修改,那么用户的进程在被允许继续执行前,要求必须在缓存中看到正确的数据.
例如当正在执行write() 系统调用时,要保证页面缓存中的页面为新页,也就是要使缓存中的页面内容和写入文件的一致,就需要更新缓存中的页面。
3. 通常当地址空间的状态改变时,调用适当的刷新机制来描述状态的改变
在Linux中刷新机制的实现是通过一系列函数(或宏)来完成的,例如常用的两个刷新函数的一般形式为:
flush_cache_foo( );
flush_tlb_foo( );
这两个函数的调用是有一定顺序的,它们的逻辑意义是:
在地址空间改变前必须刷新缓存,防止缓存中存在非法的空映射。函数flush_cache_*()会把缓存中的映射变成无效( 这里的缓存指的是MMU中的缓存,它负责虚地址到物地址的当前映射关系;注意在这里由于各种处理器中MMU的内部结构不同,换存刷新函数也不尽相同。比如在80386处理器中这些函数是为空——i386处理器刷新时不需要任何多余的MMU的信息,内核页表包含了所有的必要信息)。在刷新地址后,由于页表的改变,必须刷新TBL以便硬件可以把新的页表信息装入TBL。
下面介绍一些刷新函数的作用和使用情况:
void flush_cache_all(void);
void flush_tlb_all(void);
这两个例程是用来通知相应机制,内核地址空间的映射已被改变,它意味着所有的进程都被改变了;
void flush_cache_mm(struct mm_struct *mm);
void flush_tlb_mm(struct mm_struct *mm);
它们用来通知系统被mm_struct结构所描述的地址空间正在改变;它们仅发生在用户空间的地址改变时;
flush_cache_range(struct
mm_struct *mm,unsigned long start, unsigned long end);
flush_tlb_range(struct mm_struct
*mm,unsigned long start, unsigned long end);
它们刷新用户空间中的指定范围。
void flush_cache_page(struct vm_area_struct *vma,unsigned
long address);
void flush_tlb_page(struct vm_area_struct *vma,unsigned long
address);
刷新一页面。
void flush_page_to_ram(unsigned long page);(如果使用i386处理器,此函数为空,相应的刷新功能由硬件内部自动完成)
这个函数一般用在写时复制,它会使虚拟缓存中的对应项无效,这是因为如果虚拟缓存不可以自动地回写,于是会造成虚拟缓存中页面和主存中的内容不一致。
例如,虚拟内存0x2000对任务1,任务2,任务3 共享,但对任务2只是可读,它映射物理内存0x1000,那么如果任务2要对虚拟内存0x2000执行写操作时,会产生页面错误。内存管理系统要给它重新分配一个物理页面如0x2600, 此页面的内容是物理内存0x1000的拷贝,这时虚拟索引缓存中就有两项内核别名项0x2000分别对应两个物理地址0x1000和0x2600,在任务2对物理页面0x2600的内容进行了修改后,这样内核别名即虚地址0x2000映射的物理页面内容不一致,任务3 在来访问虚地址0x2000时就会产生不一致错误。为了避免不一致错误,使用flush_page_to_ram使得缓存中的内核别名无效。
一般刷新函数的使用顺序如下:
copy_cow_page(old_page,new_page,address);
flush_page_to_ram(old_page);
flush_page_to_ram(new_page);
flush_cache_page(vam,address);
….
free_page(old_page);
flush_tlb_page(vma,address);
4.函数代码简介
大部分刷新函数都在include/asm/pttable.h中定义,这里就i386中__flush_tlb()的定义给予说明:
#define __flush_tlb()
\
do {
\
unsigned int tmpreg;
\
\
__asm__ __volatile__(
\
"movl %%cr3, %0; #
flush TLB \n" \
"movl %0, %%cr3;
\n"
\
: "=r" (tmpreg)
\
:: "memory");
\
} while (0)
这个函数比较简单,通过对CR3寄存的重新装入,完成对TLB的刷新。