调度器域¶
每个 CPU 都有一个“基本”调度域(struct sched_domain)。域层次结构是通过 ->parent 指针从这些基本域构建的。 ->parent 必须以 NULL 结尾,并且域结构应该是每个 CPU 的,因为它们是无锁更新的。
每个调度域跨越多个 CPU(存储在 ->span 字段中)。域的 span 必须是其子域 span 的超集(如果需要,可以放宽此限制),并且 CPU i 的基本域必须至少跨越 i。每个 CPU 的顶层域通常跨越系统中的所有 CPU,尽管严格来说不一定非要如此,但这可能导致某些 CPU 永远不会被分配运行任务,除非显式设置了允许的 CPU 掩码。调度域的 span 意味着“在这些 CPU 之间平衡进程负载”。
每个调度域必须有一个或多个 CPU 组(struct sched_group),这些组组织为从 ->groups 指针开始的循环单向链表。这些组的 cpumask 的并集必须与域的 span 相同。 ->groups 指向的组必须包含该域所属的 CPU。组可以在 CPU 之间共享,因为它们包含设置后只读的数据。任何两个组的 cpumask 的交集可以是非空的。如果出现这种情况,则在相应的调度域上设置 SD_OVERLAP 标志,并且其组可能不会在 CPU 之间共享。
调度域内的平衡发生在组之间。也就是说,每个组都被视为一个实体。组的负载定义为每个成员 CPU 的负载之和,只有当组的负载失衡时,任务才会在组之间移动。
在 kernel/sched/core.c 中,sched_balance_trigger() 通过 sched_tick() 定期在每个 CPU 上运行。它在当前运行队列的下一个定期调度的重新平衡事件到达后引发一个软中断。实际的负载平衡主力函数 sched_balance_softirq() -> sched_balance_domains() 随后在软中断上下文(SCHED_SOFTIRQ)中运行。
后一个函数接受两个参数:当前 CPU 的运行队列以及 sched_tick() 发生时 CPU 是否处于空闲状态,并迭代我们的 CPU 所在的所有调度域,从其基本域开始并向上遍历 ->parent 链。在此过程中,它会检查当前域是否已耗尽其重新平衡间隔。如果是,则在该域上运行 sched_balance_rq()。然后,它会检查父调度域(如果存在)、父域的父域,依此类推。
最初,sched_balance_rq() 查找当前调度域中最繁忙的组。如果成功,它会查找该组中所有 CPU 的运行队列中最繁忙的运行队列。如果它设法找到这样的运行队列,它会锁定我们的初始 CPU 的运行队列和新找到的最繁忙的运行队列,并开始将任务从后者移动到我们的运行队列。任务的确切数量等于在迭代此调度域的组时先前计算出的失衡量。
实现调度域¶
“基本”域将“跨越”层次结构的第一级。在 SMT 的情况下,您将跨越物理 CPU 的所有兄弟姐妹,每个组都是一个虚拟 CPU。
在 SMP 中,基本域的父域将跨越节点中的所有物理 CPU。每个组都是一个物理 CPU。然后,对于 NUMA,SMP 域的父域将跨越整个机器,每个组都有一个节点的 cpumask。或者,例如,您可以进行多级 NUMA 或 Opteron,可能只有一个域覆盖其一个 NUMA 级别。
实现者应阅读 include/linux/sched/sd_flags.h 中的注释:SD_*,以了解具体的细节以及如何针对调度域的 SD 标志进行调整。
架构可以通过创建 sched_domain_topology_level 数组并使用此数组作为参数调用 set_sched_topology() 来覆盖给定拓扑级别的通用域构建器和默认 SD 标志。
可以通过启用 CONFIG_SCHED_DEBUG 并在您的 cmdline 中添加 ‘sched_verbose’ 来启用调度域调试基础设施。如果您忘记调整 cmdline,您也可以翻转 /sys/kernel/debug/sched/verbose 开关。这将启用对调度域的错误检查解析,该解析应捕获大多数可能的错误(如上所述)。它还以可视化格式打印出域结构。