Linux kernel 3.10内核源码分析--TLB相关--TLB概念、flush、TLB lazy模式
一、概念及基本原理
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);
- }
- }
原文地址: http://blog.chinaunix.net/uid-14528823-id-4808877.html
Linux kernel 3.10内核源码分析--TLB相关--TLB概念、flush、TLB lazy模式相关推荐
- Linux kernel 3.10内核源码分析--进程上下文切换
一.疑问 进程调度时,当被选中的next进程不是current进程时,需要进行上下文切换. 进行上下文切换时,有一些问题不太容易理解,比如: 1.进程上下文切换必然发生在内核态吗? 2.上下文切换后原 ...
- Linux Kernel 3.10内核源码分析--块设备层request plug/unplug机制
一.基本原理 Linux块设备层使用了plug/unplug(蓄流/泄流)的机制来提升IO吞吐量.基本原理为:当IO请求提交时,不知直接提交给底层驱动,而是先将其放入一个队列中(相当于水池),待一定时 ...
- Linux kernel 3.10内核源码分析--slab原理及相关代码
1.基本原理 我们知道,Linux保护模式下,采用分页机制,内核中物理内存使用buddy system(伙伴系统)进行管理,管理的内存单元大小为一页,也就是说使用buddy system分配内存最少需 ...
- Linux kernel 3.10内核源码分析--进程退出exit_code
进程退出时,有相应的exit_code,可用于判断进程退出的原因. 比如,waitpid()接口用于等待进程退出,此时被等待退出的进程的返回值比较重要,需要用其来判断进程退出的相应状态,而这就是通过进 ...
- ernel 3.10内核源码分析--KVM相关--虚拟机运行
1.基本原理 KVM虚拟机通过字符设备/dev/kvm的ioctl接口创建和运行,相关原理见之前的文章说明. 虚拟机的运行通过/dev/kvm设备ioctl VCPU接口的KVM_RUN指令实现, 在 ...
- kernel 3.10内核源码分析--中断--中断和异常返回流程
一.问题 1.内核调度与中断/异常/系统调用的关系如何? 2.信号处理与中断/异常/系统调用的关系如何? 3.内核抢占与中断/异常/系统调用的关系如何? 4.内核线程的调度有何特别之处?中断/异常/系 ...
- kernel 3.10内核源码分析--内核栈及堆栈切换
1.概念 Linux中有3种栈: 1)用户栈.当进程处于用户态时使用,位于进程地址空间(用户态部分(如:0-0xc0000000))底部,用户态分配局部变量和函数调用时时,使用该栈,跟平时我们见到和理 ...
- Linux Kernel 2.6.9源码分析 -- send/recieve 报文
Linux Kernel 2.6.9源码分析 – send/recieve 报文 可用户socket报文读写的函数有以下几对: ssize_t read(int fd, void *buf, size ...
- Linux内核源码分析之内存管理
本文站的角度更底层,基本都是从Linux内核出发,会更深入.所以当你都读完,然后再次审视这些功能的实现和设计时,我相信你会有种豁然开朗的感觉. 1.页 内核把物理页作为内存管理的基本单元. 尽管处理器 ...
最新文章
- Android6.0源码分析—— Zygote进程分析(补充)
- java poi excel 单元格样式_java poi批量导出excel 设置单元格样式
- Object 的静态方法之 defineProperties 以及数据劫持效果
- [html] 页面上的登录表单记住了密码(显示星号),但我又忘了密码,如何找回这个密码呢?
- 最让人心动的十大互联网界广告语+超笑评语
- SAP GUI 一些实用技巧分享
- 50步带你在windows PC上创建属于自己的虚拟机(一)
- 听见丨三星Bixby中文(普通话)版正式发布 云端服务商Scalyr获2000万美元A轮融资
- 学习微信小程序的资料汇总---转载自知乎
- 拯救者win10重置系统出现“初始化出现错误,未进行任何更改”问题解决方法
- ASP微信头像保存到服务器,asp微信小程序获取用户头像和微信名-asp写的服务端...
- react-native打包失败: Expiring daemon because jvm heap space is exhausted
- 【Java常见面试题】JVM篇
- QT creator 新建项目
- 机器学习-40-GAN-07-Feature Extraction(InfoGAN,VAE-GAN,BiGAN,Feature Disentangle(Voice Conversion))
- 卢沟桥对于古代北京的作用
- C语言/C++常见习题问答集锦(五十二) 之职工信息管理系统
- ssh备考-05Struts2 Action类下的重要API(原生Servlet的API、跳转配置、框架自身的数据封装、自定义拦截器)
- 海马汽车经销商管理系统技术解析(七)预约失败处理
- 【移动机器人技术】move_base中障碍物无法清除的解决办法
热门文章
- iOS UIKit:UITableView
- WPF开发中遇到的问题及解决系列(一):How can I programmatically click a Button
- 统计日志中ip访问次数并排序的三种方法
- 图像中值处理MATLAB实现
- 使用echo输出一绝对路径,使用egrep取出其基名
- After paper reading.......
- Linux下c和cuda混合编译,并生成动态链接库.so和使用
- 看google三篇论文的感触
- 二十万字C/C++、嵌入式软开面试题全集宝典二
- php laravel 排序,php – 在laravel中排序数组