kernel 3.10内核源码分析--TLB相关--TLB概念、flush、TLB lazy模式 【转】

 

转自:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&id=4808877&uid=14528823

 

一、概念及基本原理
TLB即Translation Lookaside Buffer,是MMU中的一种硬件cache,用于缓存页表,即缓存线性地址(虚拟地址)到物理地址的映射关系。
如果没有TLB,那么正常的内存数据访问前需要先通过线性地址查进程页表将其转换为物理地址,页表实际也是放在物理内存中的,页表分级存放,一次地址转换需要经过多次内存访问,效率不高,尤其是类似的操作非常频繁,由此带来的性能损耗不小。
有了TLB之后,内存数据访问前只需要先从TLB中查找相应的匹配项,找到后即可跳转页表查找的操作,由于TLB是硬件cache,相对于内存访问来说,效率要高许多,所以通过TLB能较大程度改善地址转换效率。
TLB中保存着线性地址(前20位)和物理页框号(pfn)的对映关系,在TLB中查找时,通过匹配线性地址的前20位,如果匹配即可获取pfn,通过pfn与虚拟地址后12位的偏移组合即可得到最终的物理地址。
如果在TLB中没有找到匹配的entry,即出现TLB miss,此时仍需通过查找页表来进行线性地址到物理地址的转换,此时硬件会自动将相应的映射关系缓存到TLB中。
不同的硬件环境中TLB中的entry数量不一,x86中相对较多。

二、TLB刷新
软件(OS)对于TLB的控制只有一种方式:TLB刷新(flush),即使TLB失效。失效后,需要重新通过页表进行地址转换,同时会生成相应的新的TLB entry。TLB刷新会带来一定的性能损失,但当页表被修改时,或发生进程切换时,由于原有TLB中缓存的内容已经失效,此时必须通过软件触发TLB刷新操作。
1、TLB刷新的方式
Intel x86架构CPU硬件提供的TLB刷新的方式有多种,软件(OS)可以根据实际场景选择使用。
1)INVLPG
flush参数指定的线性地址对应的单个TLB entry,如果相应的TLB entry有global标记(表示该entry可能由多个进程共享),也会被flush。同时,还会invalidates所有paging-structure caches(PML4 cache、PDPTE cache和PDE cache)中的所有与当前PCID相关的entry,而不管其是否与参数指定的线性地址对应。
2)mov to CR3(Linux中task switch时使用)
flush当前CPU上除带global标记外的所有TLB entry,同时还会invalidates所有paging-structure caches中的所有与当前PCID相关的entry。具体逻辑还取决于CR4.PCIDE的设置,详见Intel的SDM手册,这里不详述。
3)mov to CR4
当修改CR4.PGE位时,flush当前CPU上所有TLB entry,同时还会invalidates所有paging-structure caches(for all PCIDs)。在Linux中,效果跟mov to CR3效果差不多,主要差别在于其会flush掉带global标记的TLB entry,实际逻辑还跟具体操作相关,详见Intel的SDM手册,这里不详述。
mov to CR0:当将原有的CR0.PG的值从1改为0时(关闭分页支持?),会flush掉当前CPU所有的TLB entry(包括带global标记的TLB entry)和paging-structure caches。
4)INVPCID 
Linux中通常不使用,这里不详述。
5)VMX transitions
虚拟化相关,详见Intel的SDM手册,这里不详述。
另外,根据SDM描述,硬件flush TLB和paging-structure caches是比较free的,不一定会严格遵循上述规则,比如mov to CR3,也是有可能会flush掉global TLB entry的。所以,最好不好做肯定的假设。

2)内核实现
Linux 3.10内核代码中对于TLB刷新,定义了如下接口:


点击(此处)折叠或打开

/*flush mm相关的TLB项,即flush指定进程相关的TLB项*/
static inline void flush_tlb_mm(struct mm_struct *mm)
{
/*要求被flush的mm必须是当前的active mm,因为只有active mm对应的映射才存在于硬件TLB中*/
if (mm == current->active_mm)
__flush_tlb();
}
/*flush指定vma中的虚拟地址对应的TLB项*/
static inline void flush_tlb_page(struct vm_area_struct *vma,
 unsigned long addr)
{
if (vma->vm_mm == current->active_mm)
__flush_tlb_one(addr);
}
/*
  * flush指定虚拟地址范围对应的TLB项,目前未实现,等价于__flush_tlb,最终通过load cr3将所有
  * 的TLB(不包括global项)flush
  */
