标题中文释义:Enoki:高速 Linux 内核调度程序开发
作者:Samantha Miller, Anirudh Kumar, Tanay Vakharia, Ang Chen, Danyang Zhuo, Thomas Anderson
会议:EuroSys '24
GitHub:https://github.com/smiller123/enoki
论文链接:https://dl.acm.org/doi/abs/10.1145/3627703.3629569
本文介绍了用于快速开发高性能 Linux 内核调度程序的框架 Enoki。Enoki 使安全、高性能的内核调度程序具有无缝实时升级、双向用户通信通道以及记录和重放调试功能。使用 Enoki 实现的调度程序在各种基准测试中的性能与默认的 Linux 调度程序 CFS 相近。其他 Enoki 调度器模仿了最新的研究调度器,但与 Linux 集成。Enoki调度程序的升级只需10.1𝑢s的服务中断时间,而且记录和重放调试允许对内核调度程序代码进行缓慢但实用的用户空间调试。
1.引入
Enoki,一个用于高速开发 Linux 内核调度程序的框架。Enoki 调度器采用安全的 Rust 语言编写,系统支持将新的调度策略实时升级到内核、用户空间调试以及与应用程序的双向通信。
-
次优的内核决策会导致较高的尾部延迟和较差的整体工作性能
-
异构硬件会增加调度决策的复杂性
-
调度器除了简单的优先级和手工编码的位置安排外,对用户偏好视而不见
-
linux里面只有三个主线调度程序a real time scheduler(RT), an earliest deadline first scheduler(DL), and the Completely Fair Scheduler(CFS);三种的代码都很长,负载平衡机制很复杂;在一些特定的任务下,不同的调度程序会比CFS更好,比如Multilevel scheduling、Nest等,但是Linux对这些的吸收很慢
要解决上述问题,研究人员尝试过以下办法:
-
内核旁路(kernel bypass):
-
优点:无需编译内核,并且提供了用户对空间调试工具的访问,提高了开发速度
-
缺点:会干扰调度程序和系统其他部分的资源共享,使部署复杂化,限制研究的潜在范围
-
-
GhOSt:基于eBPF
-
优点:旨在为Linux中的用户空间调度程序提供通用的可部署解决方案;使用一种上调方案(调度策略的决策由用户空间做出,而机制仍保留在内核中);调度器可在少量用户空间代码中实现,并可轻松重新部署
-
缺点:每次调度决策都需要调度用户空间调度器,这给调度决策增加了大量开销;为了减少一些延迟开销,ghOSt 采用了异步模式,即在用户空间调度程序运行时,内核可以继续接受中断并做出调度决策。这意味着调度决策可能会过时
-
-
eBPF:
-
优点:用户可以加载程序来定制内核调度程序代码,前提是调度程序的结构不发生变化。
-
缺点:使用 eBPF 很难实现大型或复杂的代码,例如整个调度程序。例如,Linux 的默认调度程序 Completely Fair Scheduler 就有 6000 多行代码。此外,eBPF 的信任模型与调度不匹配。eBPF 认为加载的程序可能是恶意程序,并验证它们不会破坏内核执行。目前还不清楚如何在调度中实现这一点,因为错误的调度策略决策,尤其是选择在未排队的 CPU 上运行任务,可能会导致内核崩溃,从而违反 eBPF 的安全要求
-
以上做法各有优缺点,这篇文章提出了一个新的做法:Enoki
Enoki 的目标是在 Linux 内核中高速开发和部署高性能调度程序。作者设想 Enoki 调度器将用于再研究原型和生产部署。Enoki 调度器的编写和调试速度很快,而且易于测试,能与内核的其他部分无缝共享资源。Enoki 支持在 Linux 内核中运行调度程序,以实现广泛部署,但作者的方法并不局限于 Linux 内核。新的Enoki调度程序是用安全的Rust语言编写的,界面简洁,不易引入错误。Enoki 能够在实时内核中动态更新调度代码,无需重启,暂停时间仅为 10 𝜇s。利用记录和重放系统,调度策略可以使用用户空间工具进行调试。由于Enoki调度程序是在内核中实现的,因此可以很容易地与其他内核调度程序进行协调,例如在调度程序或应用程序之间传递内核。
是在内核中实现的,但是更新内核的速度快,高速开发Linux内核调度程序的框架
2.Enoki
两个部分组成Enoki, Enoki-C and libEnoki。
Enoki-C 用 C 语言编写并编译到内核中。它与 Rust 的 libEnoki 交互 与调度程序代码一起编译到动态加载的调度程序模块中的库。黑线代表代码 正常执行期间的路径。红线代表模块插入和升级。
模块化对开发速度有着诸多好处,因为接口清晰,与内核其他部分隔离,开发人员可以把精力集中在正在实现的算法上。在实现过程中,Enoki 使用函数调用和内存共享,从而限制了算法模块化设计所带来的开销。由于 Enoki 调度器在内核中运行,核心调度代码可以快速、轻松地对调度器进行同步调用,使其能够快速响应状态变化。
Enoki 的设计还便于实现其他功能。由于所有功能都包含在模块中,而Enoki-C通过单个函数指针与调度器联系,因此实时升级就像静止状态和替换函数指针一样简单。由于 Rust 支持通用数据类型和特质,自定义提示数据结构可以定义为调度器上的类型参数,对数据类型的任何要求都可以用特质边界来表示。Enoki 的设计非常流畅地支持记录和重放调试。记录非确定性行为是并行系统上记录和重放的主要挑战之一,但使用安全的 Rust 则让这一工作变得简单得多。由于 Rust 的安全保证,我们知道调度器不可能包含竞赛条件或任何其他未定义的行为,因此唯一的非确定性来源是时序和锁定获取的顺序,所有时序状态都由内核处理并传递到调度器,因此通过记录消息可以自动处理。为了正确处理并发性,只需要记录和重放锁的获取顺序。
1.Enoki-C
用 C 语言实现,并编译到 Linux 内核中。它直接与核心调度代码和内核调度数据结构对接。Enoki-C 处理调度程序的注册、注销和升级,并建立和管理用户空间与内核调度程序以及记录和重放系统之间的通信通道基础架构。它代表 Enoki 调度器处理调度所需的不安全工作,例如对内核 task_struct 数据结构(PCB进程描述符)执行状态更新,在添加或移动任务时管理与内核运行队列的交互,以及操作原始指针以向调度模块读取和传递数据。Enoki-Calso 还将核心调度代码的调用转换为调度程序可以安全执行的调用,确保传给调度程序的所有数据都能被安全访问。Enoki 调度器不直接操作内核状态或运行队列。
在 Linux 中,调度程序需要使用内核定时函数来跟踪任务的运行时间。在我们的系统中,Enoki-C 代表调度程序跟踪任务的运行时间,并在任务状态发生变化(如阻塞、唤醒和屈服)时将其传递给调度程序,以及传递给 pick_next_task。
2.libEnoki /Scheduler Module
是一个 Rust 库,与调度程序代码scheduler一起编译成一个模块,动态加载到内核中。该库提供安全接口,以便调度程序代码访问内核,并实现加载和管理调度程序的功能。它包含一些不安全的 Rust,因为它必须处理与 Enoki-C 中 C 代码的交互,而 Enoki-C 本身就是不安全的。每个调度程序完全由安全的 Rust 编写,只需提供调度算法的逻辑。调度模块没有进一步沙箱化;一旦加载到内核,它就会像其他内核代码一样运行。
加载调度程序模块后,libEnoki 会调用 Enoki-C 注册新的可用调度程序。这将在 libEnoki 中注册正在加载的调度程序的 ID 和一个处理函数,用于解析来自 Enoki 的调用。用户任务可使用其定义的 ID 值切换到使用新的调度程序。在正常运行期间,Enoki-C 会处理这些任务的核心调度程序代码调用,将调用转发给 libEnoki 中的处理函数,并管理内核数据结构(如 CPU 的运行队列)的更新。卸载模块时,libEnoki 也会同样取消调度程序与 Enoki-C 的注册,并且不能向调度程序附加新任务。
EnokiScheduler 特性(如表 1 所示)规定了调度程序应提供的功能。其中大部分函数与核心调度代码定义的函数非常相似,以实现 Linux 调度器,但也有一些明显的不同之处。
调度程序的核心函数是pick_next_task,它告诉核心内核调度程序代码下一个运行的任务是什么。其他重要函数包括:task_new、task_wakeup 和跟踪任务状态的类似函数,在内核间移动任务的 migrate_task_rq,告诉内核调度程序移动任务以重新平衡负载的 balance,以及从内核返回错误值的 error 函数。
表1:nokiScheduler Trait 的 API。这是调度程序模块必须实现才能作为 Enoki 加载的 API 调度程序。大多数函数用于管理调度程序中任务的状态。重新注册函数句柄 实时升级。队列函数和parse_hint用于用户到内核的通信,反向队列函数 用于内核到用户的通信。
For example,考虑一个简单的调度程序,它将任务队列分配给每个内核,并在每个内核上调度这些任务,先到先得。step1.在该调度器上创建任务时,内核会调用 select_task_rq。调度器返回任务应分配给的内核;step