随着云计算技术的发展,服务器多核多 NUMA 架构得到了广泛的使用,得益于芯片架构设计上的缓存一致性协议,数据的一致性在不同 CPU 上访问得到了保证,为此必须要通过 TLB flush 操作的方式,invalid 其他几个 cpu 上 TLB entry 缓存,但是频繁执行 TLB flush 操作往往伴影响着业务的性能,导致部分核心业务出现性能抖动的情况,为此怎样减少 TLB flush 带来的影响,成为了很多开发者探索的方向。 本文以 TLB flush 基础概念着手,对 OpenCloudOS 中 TLB flush 的原理以及相关接口进行了较为详细的介绍,并结合某个关键业务,描述了 TLB flush 在 OpenCloudOS 中所做的优化。
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 字节;
如上图所示:
有了以上部分知识后,下面我们初步讲解下 CPU 访问内存地址的过程,尤其是虚拟地址和TLB缓存之间怎么关联起来;
这里以 TLB 查找方式中的 VIPT 方式为例进行介绍,从 TLB 的角度,可以了解到到虚拟地址组织形式如下图所示。
先看下上面的张图,其中
但是这三者的值设置多少合适呢?只能说和 cache 缓存的的大小以及虚拟地址的 bit 位数目也有关系。
举个例子,录入 cache 缓存大小为 64K,有 4 路, 服务器寻址为 64bit。
了解以上概念后,此处用一张图去介绍 TLB 转换获取数据的过程:
在 TLB 查找过程中处理逻辑如下:
和 TLB flush 相关的 API 主要有如下,基本按照 flush 影响范围排列, 在最前面的函数,影响面最大,下面会分别介绍其使用以及作用范围;
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)
tlbi
dsb(ish)
isb
vmalle1is
ASID
static inline void flush_tlb_all(void)
{
dsb(ishst); // 确保页表映射已经全部更新
__tlbi(vmalle1is); // 无效所有tlb entry
dsb(ish);
isb();
}
这个函数可以理解为内核中 TLB flush 的「总舵主」, 具备至高无上的权利, 能够暗黑所有 TLB entry,使得所有 CPU 上 TLB entry 全部失效, 所以该函数中很少调用到。
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」。
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。
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 操作得到了改善。
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 的时候使用。
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);
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操作进行了大量的优化,主要可以归纳为如下几种:
下面以多 NUMA 场景下,详细介绍页迁移过程中涉及到的对 TLB flush 执行过程中的优化实现过程。
通过分析 migrate_pages() 函数调用流程,不难发现,调用过程如下:
migrate_pages页迁移处理流程
从上面调用路径图,不难发现页迁移的处理逻辑存在一个明显的热点问题(图中蓝色部分所示),尤其是在迁移大量 page 时候尤为明显,社区工作者针对该热点路径,提出了 TLB batch flush 优化,将原有 TLB flush 操作次数由 page 个数变成固定一次操作,实现机制如下图所示;
从上面优化可知,TLB batch flush 优化的引入将 migrate_pages() 页迁移过程中,将 TLB flush 执行次数由 page 的个数而定到每批量执行一次,该热点路径执行效率得到了极大的改善,此处,我们通过构造一个简短的 testcase,让其访问 1G 大小的内存,
并将该进程所属内存通过 migratepages 工具迁移到其他 NUMA 节点上,以此来验证 TLB batch flush 优化在页迁移中的效果;
---------------- 无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,同样也会根据业务需求,进行部分定制,为每一位用户带来更好的使用体验。
参考链接
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 删除。