8.5 CPU 隔离与绑定
从硬件的角度出发,Cobalt 实时内核可以接管设备中断甚至通过 RTDM 接管整个设备,但是这并不意味着 Cobalt 实时内核可以独占整个硬件系统。
在对称多处理器(SMP)系统中,Linux 内核和 Cobalt 实时内核共享 CPU 多核资源(CONFIG_SMP 选项开启)。
隔离 CPU 的必要性是什么?
如果 Linux 普通任务被调度运行在某个 CPU 上,则该 CPU 上的实时任务的上下文将会被切换出去。当需要重新执行原实时任务时,需要将该 CPU 上的实时任务的上下文切换回来。切换任务的上下文会带来延迟,另外指令缓存和数据缓存可能会被刷新或部分失效,这会增大实时任务的延迟。
为了更好的解释 CPU 隔离与绑定,假定当前 QEMU ARM64 虚拟机中有 4 个 CPU 核心,其中 CPU0/CPU1 用于运行 Linux 内核及普通任务,CPU2/CPU3 用于运行 Cobalt 实时内核及实时任务。
8.5.1 Linux 内核隔离 CPU 核心
本书使用的内核版本是 5.10,推荐使用如下内核参数:
isolcpus=managed_irq,2,3
irqaffinity=0,1
nohz_full=2,3
rcu_nocbs=2,3 rcu_nocb_poll
nosoftlockup
1. 使用 isolcpus 隔离 CPU 核心
isolcpus=managed_irq,2,3
isolcpus 是 Linux 内核的一个启动参数,用于将指定的 CPU 核心从内核的通用调度器(即默认的调度域)中隔离出来。这意味着这些被隔离的 CPU 核心将不会被普通的用户进程或内核线程(除了一些必要的例外)自动调度使用,从而为特定任务(如实时应用、高性能计算、低延迟服务等)保留专用的 CPU 资源。
根据 Linux 内核版本的不同,isolcpus 的格式可能有所不同。
| 内核版本 | isolcpus=<cpu-list> |
isolcpus=[flag-list,]<cpu-list> |
|---|---|---|
| < 4.15 | 支持 | 不支持 flag-list |
| >= 4.15 | 支持 | 支持 flag-list:domain、nohz |
| >= 5.6 | 支持 | 支持 flag-list:domain、nohz、managed_irq |
(1) isolcpus=<cpu-list>
cpu-list 语法用于指定一组处理器(CPU核心)的列表,用于指定哪些 CPU 核心应该被排除。这种语法简洁且灵活,支持单个、多个、连续范围以及混合格式的 CPU 列表定义。下面是关于 cpu-list 语法的详细解释:
-
单个CPU:直接使用CPU编号表示。
- 示例:
2表示仅选择编号为2的CPU核心。
- 示例:
-
多个独立CPU :使用逗号
,分隔各个CPU编号。- 示例:
0,3,5表示选择编号为0、3和5的CPU核心。
- 示例:
-
连续范围的CPU :使用连字符
-表示一个范围内的所有CPU。- 示例:
2-4表示选择编号从2到4的所有CPU核心,即包括2、3、4。
- 示例:
-
组合形式:可以结合以上两种方式,以创建更复杂的CPU列表。
- 示例:
0,2-4,6表示选择编号为0、2、3、4和6的CPU核心。
- 示例:
(2) isolcpus=[flag-list,]<cpu-list>
flag-list 用于指定 isolcpus 的行为,可以包含以下参数:
domain:默认行为,仅将 CPU 从通用调度域中移除,但仍可用于中断处理。nohz:等同于nohz_full=参数。为了兼容性,推荐使用nohz_full=。managed_irq:避免在隔离 CPU 上处理托管中断。具体参考下文。
2. 避免在隔离的 CPU 核心处理中断
isolcpus=managed_irq,2,3
irqaffinity=0,1
在上述推荐的参数中,与中断相关的选项有 managed_irq 和 irqaffinity。其中,managed_irq 用于避免在隔离 CPU 核心上处理托管中断,而 irqaffinity 用于指定非托管中断的亲和性。
托管(managed)与非托管(non-managed)中断的区别:托管中断由内核自动分配亲和性,用户态无法再改;非托管中断由驱动初始化时一次性指定,或由管理员通过 /proc/irq/*/smp_affinity 调整亲和性。
| 维度 | 托管中断(managed IRQ) | 非托管中断 |
|---|---|---|
| 绑定决策者 | 内核自动根据队列/NUMA/CPU 拓扑计算 | 驱动初始化时一次性指定,或由 |
| 用户态可改? | 不可。托管中断完全由内核自治 | 可以 。通过 /proc/irq/.../smp_affinity 手动调整亲和性,或者由用户态 irqbalance 服务自动调整 |
| 典型用户 | • NVMe 多队列 • 大多数 SR-IOV VF 网卡队列 • VFIO 透传设备 | • 传统单队列网卡(e1000、8139 等) • 旧版块设备 • 键盘、鼠标、定时器等 |
综上所述,通过 managed_irq 和 irqaffinity 选项组合,可以避免在隔离 CPU 核心上处理托管中断和非托管中断。
isolcpus=managed_irq,2,3用于避免在 CPU 2 和 3 上处理托管中断;irqaffinity=0,1用于将非托管中断的默认亲和性设为 0-1,同样避免在 CPU 2 和 3 上处理非托管中断。
针对非托管中断,用户态 irqbalance 服务会根据负载自动调整中断亲和性,所以必须在用户态卸载或关闭此服务。
-
卸载 irqbalance 服务:
sudo apt remove irqbalance
-
禁用
irqbalance服务:
bash
sudo systemctl stop irqbalance
sudo systemctl disable irqbalance
3. 避免在隔离的 CPU 核心处理时钟中断
nohz_full=2,3
Linux 的调度器依赖"心跳"------每隔 1 ms 产生一次的 timer tick,用来更新调度时间片、维护 CPU 负载均衡、触发 RCU 回调等。在大多数机器上,这个心跳由 CONFIG_HZ=1000 驱动,每秒 1000 次。
一般情况下,Linux 内核默认选择了 NO_HZ_IDLE 即 Idle dynticks system (tickless idle) 。推荐更换 NO_HZ_FULL 即 Full dynticks system (tickless)。
General setup --->
Timers subsystem --->
Timer tick handling (Idle dynticks system (tickless idle)) --->
( ) Periodic timer ticks (constant rate, no dynticks)
( ) Idle dynticks system (tickless idle)
(X) Full dynticks system (tickless)
二者的区别如下表所示:
| 场景 | NO_HZ_IDLE | NO_HZ_FULL |
|---|---|---|
| CPU空闲时 | 无tick中断 | 无tick中断(与NO_HZ_IDLE一致) |
| CPU运行1个任务时 | 仍产生tick中断(默认1000Hz) | 完全关闭tick(调度时钟中断停止) |
| CPU运行≥2个任务时 | 持续tick中断 | 恢复tick中断(需时间片调度) |
NO_HZ_FULL 在哪些 CPU 核心上生效?如果需要覆盖所有 CPU 核心,则启用CONFIG_NO_HZ_FULL_ALL。如果需要灵活选择 CPU 核心,可以通过启动参数 nohz_full=cpu-list 来设置。nohz_full=cpu-list 符合当前的需求。
NO_HZ_FULL 并非真完全(所谓FULL)没有 tick 中断,也仅在单任务时生效,多任务时仍会恢复tick。而且,这些任务不仅仅包括用户态任务,还包括内核态任务例如 RCU 等内核线程。需要把这些内核线程从隔离的 CPU 核心上挪走,接下来将介绍如何做到这一点。
4. 避免在隔离的 CPU 核心执行 RCU 回调内核线程
rcu_nocbs=2,3 rcu_nocb_poll
RCU(Read-Copy-Update,读取-拷贝-更新)是一种内核内用于线程互斥的无锁机制。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个RCU回调(RCU callback)在适当的时机(经过一个宽限期Grace period后)把指向原来数据的指针重新指向新的被修改的数据。
如果不启用内核配置选项 CONFIG_RCU_NOCB_CPU,则默认情况下,RCU 回调由 RCU_SOFTIRQ 软中断处理函数 rcu_process_callbacks 处理。所有 CPU 都参与处理 RCU 回调。
如果启用了 CONFIG_RCU_NOCB_CPU,则可以将某些 CPU 排除在执行 RCU 回调的候选CPU之外,反而让其它CPU专门处理所有RCU回调。处理 RCU 回调的 CPU 被称为 "管家CPU"(housekeeping CPU)。
// CONFIG_RCU_NOCB_CPU
General setup --->
RCU Subsystem --->
-*- Offload RCU callback processing from boot-selected CPUs
该选项会把回调调用任务从启动参数 rcu_nocbs= 指定的 CPU 集合中剥离出来。对于每个被指定的 CPU,都会创建一个 RCU 回调内核线程 "rcuo[p|s]/N" 来负责调用这些回调。
rcuo: RCU offload 的缩写。[p|s]: 代表 RCU 类型- RCU-preempt,则用 "p" (默认)。
- RCU-sched,则用 "s"。
- RCU-bh,则用 "b" (ARM64 5.10 内核未见此选项)。
N: 代表 CPU 编号。
以 rcu_nocbs=2,3 为例,因为已经使用了 isolcpus= 隔离 CPU 核心,所以 CPU 2 和 3 的 RCU 回调内核线程仅在CPU 0 和 CPU 1 上运行。
除此之外,虽然 RCU 回调内核线程负责执行回调,但唤醒这些线程的动作仍由原 CPU (例如CPU 2 和 CPU 3)自己完成。设置内核参数 rcu_nocb_poll 可以让原 CPU 省去唤醒工作:此时 RCU 卸载线程会由一个周期性的定时器定期唤醒并检查是否有回调需要处理。代价是线程被唤醒得更频繁,导致能耗升高、系统负载增加。
实际运行时,ps -elf | grep rcu 命令可以看到所有 RCU 相关的内核线程。其中不仅包括 [rcuop/2] 和 [rcuop/3] 线程,还包括其它内核线程例如 [rcu_gp], [rcuog/2] 等。所有的内核线程都仅在CPU 0 和 CPU 1 上运行,不再一一赘述。
5. 避免在隔离的 CPU 核心执行 softlockup 看门狗内核线程
nosoftlockup
nosoftlockup 是一个 内核启动参数 (boot parameter),其作用是 彻底禁用"soft lockup"检测机制。
(1) 什么是 soft lockup?
- Linux 在每 CPU 上运行一个 watchdog/[softlockup]线程 ,周期性地检查: "当前 CPU 有没有在 N 秒内一直不调度其他任务?"
- 如果某个 CPU 连续 kernel 模式运行 ≥ watchdog_thresh 秒 (默认 20 s)而 没有被调度打断 ,内核就认为发生了 soft lockup :
-
会在控制台打印:
BUG: soft lockup - CPU#N stuck for 22s! -
同时把栈回溯(stack trace)打印出来,方便调试。
-
(2) nosoftlockup 做了什么?
- 在内核初始化 watchdog 时,如果检测到启动参数里有
nosoftlockup,就 完全跳过 watchdog 线程的创建。 - 结果:
- 不再检测 soft lockup;
- 即使某 CPU 永远关中断/关抢占,也不会再有 lockup 警告和栈回溯;
- 节省 CPU 开销。
8.5.2 Cobalt 实时内核管理的 CPU 核心
xenomai.supported_cpus=0x0d
Cobalt 实时内核管理的 CPU 核心可以通过 xenomai.supported_cpus 启动参数来指定。
xenomai.supported_cpus 是实时 CPU 亲和性掩码,该掩码对应源码中的 xnsched_realtime_cpus 变量,表示哪些CPU核心允许运行Xenomai的实时任务。当然,讨论 CPU 掩码的前提是开启 CONFIG_SMP 选项。
xenomai.supported_cpus 输入格式为 16 进制数,每一个 bit 位表示系统中的一个CPU。如果该位为 1,则表示相应的CPU是 Cobalt 实时内核所管理的,并且可用于调度实时应用程序。实时任务默认的 CPU 亲和性掩码会和此掩码保持一致。
如果不传递 xenomai.supported_cpus 这个参数,则默认值为 -1。在 32 位系统上,-1 的值是 0xffffffff,表示 Cobalt 实时内核默认管理 32 个CPU。在 64 位系统上,-1 的值是 0xffffffffffffffff,表示 Cobalt 实时内核默认管理 64 个 CPU。
如果要支持 CPU 2 和 CPU 3,需要置位 bit 2 和 bit 3(0b00001100),即传入参数 xenomai.supported_cpus=0x0c。使用此参数后,实际运行发现 Cobalt 实时内核没有启动。
# corectl --status
0"000.000| BUG in low_init(): [main] Cobalt core not enabled in kernel
因为对于 Xenomai3.2 以上版本,xenomai.supported_cpus 必须包含 CPU 0,否则 Cobalt 实时内核无法启动。修改参数为 xenomai.supported_cpus=0x0d 后,Cobalt 实时内核可以正常启动。
# corectl --status
running
在 Xenomai 系统启动之后,可以通过 /proc/xenomai/affinity 文件查看和修改实时 CPU 亲和性掩码。
# cat /proc/xenomai/affinity <- 显示当前 CPU 亲和性掩码
0000000f
# echo 0x3 > /proc/xenomai/affinity <- 设置 CPU 亲和性掩码
# cat /proc/xenomai/affinity <- 显示当前 CPU 亲和性掩码
00000003
cat /proc/xenomai/affinity 得到的是按16进制输出的CPU亲和性掩码,每一个 bit 位表示系统中的一个CPU。
同时,/proc/xenomai/affinity 支持在线修改,即可以通过 echo 命令将新的 CPU 亲和性掩码写入该文件,系统会立即更新 CPU 亲和性。
8.5.3 实时任务绑定 CPU
上文已经提到,实时任务默认的 CPU 亲和性掩码会和 xenomai.supported_cpus 传入的实时 CPU 亲和性掩码保持一致。
但是,有时我们需要将实时任务进行更加精细的控制,绑定到特定的 CPU 上运行。
对于实时进程或线程,可以通过 taskset 命令或 pthread_setaffinity_np 设置自身的 CPU 亲和性,注意要确保指定的 CPU 位于 Cobalt 实时内核管理的 CPU 范围内。
例如,启动 latency 并将其绑定在 CPU 0 和 1 上,对应的亲和性掩码为 0x3。
# taskset -c 2,3 latency
例如,latency 在代码中支持使用 pthread_setaffinity_np 设置采样线程帮到某个CPU。执行 latency -c 2 命令,绑定采样线程到CPU 2 上。其对应的源码为:
cpu_set_t cpus;
CPU_ZERO(&cpus);
CPU_SET(cpu, &cpus);
ret = pthread_attr_setaffinity_np(&tattr, sizeof(cpus), &cpus);