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