系统解读CPU 隔离:Full Dynticks 深探

2022-04-19 11:12:36 浏览数 (1)

作者 | Frederic Weisbecker

策划 | 闫园园

SUSE Labs 团队探索了 Kernel CPU 隔离及其核心组件之一:Full Dynticks(或 Nohz Full),并撰写了本系列文章:

  1. CPU 隔离 – 简介
  2. CPU 隔离 – Full Dynticks 深探
  3. CPU 隔离 – Nohz_full
  4. CPU 隔离 – 管理和权衡
  5. CPU 隔离 – 实践

本文是第二篇。

我们之前介绍了 linux kernel 定时器中断,以及它在 kernel 内部状态和服务维护中扮演的角色。抖动敏感型工作负载的用户可能希望消除这种高速率事件导致的 CPU 周期盗用和 CPU 缓存清除。

然而,停止定时器中断并非易事,因为许多 kernel 组件依赖周期性事件,主要是定时器、定时和调度程序。但有一个例外:当 CPU 空闲时,不需要这种 100~1000 Hz 频率的中断。事实上,当 CPU 无需工作时,就没有任务调度器需要维护,没有定时器排队,也没有定时用户。

因此,“节能”自然成为打破周期性定时器的第一个诱因,因为它是专门针对 CPU 空闲状态的优化。CONFIG_NO_HZ_IDLE (https://lwn.net/Articles/223185/) 内核选项带来了一种停止周期性中断的机制,并在 CPU 空闲状态时实现。这一重大进展为满足抖动敏感型工作负载的需求铺平了道路,并提供了一个动态中断的基础架构。接下来就是扩展这个功能,以便在 CPU 忙碌的时候,也可以停止时钟中断。然而,目前的技术水平并不能达到人们预期的目标,下一节中介绍的每一个问题都花了几年时间来解决。

时钟中断服务的替代方案

如前文所述,定时的一次性事件(计时器回调)或周期性事件(调度程序、计时、RCU 等)的几个子系统需要时钟中断 。因此,如果我们想在 CPU 运行实际任务时停止时钟中断,则不能忽略那些请求事件。我们必须使用替代方案为它们提供服务,或者在最坏的情况下限制我们的服务。也就是说,对于这些子系统对周期性时钟中断的依赖性,我们必须从以下各种方式中选择哪些是可能且相关的:

绑定到另一个 CPU

有些工作碰巧在当前 CPU Tick 时执行,但它也可以在另一个 CPU 上执行,而不会出现任何问题。未绑定的计时器就是这样的情况,即未固定到任何 CPU 的计时器。这也间接适用于未绑定的延迟工作队列 (https://www.kernel.org/doc/html/v4.10/core-api/workqueue.html),因为它们依赖未绑定的计时器。这些计时器很容易绑定到其他地方,但这是以运行这些未绑定工作的 CPU 投入一些额外开销为代价的。

负载到另一个 CPU

Some tick work related to the current CPU is not initially designed to be executed on another CPU but we can manage to do it, usually at some cost. This is the case for RCU callbacks processing and regular scheduler tick. 有些与当前 CPU 相关的时钟中断,其最初设计并非是在另一个 CPU 上执行的,但我们可以设法做到这一点,这通常需要付出一定的成本。RCU 回调处理和常规调度程序就是这种情况。

RCU 回调处理

RCU (https://lwn.net/Articles/262464/) 是一种无锁同步机制,一旦保证所有 CPU 都能看到指定的更新,写入程序就可以执行回调。这些回调通常在其排队的 CPU 上执行,即可以来自 softirq 上下文,也可以来自名为“rcuc”的固定内核线程。跟踪和执行这些回调需要时钟中断以轮询它们的队列和内部状态。

为了解决这个问题,一项名为 RCU NOCB 的功能,通过配置内核参数 CONFIG_RCU_NOCB_CPU=y 得以实现。它允许将整个工作从依赖始终中断转移到一组名为“rcuog, rcuop or rcuos”的未绑定的 CPU 的内核线程。

这当然会给运行 rcuo[gps] kthreads 和锁定竞争的 CPU 带来特定的开销。

调度程序时的时钟中断

调度器需要持续收集关于本地和全局任务负载的多项统计信息,从而使其内部状态保持最新。在相当长的时间内,忙碌的 CPU 在进入完全 nohz 模式之前可能有残余的 1 Hz Tick。最终,这些残余的 1 Hz Tick 会转移到未绑定的工作队列中。

这也会给运行这些工作队列的 CPU 带来更多开销。

用上下文更改事件替换轮询事件

计时器中断从中断的上下文和频率推导信息。这是“CPU 记账”和“RCU 静态状态报告”两个重要组件的基础。为了在没有中断的情况下处理这些特性,我们需要从上下文变化和时间戳(通常需要一定代价)中推导出这些信息。这读起来可能很抽象,因此,最好在实践中多了解一下。

Cputime 记账

当在 procfs 文件系统中检查指定进程的 stat 文件时 (/proc/$PID/stat : https://man7.org/linux/man-pages/man5/procfs.5.html),可以检索多个上下文的 cputime 统计信息,例如线程在用户空间、内核空间、客户机等中花费的时间。

这些数字由调度程序 cputime 记账功能来维护。Tick 会触发并检查它中断了哪个上下文。如果中断了用户上下文,则一个 jiffy(两次 Tick 之间的时间)将计入用户时间。如果中断了内核上下文,则 jiffy 将被计入内核时间。这种行为如下图所示:

图 3:Dynticks- 空闲 Cputime 记账

在上例中,我们记录了 2 次用户 Tick 和 6 次内核 Tick。对于 1000 Hz 的 Tick,一个 jiffy 等于 1 毫秒。因此,用户时间记录为 2ms,内核时间记录为 6ms。最终结果总是与在每种环境中的实际时间相近,但通常已经足够好了。Tick 频率越高,cputime 越精确。

现在检查一下闲置记账。这种方式不同,因为空闲时间内没有 Tick,因此,我们所能做的就是计算退出空闲状态和进入空闲状态的时间戳之间的差。

为了能够在运行非空闲任务并且 Tick 停止时对用户和内核 CPU 使用时间进行记账,我们必须将空闲记账逻辑扩展到用户 / 内核记账中。如下所示:

图 4:Full dynticks Cputime 记账

在这里,内核时间可以通过用户进入空闲状态的时间戳减去提出空闲状态的时间戳来检索。同时,在 idle_enter 之前和 user_exit 之后发生的任何事情都要加在其中。用户时间是用户进入和用户退出空闲状态之间的差,它的计算非常简单,甚至比基于 Tick 的记账更加精确。

但这带来了一个问题:为什么不在 Tick 运行时一直使用这种解决方案呢?

因为每次在我们跨越用户 / 内核边界时,需要读取精确但可能提取很慢的硬件时钟。通用工作负载经常遇到这种情况,从而产生性能损失。因此,这种无 Tick 的记账必须保留给将其条目在内核的工作负载。

RCU 静止状态报告

当 RCU 写者程序发布更新并将回调排队等待执行时,它必须等待所有 CPU 报告新的“RCU 静止状态”。这意味着 CPU 已经通过了不属于受保护 RCU 读者程序的代码,称为“RCU 读取端临界区”。

实际上,rcu_read_lock() 和 rcu_read_unlock() 之间的任何代码(或任何不可抢占的代码)都是“RCU 读取端临界区”;其他剩下的都是“RCU 静止状态”。

要追踪静止状态,RCU 依赖 Tick 并检查它中断了哪个上下文。如果中断了不在 rcu_read_lock()/rcu_read_unlock() 对保护部分内的代码,它报告静止状态。如果中断了用户空间,它也被认为是静止状态,因为用户空间不能使用内核 RCU 子系统。

图 5:Dynticks- 空闲 RCU 静止状态报告

上图还说明了 tick-deprived idle 任务如何从特殊处理方式中再次受益。空闲 CPU 不是主动报告静止状态,而是通过进入“RCU 扩展静止状态”被动报告。它在进入和退出空闲状态时递增一个具有完整内存屏障的原子变量。

然后,等待所有 CPU 报告静态状态的 RCU 最终会扫描未响应的 CPU,以找出扩展的静态状态,并代表这些 CPU 报告静态状态。

这种模式之所以有效,是因为我们知道空闲上下文不使用 RCU。我们知道用户空间具有相同的属性,因此,当运行非空闲任务的时候停止 Tick 时,这种被动报告方案可以扩展到用户空间中:

图 6:Full-dynticks RCU 静止状态报告

由于 CPU 很少在内核中花费太多时间,因此,上述提议将取代基于 Tick 的静止状态报告。RCU 扩展的静止状态要么在其间出现短暂的延迟,要么就持续很长时间。

与 cputime 记账类似,这同样有一个问题:为什么即使在 Tick 运行时也不采用这种模式?

因为这将在每个用户 / 内核往返过程中产生一个代价高昂的原子操作,并且会有一个完整的内存屏障。此外,报告静态状态的责任最终由其他 CPU 承担。

如果没有其他选择,则继续使用 Tick

如果没有周期性事件或者频繁事件,有些情况根本无法解决。例如,调度程序任务抢占就是如此。为了保证本地公平性,调度程序必须能够在多个任务之间共享 CPU,并定期检查是否需要抢占。因此,在 CPU 上运行单个任务是在空闲上下文中进一步停止 Tick 的要求。其他子系统也可能会请求定期 Tick,从而在某些情况下保持运行:posix cpu 计时器、perf 事件等。我们将进一步探讨这些细节。

您可以看到,在运行实际任务时,完全停止 Tick 是可能的,但会出现很多陷阱,用户必须准备做好一些权衡。我们将在下一篇文章中详细解释。

0 人点赞