11. TLB

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

  1. 使用双指令序列刷新整个 TLB。这是一个快速的操作,但会造成附带损害:非我们试图刷新区域之外的 TLB 条目会被破坏,并且稍后必须以一定的代价重新填充。

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

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

  1. 执行刷新的大小。对于整个地址空间的刷新,显然通过刷新整个 TLB 来执行比进行 2^48/PAGE_SIZE 次单独刷新要好。

  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 的正确计数器。