前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TLB flush 在 OpenCloudOS 中的执行机制与优化实现

TLB flush 在 OpenCloudOS 中的执行机制与优化实现

原创
作者头像
腾源会
修改2023-11-15 11:23:05
3430
修改2023-11-15 11:23:05
举报
文章被收录于专栏:腾源会腾源会

随着云计算技术的发展,服务器多核多 NUMA 架构得到了广泛的使用,得益于芯片架构设计上的缓存一致性协议,数据的一致性在不同 CPU 上访问得到了保证,为此必须要通过 TLB flush 操作的方式,invalid 其他几个 cpu 上 TLB entry 缓存,但是频繁执行 TLB flush 操作往往伴影响着业务的性能,导致部分核心业务出现性能抖动的情况,为此怎样减少 TLB flush 带来的影响,成为了很多开发者探索的方向。 本文以 TLB flush 基础概念着手,对 OpenCloudOS 中 TLB flush 的原理以及相关接口进行了较为详细的介绍,并结合某个关键业务,描述了 TLB flush 在 OpenCloudOS 中所做的优化。

一、认识 TLB flush

TLB 是一种内存高速缓存,用于存储虚拟内存到物理内存的最新映射关系,它是芯片内存管理单元(MMU) 的一部分,驻留在 CPU 和 CPU 缓存之间、CPU 缓存和主存之间或者不同级别的多级缓存之间,通过查找 TLB 缓存,可以减少访问用户查找物理内存地址所需的时间。

当一个进程访问某段物理地址空间时,会先从 TLB 缓存中查找是否存在保留了其虚拟到物理地址映射的 entry,匹配上的过程又称之为 TLB 命中,倘若内存访问过程中,因为访问内存权限的修改,内存的释放或者迁移,导致 TLB 缓存中内存无效,这个时候,进程再去访问,就会发生异常,这个过程又称之为未命中,即是TLB miss

所以必须要刷新 TLB 缓存以保证内存地址映射的一致性,这个操作过程就叫做 TLB flush,TLB flush 的操作通常由 CPU 硬件单元 MMU 操作来完成。

此外已知在 CPU 中按照 cache 的形式存在,所以此处大略介绍下 cache 的组织形式。

通常 cache 会划分为多个 set(集合), 每个 set 中包含多路(way)cache, cache size 大小按照芯片不同规格也不同,现有服务器设备通常为 64 字节或者 128 字节;

如上图所示:

  • 其中 V 部分代表 cache entry 的是否有效;
  • tag 匹配虚拟地址,在访问内存时,通过该 tag 找到对应的 TLB entry;
  • cache line 表示一路 cache, 也就是代表一个 TLB entry;

有了以上部分知识后,下面我们初步讲解下 CPU 访问内存地址的过程,尤其是虚拟地址和TLB缓存之间怎么关联起来;

这里以 TLB 查找方式中的 VIPT 方式为例进行介绍,从 TLB 的角度,可以了解到到虚拟地址组织形式如下图所示。

先看下上面的张图,其中

  • tag:由虚拟地址高位多个 bit 位组成;
  • index: 由虚拟地址中间多 bit 位组成;
  • offset: 由虚拟地址低 bit 位组成;

但是这三者的值设置多少合适呢?只能说和 cache 缓存的的大小以及虚拟地址的 bit 位数目也有关系。

举个例子,录入 cache 缓存大小为 64K,有 4 路, 服务器寻址为 64bit。

  • offset 的值为 2^ = 64; offset = 6;
  • index 的值为 64k / (64 * 4) = 256 = 2 ^ 8; 所以 index 的值为 8bit;
  • tag 的值为 64 - 8 - 6 = 50bit;

了解以上概念后,此处用一张图去介绍 TLB 转换获取数据的过程:

在 TLB 查找过程中处理逻辑如下:

  • 虚拟地址传入 vipt cache 缓存,通过 index,获取其所在 cache 中 set 以及ways;
  • 再从 cache line 中获取物理地址对应的 tag1;
  • 虚拟地址通过查找 TLB,从 TLB 获取物理地址,然后将该物理地址从中提取出 tag 标识 tag2;
  • 比较 tag1 和 tag2 标识的值是否相等,相等,表示 cache 命中,不相等表示 cache 未命中,需要按照页表转换过程,从内存中获取。

二、TLB flush 在内核的 API 介绍

和 TLB flush 相关的 API 主要有如下,基本按照 flush 影响范围排列, 在最前面的函数,影响面最大,下面会分别介绍其使用以及作用范围;

代码语言:javascript
复制
static inline void flush_tlb_all(void)​

static inline void local_flush_tlb_all(void)  ​

static inline void flush_tlb_mm(struct mm_struct *mm)  ​

