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推荐使用如下内核参数isolcpusmanaged_irq,2,3 irqaffinity0,1 nohz_full2,3 rcu_nocbs2,3 rcu_nocb_poll nosoftlockup1. 使用isolcpus隔离 CPU 核心isolcpusmanaged_irq,2,3isolcpus是 Linux 内核的一个启动参数用于将指定的 CPU 核心从内核的通用调度器即默认的调度域中隔离出来。这意味着这些被隔离的 CPU 核心将不会被普通的用户进程或内核线程除了一些必要的例外自动调度使用从而为特定任务如实时应用、高性能计算、低延迟服务等保留专用的 CPU 资源。根据 Linux 内核版本的不同isolcpus的格式可能有所不同。内核版本isolcpuscpu-listisolcpus[flag-list,]cpu-list 4.15支持不支持flag-list 4.15支持支持flag-listdomain、nohz 5.6支持支持flag-listdomain、nohz、managed_irq(1)isolcpuscpu-listcpu-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-listflag-list用于指定isolcpus的行为可以包含以下参数domain默认行为仅将 CPU 从通用调度域中移除但仍可用于中断处理。nohz等同于nohz_full参数。为了兼容性推荐使用nohz_full。managed_irq避免在隔离 CPU 上处理托管中断。具体参考下文。2. 避免在隔离的 CPU 核心处理中断isolcpusmanaged_irq,2,3 irqaffinity0,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 核心上处理托管中断和非托管中断。isolcpusmanaged_irq,2,3用于避免在 CPU 2 和 3 上处理托管中断irqaffinity0,1用于将非托管中断的默认亲和性设为 0-1同样避免在 CPU 2 和 3 上处理非托管中断。针对非托管中断用户态 irqbalance 服务会根据负载自动调整中断亲和性所以必须在用户态卸载或关闭此服务。卸载 irqbalance 服务sudo apt remove irqbalance禁用irqbalance服务sudosystemctl stop irqbalancesudosystemctl disable irqbalance3. 避免在隔离的 CPU 核心处理时钟中断nohz_full2,3Linux 的调度器依赖“心跳”——每隔 1 ms 产生一次的 timer tick用来更新调度时间片、维护 CPU 负载均衡、触发 RCU 回调等。在大多数机器上这个心跳由 CONFIG_HZ1000 驱动每秒 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_IDLENO_HZ_FULLCPU空闲时无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_fullcpu-list来设置。nohz_fullcpu-list符合当前的需求。NO_HZ_FULL并非真完全(所谓FULL)没有 tick 中断也仅在单任务时生效多任务时仍会恢复tick。而且这些任务不仅仅包括用户态任务还包括内核态任务例如 RCU 等内核线程。需要把这些内核线程从隔离的 CPU 核心上挪走接下来将介绍如何做到这一点。4. 避免在隔离的 CPU 核心执行 RCU 回调内核线程rcu_nocbs2,3 rcu_nocb_pollRCURead-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_nocbs2,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 看门狗内核线程nosoftlockupnosoftlockup是一个内核启动参数boot parameter其作用是彻底禁用“soft lockup”检测机制。(1) 什么是 soft lockupLinux 在每 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_cpus0x0dCobalt 实时内核管理的 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_cpus0x0c。使用此参数后实际运行发现 Cobalt 实时内核没有启动。# corectl --status 0000.000| BUG in low_init(): [main] Cobalt core not enabled in kernel因为对于 Xenomai3.2 以上版本xenomai.supported_cpus必须包含 CPU 0否则 Cobalt 实时内核无法启动。修改参数为xenomai.supported_cpus0x0d后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 亲和性掩码 00000003cat /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);