Linux Housekeeping 机制全解析:CPU 隔离、RCU 同步与 cpumask 管理
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
Housekeeping 是 Linux 内核 CPU 隔离(CPU Isolation)体系的"反向"概念:它描述那些未被隔离、专门承接内核例行工作的 CPU。本文以内核文档 Documentation/core-api/housekeeping.rst 为主线,结合 kernel/sched/isolation.c、include/linux/sched/isolation.h、kernel/cgroup/cpuset.c 与 kernel/workqueue.c 等源码,系统讲解四种 housekeeping 类型的语义、cpumask 查询接口、与 cpuset 的 RCU 同步方案,以及isolcpus=、nohz_full=等启动参数的解析细节。读完本文,你将掌握 housekeeping cpumask 的获取与更新时机、读写两侧的同步约束,以及如何在 DPDK 等对抖动敏感的负载中正确配置 CPU 隔离。
什么是 Housekeeping:隔离 CPU 的"反向"
CPU 隔离(CPU Isolation)把内核工作从某些 CPU 上移走,以降低操作系统抖动(OS jitter)。这类抖动是 DPDK 等极端负载无法容忍的。被移走的内核工作通常被称为"housekeeping"(内务处理),因为它包含大量例行事务:清理(cleanups)、统计维护(statistics maintenance)及依赖统计的动作、内存释放、各种延迟处理(deferrals)等。
从 Documentation/core-api/housekeeping.rst 的定义看:
- 有些 housekeeping 只是非绑定工作(unbound workqueues、unbound timers),可以轻松地被指派到非隔离 CPU 上执行;
- 有些 housekeeping绑定在特定 CPU上,需要精巧的技巧才能卸载到非隔离 CPU(例如 RCU_NOCB、远程调度 tick 等)。
因此,housekeeping CPU 就是隔离 CPU 的反面:它是可以执行 housekeeping 工作的 CPU。系统在任何时刻都必须至少有一个在线的 housekeeping CPU;未被隔离的 CPU 自动成为 housekeeping CPU。
四种 Housekeeping 类型:enum hk_type
Housekeeping 当前划分为四种特性,由 include/linux/sched/isolation.h 中的enum hk_type描述:
| 类型 | 对应隔离手段 | 移走的内核工作 |
|---|---|---|
HK_TYPE_DOMAIN | isolcpus=domain启动参数或 cgroup v2 的隔离 cpuset 分区 | 调度器负载均衡、非绑定 workqueue 与定时器 |
HK_TYPE_KERNEL_NOISE | nohz_full=或isolcpus=nohz | 远程调度 tick、vmstat、lockup watchdog |
HK_TYPE_MANAGED_IRQ | isolcpus=managed_irq | managed IRQ 的中断处理 |
HK_TYPE_DOMAIN_BOOT | 仅isolcpus=domain | 同HK_TYPE_DOMAIN,但忽略 cpuset 的隔离 |
源码中HK_TYPE_DOMAIN与HK_TYPE_DOMAIN_BOOT的区别被明确注释(include/linux/sched/isolation.h):HK_TYPE_DOMAIN_BOOT是启动时isolcpus=参数的反集,而HK_TYPE_DOMAIN在它的基础上还包含 cpuset 隔离分区的反集,因此始终是HK_TYPE_DOMAIN_BOOT的子集。
此外,头文件还定义了若干别名(include/linux/sched/isolation.h):
HK_TYPE_KTHREAD = HK_TYPE_DOMAIN(kthread 类型已成为 domain 的别名);HK_TYPE_TICK、HK_TYPE_TIMER、HK_TYPE_RCU、HK_TYPE_MISC、HK_TYPE_WQ全部共享HK_TYPE_KERNEL_NOISE的值,因为这几类工作只由nohz_full启动参数设置。
对应的标志位定义在 kernel/sched/isolation.c 的enum hk_flags中:HK_FLAG_DOMAIN_BOOT、HK_FLAG_DOMAIN、HK_FLAG_MANAGED_IRQ、HK_FLAG_KERNEL_NOISE,以位图方式记录当前启用了哪些类型。
启动参数解析:isolcpus= 与 nohz_full=
enum hk_type的四种类型与内核启动参数一一对应,解析逻辑全部位于 kernel/sched/isolation.c。
nohz_full=
nohz_full=的解析入口是housekeeping_nohz_full_setup()(kernel/sched/isolation.c),通过__setup("nohz_full=", ...)注册:
static int __init housekeeping_nohz_full_setup(char *str) { unsigned long flags; flags = HK_FLAG_KERNEL_NOISE; return housekeeping_setup(str, flags); } __setup("nohz_full=", housekeeping_nohz_full_setup);即nohz_full=只设置HK_FLAG_KERNEL_NOISE。从 Documentation/admin-guide/kernel-parameters.txt 可知,该参数要求内核构建时启用CONFIG_NO_HZ_FULL=y,指定列表中的 CPU 会尽可能停止 tick,其 RCU 回调也会被卸载(等价于同时指定了rcu_nocbs=);引导 CPU(boot CPU)会被强制排除在范围之外以维持 timekeeping。当CONFIG_NO_HZ_FULL未启用时,housekeeping_setup()会打印Housekeeping: nohz unsupported. Build with CONFIG_NO_HZ_FULL并直接返回(kernel/sched/isolation.c)。
isolcpus=
isolcpus=的解析入口是housekeeping_isolcpus_setup()(kernel/sched/isolation.c),它支持子参数前缀匹配:
nohz,→HK_FLAG_KERNEL_NOISE(等价于nohz_full);domain,→HK_FLAG_DOMAIN | HK_FLAG_DOMAIN_BOOT;managed_irq,→HK_FLAG_MANAGED_IRQ;- 未知子参数会被跳过并打印
isolcpus: Skipped unknown flag ...;若包含非法字符则打印isolcpus: Invalid flag ...并返回。
如果isolcpus=未带任何子参数,则默认使用domain行为(flags |= HK_FLAG_DOMAIN | HK_FLAG_DOMAIN_BOOT,见 kernel/sched/isolation.c),这与 Documentation/admin-guide/kernel-parameters.txt 中 "default: domain" 的说明一致。
housekeeping_setup() 的核心校验
两种参数最终都进入housekeeping_setup()(kernel/sched/isolation.c),其中有两处关键校验:
必须保留至少一个在线 housekeeping CPU:
cpumask_andnot(housekeeping_staging, cpu_possible_mask, non_housekeeping_mask)计算非隔离 CPU 集合后,检查cpumask_first_and(cpu_present_mask, housekeeping_staging),若不存在则强制把引导 CPU(smp_processor_id())加回 housekeeping 集合,并打印Housekeeping: must include one present CPU, using boot CPU:%d(kernel/sched/isolation.c)。nohz_full=与isolcpus=的组合一致性:第二次调用(先nohz_full=后isolcpus=,或相反)时,会校验两个参数的非隔离 CPU 集合必须一致,否则打印Housekeeping: nohz_full= must match isolcpus=(kernel/sched/isolation.c);同时还会校验至少存在一个既不在nohz_full=也不在isolcpus=domain中的 present CPU,以避免定时器迁移层级(timer migration hierarchy)出现问题——managed_irq不参与该校验(kernel/sched/isolation.c)。
首次调用时通过housekeeping_setup_type()为每个启用的类型分配并保存初始 cpumask(使用memblock_alloc_or_panic,kernel/sched/isolation.c);若设置了HK_FLAG_KERNEL_NOISE且尚未设置,还会调用tick_nohz_full_setup()(kernel/sched/isolation.c)。
Housekeeping cpumask:核心查询接口
Housekeeping cpumask 包含可以执行被隔离特性移走工作的 CPU。核心查询函数为(kernel/sched/isolation.c):
const struct cpumask *housekeeping_cpumask(enum hk_type type)其行为逻辑如下:
- 默认情况下(既不使用
nohz_full=,也不使用isolcpus=,也不使用 cpuset 隔离分区——这覆盖了绝大多数使用场景),函数返回cpu_possible_mask; - 否则返回对应隔离特性的 cpumask补集。例如:
- 使用
isolcpus=domain,7时,housekeeping_cpumask(HK_TYPE_DOMAIN)返回除 CPU 7 外所有可能 CPU 的掩码; - 使用
nohz_full=5,6时,housekeeping_cpumask(HK_TYPE_KERNEL_NOISE)返回除 CPU 5、6 外所有可能 CPU 的掩码。
- 使用
实现上,函数首先通过static_branch_unlikely(&housekeeping_overridden)判断是否覆盖了默认行为,再检查housekeeping.flags中对应类型位是否置位,命中则通过rcu_dereference读取housekeeping.cpumasks[type](这是一个struct cpumask __rcu *数组,见 kernel/sched/isolation.c)。housekeeping_overridden是导出的静态键(EXPORT_SYMBOL_GPL),当CONFIG_CPU_ISOLATION未启用时,include/linux/sched/isolation.h 提供了一组内联空实现:housekeeping_cpumask()直接返回cpu_possible_mask,housekeeping_any_cpu()返回当前 CPU。
除查询函数外,头文件还导出了其他常用接口(include/linux/sched/isolation.h):
housekeeping_any_cpu(type):返回一个可用于该类型 housekeeping 工作的 CPU;housekeeping_enabled(type):判断该类型是否已启用;housekeeping_affine(t, type):把任务的 CPU 亲和性限制到 housekeeping cpumask;housekeeping_test_cpu(cpu, type):判断指定 CPU 是否属于该类型的 housekeeping 集合;housekeeping_update(isol_mask):供 cpuset 在运行时更新HK_TYPE_DOMAIN掩码;housekeeping_init():启动早期初始化。
housekeeping_any_cpu()的实现(kernel/sched/isolation.c)值得注意:它先尝试sched_numa_find_closest()在 NUMA 上找距离当前 CPU 最近的 housekeeping CPU,失败则退回到cpumask_any_and_distribute()在 housekeeping 与在线 CPU 的交集中分布式选取;仅当没有任何候选(例如引导阶段第一个 housekeeping CPU 尚未上线)时才回退到smp_processor_id()。而housekeeping_affine()则直接调用set_cpus_allowed_ptr()收紧任务的 cpuset(kernel/sched/isolation.c)。
与 cpuset 的同步:基于 RCU 的方案
cpuset 在创建、修改或删除隔离分区时,会修改HK_TYPE_DOMAIN的 housekeeping cpumask。因此HK_TYPE_DOMAIN的用户必须与 cpuset 正确同步,以满足三个要求(Documentation/core-api/housekeeping.rst):
- cpumask 快照保持一致;
- 不允许把 housekeeping 工作排队到新近变成隔离的 CPU 上;
- 在创建/修改的隔离分区对用户空间可见之前,必须冲刷掉那些排队到"刚刚变成隔离"的 CPU 上的待处理 housekeeping 工作。
该同步由一套RCU 方案维护:
- 更新侧:cpuset 在更新
HK_TYPE_DOMAINcpumask 之后、冲刷待处理工作之前,等待一个 RCU 宽限期(grace period); - 读侧:必须在同一个 RCU 读侧临界区内完成"housekeeping 目标选取"和"工作入队"两个操作。
更新侧:housekeeping_update()
housekeeping_update()是更新侧的典型实现(kernel/sched/isolation.c),其核心骨架与文档示例一致:
rcu_assign_pointer(housekeeping.cpumasks[type], trial); synchronize_rcu(); flush_workqueue(example_workqueue);完整流程是:
- 分配一个临时 cpumask
trial,用cpumask_andnot(trial, housekeeping_cpumask(HK_TYPE_DOMAIN_BOOT), isol_mask)计算新的 domain housekeeping 掩码; - 校验新掩码与在线 CPU 集合有交集,否则返回
-EINVAL; - 首次启用时通过
static_branch_enable(&housekeeping_overridden)打开覆盖开关; - 用
rcu_assign_pointer()发布新的 cpumask 指针; - 调用
synchronize_rcu()等待读侧退出临界区; - 依次冲刷受影响的 workqueue:
pci_probe_flush_workqueue()、mem_cgroup_flush_workqueue()、vmstat_flush_workqueue(); - 调用
workqueue_unbound_housekeeping_update()把新的 housekeeping 掩码传播给非绑定 workqueue; - 调用
tmigr_isolated_exclude_cpumask()从定时器迁移层级中排除隔离 CPU; - 调用
kthreads_update_housekeeping()更新相关 kthread 的亲和性; - 释放旧掩码。
从源码结构看,第 5、6 步正是文档所述"等待 RCU 宽限期后再冲刷待处理工作"的落地实现。
读侧:选取与入队必须在同一 RCU 临界区
读侧的典型布局如下(见 Documentation/core-api/housekeeping.rst):
rcu_read_lock(); cpu = housekeeping_any_cpu(HK_TYPE_DOMAIN); queue_work_on(cpu, example_workqueue, work); rcu_read_unlock();即"选取 housekeeping CPU"与"按该 CPU 入队 work"必须处于同一个rcu_read_lock()/rcu_read_unlock()之间。这样更新侧synchronize_rcu()就能保证:在它返回时,任何先前的入队操作都已经完成,从而可以安全地冲刷 workqueue,而不会把工作遗留在刚被隔离的 CPU 上。
cpuset 侧的调用链
cpuset 侧的同步由 kernel/cgroup/cpuset.c 完成。关键调用路径是cpuset_update_sd_hk_unlock()(kernel/cgroup/cpuset.c):当update_housekeeping标志被置位时,它把isolated_cpus拷贝到isolated_hk_cpus,然后在不持有cpus_read_lock和cpuset_mutex(仅保留cpuset_top_mutex做互斥)的情况下调用housekeeping_update(isolated_hk_cpus)。
prstate_housekeeping_conflict()(kernel/cgroup/cpuset.c)则体现了HK_TYPE_DOMAIN_BOOT的约束:当该类型启用时,非隔离分区(prstate != PRS_ISOLATED)的 CPU 集合必须是housekeeping_cpumask(HK_TYPE_DOMAIN_BOOT)的子集,也就是说isolcpus=domain圈定之外的 CPU 只能用于隔离分区。注释同时说明(kernel/cgroup/cpuset.c):调用housekeeping_update()时必须持有cpus_read_lock(CPU 热插拔锁),否则热插拔操作可能与之并发。
内核其他子系统的使用与传播
非绑定 workqueue 的掩码传播
workqueue_unbound_housekeeping_update()(kernel/workqueue.c)在housekeeping_update()中被调用,用于把新的 housekeeping cpumask 传播到非绑定 workqueue:有效非绑定亲和性是"新 housekeeping 掩码"与"通过 nohz_full=/isolcpus= 或 sysfs 请求的亲和性集合"的交集;若交集为空则回退到请求掩码。更新成功后,wq_isolated_cpumask(cpu_possible_mask与 housekeeping 掩码的差集)会通过 sysfs 导出。这也是nohz_full=场景中全局 workqueue 需要手动配置/sys/devices/virtual/workqueue/cpumask来保护个别 CPU 的原因(见 Documentation/admin-guide/kernel-parameters.txt)。
调度 tick 的卸载
housekeeping_init()(kernel/sched/isolation.c)在启动早期执行:若设置了HK_FLAG_KERNEL_NOISE则调用sched_tick_offload_init()(位于 kernel/time/tick-sched.c 附近的tick_nohz_full_setup()同文件)为远程调度 tick 卸载做准备;随后把所有启用的类型重新分配为可 kfree 的 cpumask,并检查每个掩码非空("至少需要一个 CPU 处理 housekeeping 工作"),旧掩码进入memblock_freelist,最终由housekeeping_late_init()(pure_initcall)释放(kernel/sched/isolation.c)。
实践配置示例
综合上述机制,一个典型的 CPU 隔离配置(例如为 DPDK 预留 CPU 7)如下:
isolcpus=domain,managed_irq,7 nohz_full=7 rcu_nocbs=7isolcpus=domain,7:把 CPU 7 移出调度域(负载均衡、非绑定 workqueue/定时器);isolcpus=managed_irq,7:把 CPU 7 从 managed IRQ 的目标集合中移出;nohz_full=7:CPU 7 上尽量停掉 tick(需CONFIG_NO_HZ_FULL=y),其 RCU 回调被卸载;rcu_nocbs=7:显式把 CPU 7 的 RCU 回调卸载到 housekeeping CPU(nohz_full=已隐含此效果)。
需要注意的前提:
- 必须保留至少一个在线 housekeeping CPU(通常引导 CPU 会被强制保留);
nohz_full=与isolcpus=的非隔离集合必须一致,否则内核会告警并忽略;isolcpus=domain的域隔离是不可逆的(见 Documentation/admin-guide/kernel-parameters.txt),需要运行时动态调整时应改用 cpuset 隔离分区,而housekeeping_update()正是 cpuset 分区变化时动态更新HK_TYPE_DOMAIN的桥梁。
小结
Housekeeping 机制是 Linux CPU 隔离体系的内务基础设施:enum hk_type定义了四类被移走的工作(调度域、内核噪声、managed IRQ 与纯启动域),housekeeping_cpumask()等接口以补集形式返回可执行这些工作的 CPU 集合,而基于 RCU 的同步方案确保了 cpuset 运行时调整隔离分区时,housekeeping 工作的选取与入队不会落到新隔离的 CPU 上。理解这一机制,是正确配置isolcpus=/nohz_full=、分析 DPDK 等低抖动负载抖动来源的基础。
参考文件
- Documentation/core-api/housekeeping.rst(本文主线文档)
- include/linux/sched/isolation.h(
enum hk_type与接口声明) - kernel/sched/isolation.c(启动参数解析、cpumask 维护、RCU 更新)
- kernel/cgroup/cpuset.c(cpuset 隔离分区与
housekeeping_update()调用) - kernel/workqueue.c(非绑定 workqueue 掩码传播)
- Documentation/admin-guide/kernel-parameters.txt(
isolcpus=/nohz_full=参数语义)
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考