11. TLB

当内核取消映射或修改某个内存范围的属性时,它有两个选择

  1. 使用两条指令序列刷新整个TLB。这是一个快速操作,但会造成附带损害:来自我们要刷新的区域以外的TLB条目将被销毁,并且必须在稍后重新填充,这会产生一定的成本。

  2. 使用 invlpg 指令一次使单个页面失效。这可能需要更多的指令,但它是一种更精确的操作,不会对其他TLB条目造成附带损害。

采用哪种方法取决于以下几点

  1. 正在执行的刷新的大小。显然,刷新整个地址空间最好通过刷新整个TLB来完成,而不是执行2^48/PAGE_SIZE次单独的刷新。

  2. TLB的内容。如果TLB是空的,那么执行全局刷新不会造成附带损害,并且所有单独的刷新最终都会是浪费的工作。

  3. TLB的大小。TLB越大,我们用完全刷新造成的附带损害就越多。因此,TLB越大,单独刷新看起来就越有吸引力。数据和指令有单独的TLB,不同的页面大小也是如此。

  4. 微架构。TLB已成为现代CPU上的多级缓存,并且相对于单页刷新,全局刷新变得更加昂贵。

显然,内核无法知道所有这些事情,尤其是在给定的刷新期间TLB的内容。刷新的大小也会因工作负载而异。基本上没有“正确”的选择点。

如果您看到 invlpg 指令(或指令_near_它)在高配置中显示出来,则可能执行了过多的单独失效。如果您认为单独的失效被调用得太频繁,您可以降低可调参数

/sys/kernel/debug/x86/tlb_single_page_flush_ceiling

这将导致我们在更多情况下执行全局刷新。将其降低到0将禁用单独刷新的使用。将其设置为1是一个非常保守的设置,在正常情况下绝不需要将其设置为0。

尽管事实上,x86上的单个单独刷新保证刷新完整的2MB [1],但hugetlbfs始终使用完全刷新。THP的处理方式与普通内存完全相同。

您可能会在 flush_tlb_mm_range() 中看到 invlpg 出现在 profiles 中,或者您可以使用 trace_tlb_flush() 跟踪点来确定刷新操作花费的时间。

本质上,您是在平衡花费在执行 invlpg 上的周期与以后花费在重新填充 TLB 上的周期。

您可以使用性能计数器和 “perf stat” 来衡量 TLB 重新填充的开销,如下所示

perf stat -e
  cpu/event=0x8,umask=0x84,name=dtlb_load_misses_walk_duration/,
  cpu/event=0x8,umask=0x82,name=dtlb_load_misses_walk_completed/,
  cpu/event=0x49,umask=0x4,name=dtlb_store_misses_walk_duration/,
  cpu/event=0x49,umask=0x2,name=dtlb_store_misses_walk_completed/,
  cpu/event=0x85,umask=0x4,name=itlb_misses_walk_duration/,
  cpu/event=0x85,umask=0x2,name=itlb_misses_walk_completed/

这适用于 IvyBridge 时代的 CPU (i5-3320M)。不同的 CPU 可能有不同名称的计数器,但它们至少应该以某种形式存在。您可以使用 pmu-tools ‘ocperf list’ (https://github.com/andikleen/pmu-tools) 来查找给定 CPU 的正确计数器。