飞腾平台实时Linux多核心调度优化方案这两年一直在飞腾平台上折腾实时Linux前几篇写了实时方案的补丁搭建、中断线程化改造和时钟源选型这次把多核心调度这块单独拎出来聊聊。做实时系统的朋友应该都有体会在单核上把延迟压到几十微秒并不难真正让人头疼的是上了多核之后任务莫名其妙被调度到别的核心上cache一冷响应时间直接翻倍找半天还不知道问题出在哪儿。这篇就把我这段时间在飞腾多核平台上做实时调度优化的思路、参数调整过程、实测数据和一些排查经验完整梳理一遍给正在搞同类方案的朋友做个参考。先说一个基本事实飞腾平台本身是ARM架构的多核SMP系统跑Linux内核时默认的调度策略对实时任务并不友好——CFS调度器追求的是整体吞吐和公平实时任务的优先级虽然存在但内核线程、中断处理、软中断这些系统负载会把实时任务的执行环境搅得一塌糊涂。再加上ARM架构里中断控制器GIC的路由规则和x86不完全一样核间中断IPI的延迟、CPU亲和性的默认行为都直接影响实时任务的最坏响应时间。所以多核心调度优化的本质不是简单地把实时任务优先级调高而是围绕“确定性”这三个字把可能干扰实时任务的每一个因素逐个隔离掉。1. 多核心调度为什么是实时性的关键瓶颈1.1 实时系统关注的是最坏情况不是平均表现做实时系统的工程师都知道衡量实时性能的核心指标叫“最坏执行时间”和“中断/调度响应延迟”不是你跑一千次任务平均耗时多少而是那最差的一次响应到底多少微秒。工业生产里一个典型的现场设备控制场景——飞腾平台同时跑着数据采集、人机界面、网络通信和控制闭环如果控制闭环的周期是1毫秒那么从外部信号触发到控制输出翻转这个链路里任何一个环节出现几十微秒的抖动都可能让执行机构的动作产生肉眼可见的偏差严重的会直接导致报警停机。多核心调度最麻烦的地方在于任务被调度到哪个CPU核上运行不是固定的。今天跑在核0上明天可能跑在核2上核间切换这个过程里任务要么被全局调度器重新排队要么因为CPU亲和性设置不到位而反复迁移每一次迁移都意味着执行上下文要从旧的CPU核上保存再在新核上恢复新核的L1/L2 cache里没有这个任务的热数据全部缓存缺失访存延迟瞬间上去了新核的运行队列可能还有别的任务在排队实时任务需要等待如果迁移发生在实时任务的执行中场时间片分布就完全失去了确定性。所以在多核平台上谈实时第一原则就是让该固定的东西全部固定下来。CPU核固定、中断固定、内核线程固定、实时任务归属固定把“动态”的部分从这个系统里尽可能剔除。1.2 多核场景下实时任务面临的四类干扰源我把这段时间在飞腾平台上观察到的实时任务干扰因素归纳成四类这四类因素也是下面所有优化操作要解决的目标。第一类是调度器本身造成的调度延迟。即使任务已经设置了SCHED_FIFO策略和最高实时优先级内核的调度器在做出“应该切换到这个任务”的决定时仍需要处理运行队列的锁竞争、核间负载均衡的考量。在完全没有做隔离的系统里每个CPU核都可能被CFS任务占满实时任务在唤醒后不一定能立即抢占到CPU因为优先级虽然够高但调度器的抢占点必须等到内核完成当前临界区的处理后才生效。第二类是中断和软中断带来的抢占干扰。ARM架构里的中断控制器把外部中断路由到某个CPU核上如果不做配置GIC默认分发策略可能导致中断频繁落在同一个核上。更关键的是即使我们把实时任务绑到了核3如果网卡中断、串口中断、定时器中断全部往核3上灌那么实时任务执行到一半一个串口中断进来就能抢占它几十微秒到上百微秒。这个影响在实测里非常明显——中断风暴一来最大响应延迟直线上升。第三类是内核线程和内核活动的迁移干扰。Linux内核有大量周期性的辅助线程比如rcuos、migration、ksoftirqd这些它们在多核之间跳动如果恰好跳到实时任务所在的核心并且该核心没有做隔离这些内核线程的调度活动就会污染实时任务的执行环境。第四类是缓存和内存带宽的竞争。这一点在ARM多核平台上格外明显因为ARM处理器普遍是用共享L2或集群架构设计的一个核上的DMA操作或者大量内存拷贝可能通过总线争用拖慢另一个核的访存速度。某个核上的数据采集线程频繁读写内存实时任务即使绑在另一个核上内存带宽的争用一样会放大它的执行时间抖动这不是调度器能通过优先级调整解决的必须从内存分配和总线隔离层面想办法。1.3 飞腾平台做实时调度的硬件特殊性飞腾平台和x86平台在实时优化上有个显著差异x86上有现成的ACPUI和调度辅助机制而飞腾平台的ARM架构更依赖内核参数和中断路由配置的精细控制。ARM的GIC中断控制器对中断有独立的亲和性配置接口比如/proc/irq/xxx/smp_affinity_list你可以把指定中断固定到某一个核也可以通过GIC的distributor配置实现更底层的路由策略在实时场景里这两条路都要走通。再有就是飞腾平台的处理器核数通常从4核到几十核不等核心频率、L2划分方式在不同型号上有差异。高核数平台反而更适合做CPU隔离——把实时任务隔离到专门的核上其他核跑业务但核数少的板卡就需要精打细算哪个核跑实时、哪个核跑非实时、哪个核专门处理中断分配方案要因地制宜。2. 实时调度基础与策略选型2.1 PREEMPT_RT内核是必需的底座飞腾平台跑实时Linux首先要保证内核打上了PREEMPT_RT补丁。PREEMPT_RT把Linux从“抢占点受限”的内核改造成“几乎处处可抢占”的内核它做了三件关键的事将大部分spinlock替换为raw_spinlock允许在临界区里被更高优先级的实时任务抢占把中断处理程序线程化中断到来后先唤醒一个内核线程而不是直接抢占当前任务这样实时任务不会被中断硬生生打断太久引入了irqsoff、preemptoff、scheduling等追踪点方便测量延迟来源。如果你还在用标准内核做实时就谈不上多核调度优化因为标准内核里一次锁竞争占用中断现场的时间可能就顶掉整个实时任务的一个周期。飞腾平台上编译PREEMPT_RT内核和x86流程差不多但要注意交叉编译工具链与内核版本匹配以及把CONFIG_PREEMPT_RT、CONFIG_HZ_1000这样的配置项开到位。2.2 三种实时调度策略的选择Linux对实时任务提供了三种调度策略实际使用中要区分清楚SCHED_FIFO是最常用的实时调度策略带固定优先级范围1到99高优先级的任务总是先运行同级任务按先来先服务顺序除非任务主动让出CPU或被更高优先级任务抢占否则一致运行下去。适合控制闭环、运动控制这类需要连续占核的周期性任务。SCHED_RR也是固定优先级但同级任务之间按时间片轮转适合多个同优先级实时任务需要共享CPU的场景比如同时跑两路控制算法它们优先级一样希望各自分到CPU时间。SCHED_DEADLINE是Linux 3.14之后引入的基于EDF最早截止时间优先机制的调度策略你给任务设定运行时间/周期/截止时间三个参数内核按截止时间动态安排执行性能上限理论上更高但实际使用中参数设置错误会导致任务直接被调度器判为“不可调度”对系统设计能力要求较高。我给工业设备做实时方案时固定就选SCHED_FIFO优先级按控制闭环最高、数据采集次之、日志和通信最低这样的顺序排列原因很简单确定性优先而SCHED_FIFO的行为模型最容易被工程师理解和测量。2.3 优先级设定与优先级反转问题实时系统的优先级不是越高越好还要考虑优先级反转。典型的三任务场景低优先级任务持有一把锁高优先级任务在等这把锁中等优先级任务刚好在运行结果高优先级任务被锁住中等优先级任务反而抢占了低优先级任务的执行最后高优先级任务被困死。解决这个问题Linux有优先级继承机制——低优先级任务持有锁的时候临时继承等待者的优先级从而避免中等优先级任务的插队。实际操作中我见过很多工程师把实时任务优先级设成99然后发现一个问题网卡的中断线程也是高优先级某次大流量数据包进来中断线程把实时任务挤到后面去控制周期全乱了。优先级分配是要全盘考虑的线程、中断、内核线程、定时器各自占多少优先级必须在设计阶段就有一张清晰的表而不是用到哪个设哪个。3. 多核心调度优化完整实操过程3.1 第一步用内核参数把CPU核分出亲区和隔离区多核调度优化的第一步不是改代码而是先改内核启动参数。以一块飞腾8核板卡为例我需要把它分成两类核核0-核3跑Linux常规业务、网络、界面、文件系统核4-核7跑实时任务其中核4专用于主控制闭环核5用于数据采集核6/核7作为备用和中断处理核。实现这个分区在bootargs里加isolcpus4-7 nohz_full4-7 rcu_nocbs4-7就足够了。解释一下这三个参数的职责isolcpus4-7告诉内核调度器默认情况下列表里的CPU核不参与普通任务的负载均衡普通任务不会主动落到这些核上。注意isolcpus屏蔽的是默认调度不表示这些核完全不可用实时任务如果想绑定这些核仍然可以通过亲和性设置让它运行。nohz_full4-7把这些核从内核的周期性tick中解放出来当核上只有一个任务时内核时钟中断可以被关闭或大幅减少频率减少了周期性中断对实时任务的扰动。rcu_nocbs4-7把RCU回调处理从这些核上转移出去避免RCU相关的后台线程在这几个核上周期性调度。这三个参数配合起来就是给实时任务划出一块“清静之地”。启动之后你可以用cat /sys/devices/system/cpu/isolated确认隔离是否生效。另外建议把skew_tick1和CLOCK_EVT相关的动态tick选项打开skew_tick会让不同CPU核的tick在时间上错开避免多核tick在同一个时刻一起触发造成短暂的并发中断风暴。3.2 第二步中断亲和性调整把硬件中断从实时核上赶走实时任务所在的核原则上不接收任何非必要的硬件中断。实际操作中我先把网卡、串口、USB、定时器等所有外围设备的IRQ统一查看一遍命令是cat /proc/interrupts这个文件会列出每个中断向量在各个CPU核上的累计触发次数。如果发现某个中断大量落在核4上就要通过echo 8 /proc/irq/xxx/smp_affinity这样的方式把它指向核0到核3中的某个核。更精确的做法是使用/proc/irq/xxx/smp_affinity_list直接写CPU编号列表比如echo 2 /proc/irq/88/smp_affinity_list表示把88号中断固定到核2。有些中断由驱动注册时设置了IRQF_NOBALANCING标志不会自动参与亲和性迁移需要在内核代码里用irq_set_affinity显式设置飞腾平台上GIC相关的中断尤其要注意这一点。可能有人会问中断全都赶走了那实时任务所在核的定时器中断怎么办答案是通过nohz_full和动态tick机制当一个核上只有实时任务在跑的时候内核会自动降低tick频率在纯任务执行阶段甚至可以做到几乎不产生tick中断这个实测效果非常明显调度延迟的基线整个往下走了一个量级。3.3 第三步内核线程与实时任务的CPU亲和性落地隔离参数只是把核“划分”出来了真正要把实时任务、内核线程、中断处理各就各位还需要逐项设置亲和性。这个过程我分三步做第一步是把内核对应用任务的亲和性设置好。修改应用的启动脚本在start_rt_app之前调用sched_setaffinity系统调用把控制闭环线程的CPU亲和性设置为核4。如果应用用的是pthread就用pthread_setaffinity_np。这里我强烈建议不要用taskset命令在启动时临时垫一下因为应用内部的线程若不显式设置亲和性很可能被调度到隔离核之外的核上跑这个问题我踩过后来干脆在应用代码里统一封装了一个set_realtime_thread函数里面一次性完成策略、优先级、亲和性三个设置。第二步是处理内核线程。隔离核上的migration/4、ksoftirqd/4这些内核线程虽然没有实时任务在跑但它们依然存在会在特定情况下被唤醒。把它们的CPU亲和性全部保留在各自核上就行不需要特别处理。关键是rcu_preempt、kworker这些全局线程可能会跳到隔离核上执行通过taskset -pc 4-7 pid把它们全部限定到核0-核3防止污染。第三步是配置实时任务的调度优先级。控制闭环主线程设为SCHED_FIFO优先级80数据采集线程设SCHED_FIFO优先级70备用调度处理线程设60非实时业务线程全部用CFS默认策略。这样在任何时刻只要控制闭环线程处于就绪态它一定是系统里第一个被调度执行的没有任何其他线程能打断它。3.4 第四步调度器全局参数细调完成绑核之后还有一个很容易被忽略的层面就是调度器自身的几个全局参数。/proc/sys/kernel/sched_rt_period_us和/proc/sys/kernel/sched_rt_runtime_us构成了实时任务CPU带宽限制系统默认实时任务最多占用CPU的95%带宽剩余5%留给非实时任务。如果你的系统里实时任务确实需要长期独占某个核可以把runtime设为-1表示不限制命令是echo -1 /proc/sys/kernel/sched_rt_runtime_us。不过一般不建议这么干除非你已经把实时任务限定到了隔离核并且非实时任务不会饿死。/proc/sys/kernel/sched_cfs_bandwidth_slice_us这个参数是CFS带宽控制的调度粒度默认值在5毫秒到10毫秒之间可以适当调小增强CFS任务之间的响应性但这跟实时任务关系不大我不太建议在这个层面增加复杂度。另外需要关注的是/sys/kernel/debug/sched_features里的NO_HZ_FULL和NO_HZ标记确保内核确实启用了动态tick。调试的时候可以通过watch -n 1 -d cat /proc/interrupts观察实时核上的中断计数是否明显减少之前没做隔离时每秒几百次的定时器中断做完隔离后每秒只剩个位数甚至零直观且有说服力。4. 实测数据调优前后的延迟对比4.1 测试环境与压测工具我在一块飞腾8核板卡上跑了一套完整的对比测试。实时任务设计成典型的控制闭环模型1毫秒周期任务体内做20微秒的计算然后挂起等待下一轮唤醒另一个数据采集任务以500微秒周期运行通过DMA搬运传感器数据。外围同时跑着网络流量注入、日志写入和一块压力测试程序故意占用核0-核3的CPU制造系统负载。测量方式用的是cyclictest的扩展版本同时用内核的trace-cmd记录了调度器和中断事件。测三项指标实时任务从唤醒到实际运行的调度延迟从外部GPIO信号触发到任务响应并翻转GPIO的端到端延迟任务执行时间本身的抖动范围。4.2 调优前后的数据汇总默认配置无任何隔离和亲和性设置下调度延迟的表现是最差的。这里把实测数据统计出来我挑几个有代表性的测试场景平均调度延迟最大调度延迟执行时间P99抖动端到端最长响应默认配置全核负载约120微秒约840微秒波动超过200微秒约1.3毫秒仅设置SCHED_FIFO高优先级约65微秒约320微秒波动约120微秒约560微秒完成隔离绑核中断亲和性约8微秒约85微秒波动约40微秒约150微秒看到这个数字变化大家就能理解为什么多核心调度优化这么值得做——840微秒到85微秒最大延迟直接缩到了十分之一端到端响应从超过1毫秒降到150微秒以内。对一个周期1毫秒的控制闭环来说这个差距意味着系统是否有能力跑起来。4.3 数据解读与稳定性分析调优后的最大调度延迟仍然有85微秒这个数值肯定不能让人满意问题出在哪里呢用trace-cmd深挖后发现这部分延迟主要来自两个地方一个是实时任务本身被其他实时优先级线程抢占。备用数据采集线程的优先级设定过高在极端情况下它执行DMA搬运时中间还有内核的irq线程参与优先级链路的嵌套产生了额外的延迟。解决办法是把备用线程优先级从70降到50并且给它单独绑核不让它和主控制任务在同一核上竞争。另一个是缓存刷新带来的执行时间抖动。即便绑核成功了控制闭环任务运行所需的代码和数据结构还是分散在不同的内存页上每次唤醒时指令cache和data cache重新填充都需要时间这个体现了内存访问延迟对实时任务的影响。后来我把实时任务的关键数据结构和控制代码做了mlock内存锁页同时在编译时通过-falign-functions -falign-loops优化代码对齐并让控制算法使用的数据放在预分配的热页里。这样处理之后执行时间的P99抖动从40微秒降到了12微秒左右整体稳定了很多。5. 常见问题与排查技巧实录5.1 最容易踩的四个坑第一个坑是isolcpus参数不生效或者部分生效。很多时候bootargs写了隔离核但启动后/sys/devices/system/cpu/isolated里确实是空的原因是内核配置里CONFIG_CPUSET没有打开或者initramfs阶段的systemd把cpuset重新初始化了。解决方法是检查内核配置同时确认isolcpus的写法和grub/uboot的引导参数传递是否正确。第二个坑是任务的亲和性被继承机制改掉。应用主进程设置了亲和性之后通过fork()或pthread_create()创建的子线程默认继承父进程的CPU亲和性听起来没问题但如果父进程设置亲和性的代码在创建线程之后执行那么已经创建的线程就错过了亲和性设置继续乱跑。我踩过几次之后现在一律在主线程创建任何子线程之前完成亲和性初始化或者在每个实时线程的首行代码里显式pthread_setaffinity_np。第三个坑是DMA内存的缓存一致性问题。实时任务绑核后如果相邻核上有DMA操作频繁改写同一块内存ARM架构下DMA虽然走的是总线但缓存一致性问题会导致实时任务读到的数据不稳定。我在做飞腾平台的实时采集时把DMA缓冲区的内存配置为device属性或使用无缓存映射避免缓存一致性问题干扰实时数据链路。第四个坑是盲信“隔离核完全免打扰”。隔离核在启动后仍然可能收到系统管理中断、性能计数器溢出中断、NMIC等不可屏蔽中断。其中性能计数器溢出中断最隐蔽如果你开启了perf事件剖析板上某个核的PMU溢出中断可能会路由到隔离核上。排查办法是cat /proc/interrupts看是否存在PMU中断计数如果有把它也设置亲和性指向非隔离核。5.2 基于追踪的排查思路如果做完优化后延迟指标依然不理想建议用追踪的方式定位而不是猜。Linux实时内核自带ftrace配合trace-cmd就能把每一个延迟事件的现场完整记录出来。我常用的排查流程是先全量记录调度和中断事件跑10分钟后在trace-cmd report里筛选实时任务唤醒后到sched_switch到它执行之间的时间窗口看这段时间里是什么东西占据了CPU是中断还是高优先级线程。再看核间迁移事件确认实时任务是否被迁移过如果被迁移了检查亲和性设置是不是没生效。排查软中断问题时可以观察softirq进入和离开的耗时特别是网络接收路径的NEtwork softirq很多不必要的网络包处理可以通过napi参数调整或RPSReceive Packet Steering的配置来减少对实时核的影响。RPS在嵌入式平台上其实不建议对实时核开它会引入额外的IPI增加噪声。对于时间抖动持续偏大的场景重点看是不是cgroup的CPU带宽限制在起作用。某些系统基础镜像里会默认给cgroup加cpu.max限制实时任务被放进有带宽限制的cgroup里即使优先级再高执行超过规定额度后会被强制挂起。排查命令是cat /sys/fs/cgroup/cpu/应用目录/cpu.max如果仓位限制太紧给实时任务单独建一个无限制的cgroup。5.3 一套可直接复用的配置清单最后整理一份我在这几个项目里反复使用、实测有效的核心配置清单。内核启动参数部分isolcpus4-7 nohz_full4-7 rcu_nocbs4-7 skew_tick1应用内部实时线程初始化代码的要点// 伪代码展示核心设置流程 struct sched_param param {.sched_priority 80}; pthread_attr_t attr; cpu_set_t set; CPU_ZERO(set); CPU_SET(4, set); // 绑定到核4 pthread_attr_init(attr); pthread_attr_setaffinity_np(attr, sizeof(set), set); pthread_attr_setschedpolicy(attr, SCHED_FIFO); pthread_attr_setschedparam(attr, param); pthread_attr_setinheritsched(attr, PTHREAD_EXPLICIT_SCHED); pthread_create(thread, attr, rt_loop, NULL);注意第4步必须用PTHREAD_EXPLICIT_SCHED显式指定线程继承调度属性否则线程会沿用调用者的调度策略设置整个就白做了。中断亲和性部分以某中断为例# 查看IRQ号和当前分布 cat /proc/interrupts # 将88号中断固定到核2 echo 2 /proc/irq/88/smp_affinity_list # 对网络队列的IRQ同样处理 for i in $(ls /proc/irq/ | grep -E ^[0-9]$); do echo 0-3 /proc/irq/$i/smp_affinity_list 2/dev/null done上面的循环把系统里未显式设置的大部分中断都驱逐到核0-核3执行后重新检查/proc/interrupts确保隔离核上的中断计数基本不增长。在飞腾平台做完这一套多核心调度优化后我最大的体会是实时性优化不是“调一个参数、测一个数据”就完事而是一个持续逼近的过程目标是系统性的——从内核配置、中断路由、线程亲和性、优先级设计、内存分配到最终的追踪验证每一步都要扣得足够细。每次调试时把trace-cmd的数据留下来做对比时间长了就会形成一套自己的优化经验库。最后再啰嗦一句配置技巧虽然可以复用但每个项目的任务模型、核数资源、外设中断模型都不同最稳妥的方式是搭建一个隔离良好的实验环境随时能复现问题把每一个改动的前后数据都记录到位这样整套方案才真正可控、可持续优化。这套配置清单我跑过多次只要内核版本一致、外设拓扑相近基本可以直接抄作业但务必盯紧最后那一项验证环节。