11. TLB

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

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

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

采用哪种方法取决于以下几个因素

  1. 执行刷新的大小。显然,刷新整个地址空间比执行 2^48/PAGE_SIZE 个单独的刷新操作更适合通过刷新整个 TLB 来完成。

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

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

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

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

如果您在配置文件中看到 invlpg 指令(或_靠近_它的指令)出现频率很高,则可能正在进行过多的单独失效操作。如果您认为单独的失效操作被调用得过于频繁,您可以降低可调参数

/sys/kernel/debug/x86/tlb_single_page_flush_ceiling

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

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

您可能会在配置文件中看到 flush_tlb_mm_range() 中出现 invlpg,或者您可以使用 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 的正确计数器。