static inline void flush_tlb_range(struct vm_area_struct *vma,
  unsigned long start, unsigned long end)
{
if (vma->vm_mm == current->active_mm)
__flush_tlb();
}
/*flush 指定进程虚拟地址空间中start-end之间的线性地址对应的TLB项*/
static inline void flush_tlb_mm_range(struct mm_struct *mm,
  unsigned long start, unsigned long end, unsigned long vmflag)
{
if (mm == current->active_mm)
__flush_tlb();
}
/* 通过向其它CPU发送IPI(核间中断)来使其它CPU flush 指定地址范围内的TLB*/
void native_flush_tlb_others(const struct cpumask *cpumask,
struct mm_struct *mm, unsigned long start,
unsigned long end)
{
...
}

最终都是通过如下几个接口实现最终的flush操作。
1)flush除global外的所有TLB entry,通过mov to cr3方法实现。


点击(此处)折叠或打开

/* 等价于__flush_tlb,最终通过load cr3将当前CPU对应的所有的TLB(不包括global项)flush*/
static inline void __native_flush_tlb(void)
{
/*通过重新加载cr3实现,flush除global项之外的所有TLB*/
native_write_cr3(native_read_cr3());
}
/*将val的值写入CR3寄存器*/
static inline void native_write_cr3(unsigned long val)
{
asm volatile("mov %0,%%cr3": : "r" (val), "m" (__force_order));
}

2)flush包括global项的所有TLB entry,通过mov to cr4方法实现。


点击(此处)折叠或打开

/*通过Read-modify-write to CR4来flush TLB,包括global项*/
static inline void __native_flush_tlb_global(void)
{
unsigned long flags;
/*
* Read-modify-write to CR4 - protect it from preemption and
* from interrupts. (Use the raw variant because this code can
* be called from deep inside debugging code.)
*/
/*关中断,防止竞争*/
raw_local_irq_save(flags);
__native_flush_tlb_global_irq_disabled();
/*开中断*/
raw_local_irq_restore(flags);
}
static inline void __native_flush_tlb_global_irq_disabled(void)
{
unsigned long cr4;
/*读取CR4*/
cr4 = native_read_cr4();
/* clear PGE */
/*
 * 清除掉X86_CR4_PGE位后,重新将其写入CR4,清除X86_CR4_PGE位的目的是intel x86架构CPU中,当使用mov to cr4的方式flush TLB时,
 * 只有当CR4.PGE位有修改时,才会触发TLB flush。详见Intel sdm 4.10节。
 */
native_write_cr4(cr4 & ~X86_CR4_PGE);
/* write old PGE again and flush TLBs */
/*
 * 重新将原CR4内容写入CR4,因为上一行代码将CR4.PGE位清除掉了,需要恢复重新写入,才能恢复正常状态,此处其实
 * 再次发生了TLB flush,也就是说Linux中通过mov to cr4的方法实现的TLB flush实际进行了两次flush操作。
 * Fixme:是否能优化下?
 */
native_write_cr4(cr4);
}

3)flush单个TLB entry


点击(此处)折叠或打开

/*flush单个TLB项,由入参addr指定具体的TLB项*/
static inline void __native_flush_tlb_single(unsigned long addr)
{
/*invlpg指令flush指定的TLB项,不考虑该TLB是否有Global标记*/
asm volatile("invlpg (%0)" ::"r" (addr) : "memory");
}