static inline void flush_tlb_range(struct vm_area_struct *vma,
                                  unsigned long start, unsigned long end)​
                                  
static inline void flush_tlb_kernel_range(unsigned long start, unsigned long end)  ​
                                  
static inline void flush_tlb_page(struct vm_area_struct *vma, unsigned long uaddr)  ​

static inline void flush_tlb_page_nosync(struct vm_area_struct *vma,

在介绍以上这些函数前,需要额外介绍下面几个汇编指令(此处以 ARM64 为例)

dsb(ishst)

  • dsb 通常表示数据同步屏障, 结合 ishst 标识确保页表更新操作已经已完成;
  • ishst 中的 ish 表示共享域是内部共享(inner shareable),st 表示存储(store),ishst 表示数据同步屏障指令对所有核的存储指令起作用。

tlbi

  • TLB 无效指令,也就是我们常挂在嘴边的 TLB flush;

dsb(ish)

  • 确保 TLB flush 指令操作已完成;

isb

  • 丢弃任何指令预取操作从旧的页表映射(只针对内核页表映射)这条指令冲刷处理器的流水线,重新读取屏障指令后面的所有指令。;

vmalle1is

  • vmalle1is 结合 tlbi 使用,用于表示 TLB flush 的范围,vm 表示本次 invalidate操作对象当前 vmid,all 表示要 flush 所有的 TLB entry,e1 表示执行权限 EL1,is 表示 inner share,可单纯理解为所有 CPU 上的 TLB entry.

ASID

  • 地址空间标识, 用于区分地方的有效范围,通常和 task 绑定在一起,保存在 mm_struct 中,一般低 16bit 对应硬件寄存器支持的 16bit,其余 48bit 表示软件。

flush_tlb_all

代码语言:javascript
复制
static inline void flush_tlb_all(void)
{
    dsb(ishst);  // 确保页表映射已经全部更新    
    __tlbi(vmalle1is); // 无效所有tlb entry    
    dsb(ish);    
    isb(); 
}

这个函数可以理解为内核中 TLB flush 的「总舵主」, 具备至高无上的权利, 能够暗黑所有 TLB entry,使得所有 CPU 上 TLB entry 全部失效, 所以该函数中很少调用到。

local_flush_tlb_all

代码语言:javascript
复制
static inline void local_flush_tlb_all(void)
{
        dsb(nshst);                
        __tlbi(vmalle1);                
        dsb(nsh);                
        isb(); 
}

可以理解为 flush_tlb_all 的轻量版,不针对所有 CPU,只针对当前调用的 CPU,所以 __tlbi(vmalle1) 指令中比 vmalle1is 少了一个「inner-share」。

flush_tlb_mm

代码语言:javascript
复制
static inline void flush_tlb_mm(struct mm_struct *mm)
{
        unsigned long asid;                 
        dsb(ishst);                
        asid = __TLBI_VADDR(0, ASID(mm)); // 获取mm维护的asid值                
        __tlbi(aside1is, asid);                  
        __tlbi_user(aside1is, asid);                
        dsb(ish); 
}

该函数用来刷新某个进程下所维护的所有 TLB entry,通过其维护的 asid 值, 将所有含有该 asid 的 TLB entry 失效,该进程 tlb_entry 可能在多个 CPU 上,所以调用了 aside1is。

flush_tlb_range

代码语言:javascript
复制
static inline void __flush_tlb_range(struct vm_area_struct *vma,
                unsigned long start, unsigned long end,
                unsigned long stride, bool last_level,                                                     
                int tlb_level) 
{
        ......
        start = round_down(start, stride);                
        end = round_up(end, stride);                
        pages = (end - start) >> PAGE_SHIFT;                 
              while (pages > 0) {                               
                  if (!system_supports_tlb_range() ||                                   
                       pages % 2 == 1) {                                         
                             addr = __TLBI_VADDR(start, asid);                                               
                             if (last_level) {                                                     
                                    __tlbi_level(vale1is, addr, tlb_level);                                                             
                                    __tlbi_user_level(vale1is, addr, tlb_level);                                             
                             } else {
                                    __tlbi_level(vae1is, addr, tlb_level);
                                    __tlbi_user_level(vae1is, addr, tlb_level);
                             }                                               
                             start += stride;
                             pages -= stride >> PAGE_SHIFT;
                             continue;                                
                    }                                          
                    
                    num = __TLBI_RANGE_NUM(pages, scale);
                    if (num >= 0) {                                     
                          addr = __TLBI_VADDR_RANGE(start, asid, scale,
                                                    num, tlb_level);
                          if (last_level) {
                                  __tlbi(rvale1is, addr);
                                  __tlbi_user(rvale1is, addr);
                          } else {
                                  __tlbi(rvae1is, addr);
                                  __tlbi_user(rvae1is, addr);
                          }                                                
                          start += __TLBI_RANGE_PAGES(num, scale) << PAGE_SHIFT;
                          pages -= __TLBI_RANGE_PAGES(num, scale);
                     }                                
                     scale++;               
          }               
          dsb(ish);               
          ......
}

按照字面意思,就应该知道该函数作用表示 TLB flush 一段地址空间, 并且可以指定该进程 mm 维护的一段虚拟地址空间[start, end],由于在 ARM64 的机器中,并没有强相关的硬件支持一次性所有地址刷新操作, 所以从上面代码来看,它其实是通过截取一小段一小段范围地址,通过调用 rvae1is 实现的,这也是 ARM64 特有的特性 ARM64_HAS_TLB_RANGE, 才使得大范围 TLB flush 操作得到了改善。

flush_tlb_kernel_range

代码语言:javascript
复制
static inline void flush_tlb_kernel_range(unsigned long start, unsigned long end)
{
        unsigned long addr;         
        
        if ((end - start) > (MAX_TLBI_OPS * PAGE_SIZE)) {                
                 flush_tlb_all();                
                 return;        
        }         
        
        start = __TLBI_VADDR(start, 0);        
        end = __TLBI_VADDR(end, 0);         
        
        dsb(ishst);        
        for (addr = start; addr < end; addr += 1 << (PAGE_SHIFT - 12))            
                  __tlbi(vaale1is, addr);        
        dsb(ish);        
        isb(); 
}

该函数只用于内核态虚拟地址,通常通过 vmalloc 或者 io 映射后,需要 unmap 的时候使用。

flush_tlb_page_nosync

代码语言:javascript
复制
static inline void flush_tlb_page_nosync(struct vm_area_struct *vma,
                                         unsigned long uaddr) 
{
        unsigned long addr;         
        
        dsb(ishst);        
        addr = __TLBI_VADDR(uaddr, ASID(vma->vm_mm));        
        __tlbi(vale1is, addr);        
        __tlbi_user(vale1is, addr); 
}

该函数相比 flush_tlb_range,表示 TLB flush 的是一个地址对应的 TLB entry,而非一段地址区间,该 TLBflush 作用范围更小,并且支持异步操作,不用考虑 TLB flush 是否完成,所以相比其他 TLB flush 函数,少了 dsb(ish);

flush_tlb_page

代码语言:javascript
复制
static inline void flush_tlb_page(struct vm_area_struct *vma,
                                  unsigned long uaddr) 
{
        flush_tlb_page_nosync(vma, uaddr);        
        dsb(ish); 
}

作用同上,但是多了 dsb(ish)指令, 表示为保证缓存一致性必须保证 TLB flush同步操作完成。

三、TLB flush 在内核中的主要优化方法

在现有芯片架构无法改动的情况下,通过从软件的手段去优化 TLB flush 是一种不错的方式,这些优化技巧都避不开以下两种方式。

  • 减少 TLB flush 广播范围;
  • 减少 TLB flush 执行次数;

为了从这两个方面进行优化,内核在代码中对TLB flush操作进行了大量的优化,主要可以归纳为如下几种:

  1. 配备 asid 号,为每一个进程进程分配一个 asid,通过 asid 区分不同进程,以此来减少 TLB flush 时在不同进程的广播范围;
  2. 通过区分用户态地址和内核态地址,执行 TLB flush 操作时候,不需要考虑对内核态进行 TLB flush 操作,通过这种方式,来减少 TLB flush 操作的地址范围;
  3. TLB lazy mode(x86 上支持),该方式主要是考虑进程切换时, 下一个进程处于内核态,由于内核态线程不用担心 TLB 缓存不一致的问题,所以当用户态进程切换到内核态线程时候,不用急着执行 TLB flush 操作,通过这种方式,减少了 TLB flush 操作次数,只需要下一次进程切换到用户态进程时候,默认通过 load CR3 再执行 TLB flush 操作即可;
  4. TLB batch flush,该操作主要是为了针对同一类操作,例如某个进程批量通过调用 try_to_unmap 函数解除地址映射,正常情况下是每一个 page 对应的地址,先解除映射,修改页表,执行 TLB flush 操作,通过 TLB batch flush 优化后,首先对所有地址解除映射,修改页表,最后批量执行 TLB flush 操作,以此来减少 TLB flush 执行次数;
  5. TLB flush range (对于一段连续的虚拟地址),TLB flush range 原理和第 4 点较为相似,但是要求比较严格,必须是一段连续的虚拟地址,所以 TLB flush range 使用于 VMA 区间中某一段连续的地址,例如批量释放处理流程;
  6. TLB flush reduce ipi call, 该方法主要是减少 IPI 中断广播的范围,IPI 中断处理会阻塞当前 CPU 的执行,进程大多数情况下不可能在所有 CPU 上都运行过,所以通过记录进程在哪些 CPU 上运行过,在执行 IPI 广播的时候,通过这些记录,找到需要,需要执行 TLB flush 操作的 CPU, 以此来减少 TLB flush 在 CPU 中的广播范围;

四、TLB flush 在 OpenCloudOS 中的优化实践

下面以多 NUMA 场景下,详细介绍页迁移过程中涉及到的对 TLB flush 执行过程中的优化实现过程。

通过分析 migrate_pages() 函数调用流程,不难发现,调用过程如下:

migrate_pages页迁移处理流程
migrate_pages页迁移处理流程

migrate_pages页迁移处理流程

从上面调用路径图,不难发现页迁移的处理逻辑存在一个明显的热点问题(图中蓝色部分所示),尤其是在迁移大量 page 时候尤为明显,社区工作者针对该热点路径,提出了 TLB batch flush 优化,将原有 TLB flush 操作次数由 page 个数变成固定一次操作,实现机制如下图所示;

migrate_pages 中 TLB batch flush 操作
migrate_pages 中 TLB batch flush 操作

从上面优化可知,TLB batch flush 优化的引入将 migrate_pages() 页迁移过程中,将 TLB flush 执行次数由 page 的个数而定到每批量执行一次,该热点路径执行效率得到了极大的改善,此处,我们通过构造一个简短的 testcase,让其访问 1G 大小的内存,

并将该进程所属内存通过 migratepages 工具迁移到其他 NUMA 节点上,以此来验证 TLB batch flush 优化在页迁移中的效果;

代码语言:javascript
复制
---------------- 无TLB batch flush优化下,页迁移测试情况
# time migratepages 1387 0 1
real 0m0.325s 
user 0m0.000s 
sys 0m0.324s

# time migratepages 1387 1 0
real 0m0.320s 
user 0m0.000s 
sys 0m0.320s

# perf stat -e tlb:tlb_flush  migratepages 1387 0 1
             1,078      tlb:tlb_flush       
             
       0.319386336 seconds time elapsed       
       
       0.000000000 seconds user       
       0.318718000 seconds sys
---------------- 结合TLB batch flush优化后,页迁移测试情况    
# time migratepages a.out 1 0
real 0m0.160s 
user 0m0.001s 
sys 0m0.159s
time migratepages a.out 0 1
real 0m0.163s 
user 0m0.000s 
sys 0m0.163s

# perf stat -e tlb:tlb_flush migratepages 1412 0 1

               268      tlb:tlb_flush       
               
       0.170615481 seconds time elapsed   
           
       0.000000000 seconds user       
       0.162142000 seconds sys

从测试情况来看,1G 大小内存,跨 NUMA 迁移时候,热点 TLB flush IPI 减少了 75%,迁移时间节省了一半。

五、后记

开发者们在内核中对TLB flush的极致优化一直都没有停止过脚步,例如社区最近提交的 Reduce TLB flushes 系列 patch,该方式通过标注每一个 page 的访问属性,只读 page 在任何路径可以不用考虑执行 TLB flush,只有其状态发生改变后,例如读变成写访问抑或解除映射,才需要考虑执行 TLB flush。

通过这种操作,极大的降低了 page 需要执行 TLB flush 的次数,限于篇幅和笔者水平有限,此处就不过多介绍了。总而言之,OpenCloudOS 会持续跟进社区关于 TLB flush 优化的相关 patch,同样也会根据业务需求,进行部分定制,为每一位用户带来更好的使用体验。

参考链接

代码语言:javascript
复制
1、https://www.geeksforgeeks.org/virtually-indexed-physically-tagged-vipt-cache/
2、https://lore.kernel.org/lkml/874jrg7kke.fsf@yhuang6-desk2.ccr.corp.intel.com/T/#m5353ff9cc4eec4889ce1c29fdf51b5cf2ef4a3d5
3、https://docs.kernel.org/core-api/cachetlb.html
4、http://www.wowotech.net/memory_management/tlb-flush.html
5、Reduce TLB flushes 系列 patch:https://lwn.net/Articles/941875/

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、认识 TLB flush
  • 二、TLB flush 在内核的 API 介绍
    • flush_tlb_all
      • local_flush_tlb_all
        • flush_tlb_mm
          • flush_tlb_range
            • flush_tlb_kernel_range
              • flush_tlb_page_nosync
                • flush_tlb_page
                • 三、TLB flush 在内核中的主要优化方法
                • 四、TLB flush 在 OpenCloudOS 中的优化实践
                • 五、后记
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档