LWN:为 sched_ext 引入子调度器

关注了就能看到更多这么棒的文章哦~

Jonathan Corbet
 Gemini translation
 原文链接:https://lwn.net/Articles/1056014/ 

可扩展调度类(extensible scheduler class,简称 sched_ext)允许安装由一组 BPF 程序构建的自定义 CPU 调度器。它在 6.12 内核版本中的合并,使内核摆脱了此前一直采用的“一刀切”调度方式;现在,任何系统都可以拥有针对其工作负载进行优化的专用调度器。然而,在任何给定的机器内部,它仍然是“一刀切”的:整个系统只能加载一个调度器。来自 Tejun Heo 的 sched_ext 子调度器(sub-scheduler)补丁系列旨在改变这一现状,允许在单个系统上运行多个 CPU 调度器。 

sched_ext 的构建基于这样一个理念:没有哪个调度器能针对它可能遇到的所有工作负载都进行优化。子调度器的工作扩展了这一理念,认为没有任何调度器——甚至是 sched_ext 调度器——能够为给定系统可能运行的每种工作负载都提供最优性能。正如补丁说明(cover letter)中所说: 

“应用程序通常拥有通用调度器无法掌握的特定领域知识。数据库系统了解查询优先级和锁持有者的关键性。虚拟机监控器可以与客户机调度器协作,并智能地处理 vCPU 放置。游戏引擎知道渲染截止日期以及哪些线程是对延迟敏感的关键线程。” 

运行单一工作负载的系统也可以运行针对该工作负载优化的专用调度器。但系统所有者往往希望系统保持繁忙,这意味着在同一台机器上运行多个工作负载。如果同一系统上的两个工作负载能从不同的调度算法中受益,那么其中至少有一个最终会出现性能不佳的情况。 

解决方案是允许将 sched_ext 调度器附加到控制组(control groups)。系统中的每个任务都将由附加到其所在控制组(或最近的祖先组)的调度器管理。内核长期以来一直支持 CPU 控制器(CPU controller),允许管理员跨控制组分配 CPU 资源。有趣的是,子调度器功能并不依赖于 CPU 控制器;相反,它直接与控制组机制绑定。因此,CPU 控制器仍然负责每个组获得多少 CPU 时间,而子调度器则负责管理这些 CPU 时间如何被其各自组内的进程使用。 

附带调度器的控制组最多可以嵌套四层。在控制组层级结构中,任何要作为另一个调度器父节点的调度器,在编写时都必须考虑到这一职责。只有在父调度器允许的情况下,将子调度器附加到控制组的操作才会成功。父调度器还控制何时调用子调度器的 dispatch() 回调函数。该回调指示调度器选择下一个要运行的任务,并将其添加到特定 CPU 的分发队列(dispatch queue)中。换句话说,父调度器控制给定工作负载(由控制组表示)何时可以运行,子调度器控制构成该工作负载的进程如何访问 CPU,而 CPU 控制器则负责它们运行可用的 CPU 时间总量。 

内核导出了大量的 kfuncs(内核函数),允许 sched_ext 程序对调度器及其下运行的进程进行操作。这些 kfuncs 需要进行通用化处理,以便它们不再是操作“唯一的”调度器,而是操作相应的子调度器。运行多个调度器的系统还必须格外小心,确保这些调度器之间互不干扰。例如,代表给定调度器运行的 BPF 程序不应能够影响——甚至看到——可能存在的任何其他调度器。因此,sched_ext kfuncs 的通用化必须以维护整个系统安全性和健壮性的方式进行。 

为此,许多 kfuncs 已通过增加一个隐式参数(implicit argument)得到了增强,使它们能够访问与运行任务相关联的 bpf_prog_aux 结构;通过该结构,它们可以获取指向应处理的子调度器数据的指针。BPF 程序本身永远不需要指定它们正在操作哪个调度器,也没有能力操作除其所属调度器之外的任何其他调度器。内核能够确保它们始终与正确的子调度器绑定。 

同样,必须防止 sched_ext 程序操作非其实现的调度器管辖下的进程。内核已经在进程任务结构中维护了一个结构(struct sched_ext_entity),其中包含使用 sched_ext 管理每个任务所需的信息。新补丁系列在该结构中添加了一个新字段(名为 sched),指向控制该任务的(子)调度器。任何对进程进行操作的 kfunc 都可以利用这一信息来确保该进程确实处于尝试进行更改的调度器的管辖之下。 

sched_ext 的设计初衷是防止出现故障的调度器造成过大损害。当检测到运行中的调度器出现问题时(例如,一个可运行任务未能在合理时间内分发到 CPU),该调度器将被置于“旁路模式”(bypass mode)。当有意关闭调度器时,也会进入此模式。在旁路模式下,调度器将被停用,其下运行的所有任务都将被置于一个简单的 FIFO(先入先出)调度器管理之下。在当前的内核中,这种旁路调度器是全局性的。 

然而,在拥有多个调度器的系统中,允许一个子调度器将进程抛入全局 FIFO 队列可能会干扰其他子调度器。因此,当使用子调度器时,如果存在父调度器,它将继承进入旁路模式的子调度器中的任务。反之,如果父调度器进入旁路模式,层级结构中其下的任何调度器也将被置于旁路模式。 

当前的补丁集(第 1 版,尽管在 2025 年 9 月也发布过一个 RFC 版本)还不是一个完整的实现。它主要涵盖了分发路径(dispatch path)——即给定调度器将任务发送到 CPU 执行的过程。然而,在分发之前还有几个重要的阶段,本系列补丁尚未涉及: 

  • select_cpu() 回调在任务首次唤醒时被调用。调度器应决定如何处理该任务,包括为其选择运行的 CPU(尽管在此阶段选择并非最终决定)。

  • enqueue() 回调将实际把任务放入分发队列。该队列可能是特定 CPU 的本地分发队列,也可能是调度器维护的其他队列,任务稍后将从这些队列放入 CPU 本地队列。

为了实现完整的子调度器方案,显然需要完善这些回调路径。不过,目前已有的内容足以展示整个设计的运作方式。目前已经有一个 scx_qmap 调度器的修改版本,它既能作为父调度器运行,也能作为子调度器运行;它以一种相对简单的形式展示了调度器本身需要进行的更改类型。 

如前所述,这是一项处于早期阶段的工作,尚未完成。因此,人们不应指望在短时间内就能看到内核支持子调度器。然而,不难想象这一功能对于运行多种工作负载的系统是多么有用,因此会有明确的动力推动其最终完成。届时,“一刀切”的调度模型将被远远抛在身后。 

LWN 评论概述:

一些读者探讨了应用程序捆绑专用调度器的前景,以及子调度器在简化新调度器测试方面的潜力。 

具体而言: 

  • 应用程序捆绑调度器?

     有人猜测未来是否会出现自带调度器的应用程序。这听起来合乎逻辑,因为开发者最了解其软件的行为,提供一个合理的默认调度器并允许用户进一步定制,可以更好地榨取系统性能。

  • 测试新调度器:

     另一个优势在于测试。开发者可以在受限的 cgroup 中运行新调度器,而不必担心在开发早期阶段因调度器故障导致整个系统崩溃。

  全文完
 LWN 文章遵循 CC BY-SA 4.0 许可协议。 

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值