三、TLB lazy模式
1、原理
由于TLB刷新会带来一定的性能损失,所以,需要尽量减少使用。
当内核中进行进程上下文切换时,有如下两种情况,实际上是不需要立刻进行TLB刷新的,可以避免应TLB刷新代理店额性能损失,Linux充分考虑了这些情况,可谓将
相关性能进行了充分发挥:
1)当从普通进程切换到内核线程时。由于Linux中,所有进程共享内核地址空间,内核线程并不使用用户态部分的地址空间,只使用内核部分,所以,当从普通进程切换到内核线程时,内核线程继续沿用prev进程的用户态地址空间,但是并不访问,其只访问内核部分。因此,这种情况下,实际不不需要立刻flush TLB。
2)当新切换的next进程和prev进程使用相同的页表时,比如同一进程中的线程,共享地址空间。此时也不需要进行TLB刷新。
对于上述的第2中情况,由于不会重新加载CR3,不会切换页表,自然也不会触发TLB刷新。
对于上述的第1中情况,如果不进行特殊处理,实际是会在重新加载CR3时触发TLB刷新的,从而导致性能损失。TLB lazy刷新模式即针对这种情况设计。其基本原理为:
当发生内核调度,从普通进程切换到内核线程时,则当前CPU进入TLB lazy模式,当切换到普通进程时退出lazy模式。进入TLB lazy模式后,如果其它CPU通过IPI(核间中断)通知当前CPU进行TLB flush时,在IPI的中断处理函数中,将本CPU对应的active_mm的mask中的相应位清除,因此,当其它CPU再次对该mm进行TLB flush操作时,将不会再向本CPU发送IPI,此后至本CPU退出TLB lazy模式前,本CPU将不再收到来自其它CPU的TLB flush请求,由此实现lazy,提升效率。
值得注意的是,在进入TLB lazy模式后,当第一次收到TLB flush的IPI时,本CPU重新新加载主内核页目录swapper_pg_dir到CR3中,从而将本CPU的TLB刷新一次(不包括Global项)。如此操作的目的注意是因为担心X86架构CPU的超长指令预取,预取的指令可能会访问到需要刷新的TLB entry对应的物理内存,此时如果不flush TLB,可能会出现一致性问题。Linux内核中采用这种相对比较暴力的方式避免了这种情况,虽然看似有点暴力,实则是没有更好的其他做法的无奈之举。
所以,在TLB lazy模式下,如果收到TLB flush请求,实际上还是会刷新一次,看起来好像不怎么lazy,但由于清除了active_mm中相应的cpu mask位,可以避免后续的TLB flush,实际还是有点效果的。

2、代码实现
内核中定义了相应的数据结构用于表示CPU的模式:


点击(此处)折叠或打开

/*表示CPU的TLB模式的结构体*/
struct tlb_state {
/*当前CPU上的active mm,通常通过读取相应的per CPU变量获取*/
struct mm_struct *active_mm;
/*模式,可选状态为TLBSTATE_OK或TLBSTATE_LAZY(lazy模式)*/
int state;
};

可选的模式有:


点击(此处)折叠或打开

/*TLB 非lazy模式,即正常模式*/
#define TLBSTATE_OK    1
/*TLB lazy刷新模式*/
#define TLBSTATE_LAZY    2

同时,定义相应的per-CPU变量,用于存放当前CPU的TLB模式信息


点击(此处)折叠或打开

/*定义per CPU变量,用于存放当前CPU的TLB模式信息*/
DECLARE_PER_CPU_SHARED_ALIGNED(struct tlb_state, cpu_tlbstate);

多核间通过IPI进行TLB flush的相关代码流程如下:
flush_tlb_mm->
    flush_tlb_mm_range->
        flush_tlb_others->
            native_flush_tlb_others->
                smp_call_function_many->
                    smp_call_function_single->
                        generic_exec_single->
                            arch_send_call_function_single_ipi->
                                send_call_func_single_ipi->
                                    native_send_call_func_single_ipi->
最终的IPI发送通过apic的send_IPI_mask接口


点击(此处)折叠或打开

void native_send_call_func_single_ipi(int cpu)
{
apic->send_IPI_mask(cpumask_of(cpu), CALL_FUNCTION_SINGLE_VECTOR);
}

flush_tlb_mm->flush_tlb_mm_range->flush_tlb_others->native_flush_tlb_others():

点击(此处)折叠或打开

/* 通过向其它CPU发送IPI(核间中断)来使其它CPU flush 指定地址范围内的TLB*/
void native_flush_tlb_others(const struct cpumask *cpumask,
struct mm_struct *mm, unsigned long start,
unsigned long end)
{
struct flush_tlb_info info;
info.flush_mm = mm;
info.flush_start = start;
info.flush_end = end;
if (is_uv_system()) {
unsigned int cpu;
cpu = smp_processor_id();
cpumask = uv_flush_tlb_others(cpumask, mm, start, end, cpu);
if (cpumask)
smp_call_function_many(cpumask, flush_tlb_func,
&info, 1);
return;
}
/*
 * 通过smp_call_function_many机制,向其它核发送ipi,使其执行指定的函数(flush_tlb_func),
 * 最后一个入参表示是否wait,此处传入1,表示需要阻塞等待
 * 所有核都执行完成后才继续后面的流程。
 */
smp_call_function_many(cpumask, flush_tlb_func, &info, 1);
}

其它CPU收到IPI后执行的函数为flush_tlb_func
flush_tlb_mm->flush_tlb_mm_range->flush_tlb_others->native_flush_tlb_others->smp_call_function_many->flush_tlb_func():

点击(此处)折叠或打开

/*
 * TLB flush funcation:
 * 1) Flush the tlb entries if the cpu uses the mm that's being flushed.
 * 2) Leave the mm if we are in the lazy tlb mode.
 */
/*刷新当前CPU的TLB,如果当前处于lazy模式,则调用leave_mm*/
static void flush_tlb_func(void *info)
{
struct flush_tlb_info *f = info;
inc_irq_stat(irq_tlb_count);
if (f->flush_mm != this_cpu_read(cpu_tlbstate.active_mm))
return;
/*判断当前CPU是否处于TLB lazy模式*/
if (this_cpu_read(cpu_tlbstate.state) == TLBSTATE_OK) {
/*flush当前CPU的所有TLB项(不包括global项)*/
if (f->flush_end == TLB_FLUSH_ALL)
local_flush_tlb();
else if (!f->flush_end)
/*当没有指定flush_end时,flush flush_start对应的单个TLB entry*/
__flush_tlb_single(f->flush_start);
else {
/*当指定了flush_end时,循环flush 从flush_start到flush_end的地址范围对应的所有TLB entry*/
unsigned long addr;
addr = f->flush_start;
while (addr < f->flush_end) {
__flush_tlb_single(addr);
addr += PAGE_SIZE;
}
}
} else
/*TLB lazy模式,重新加载CR3,并清除掉当前mm中相应的cpu mask位,防止其它CPU再次向本CPU发送TLB flush的IPI*/
leave_mm(smp_processor_id());
}

flush_tlb_mm->flush_tlb_mm_range->flush_tlb_others->native_flush_tlb_others->smp_call_function_many->flush_tlb_func->leave_mm():

点击(此处)折叠或打开

/*
 * We cannot call mmdrop() because we are in interrupt context,
 * instead update mm->cpu_vm_mask.
 */
/*重新加载CR3 刷新当前CPU TLB(不包括global项),并清除掉当前mm中相应的cpu mask位,防止其它CPU再次向本CPU发送TLB flush的IPI*/
void leave_mm(int cpu)
{
/*获取当前的active_mm*/
struct mm_struct *active_mm = this_cpu_read(cpu_tlbstate.active_mm);
/*Fixme:一定是lazy模式使用?*/
if (this_cpu_read(cpu_tlbstate.state) == TLBSTATE_OK)
BUG();
if (cpumask_test_cpu(cpu, mm_cpumask(active_mm))) {
/*
 *去除当前CPU对该当前active_mm的引用,这样的话,当其它CPU再次通过IPI请求flush TLB时,本CPU就不会收到相应的IPI,也就不会再刷新TLB了,因为这里
 *已经刷过了,这也是lazy TLB模式的核心所在
 */
cpumask_clear_cpu(cpu, mm_cpumask(active_mm));
/*并通过重新加载cr3(主内核页目录)刷新TLB(不包括Global的TLB项)*/
load_cr3(swapper_pg_dir);
}
}

 

posted @ 2016-01-15 17:00  Sky&Zhang  阅读(1932)  评论(0编辑  收藏  举报