ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux实时调度器深度解析:SCHED_FIFO与SCHED_RR混合使用实战

Linux实时调度器深度解析:SCHED_FIFO与SCHED_RR混合使用实战 做嵌入式 Linux 开发这些年调度器是我反复啃的一块硬骨头。尤其是当系统里同时跑着 SCHED_FIFO 和 SCHED_RR 两种实时策略时很多人会理所当然地以为“按优先级排就完了”但真到调优和排查问题的时候才发现事情远没这么简单。我最早被这个问题卡住是在做一块多传感器机器人控制板。采集线程要严格按时钟周期来控制线程不能容忍被打断日志线程偶尔跑一跑但不能饿死别的任务——这就逼着我在同一个系统里混用了两种实时调度策略。折腾一段时间后踩了不少坑也把内核调度器源码翻了几遍今天把这一套东西彻底理清楚。先说结论调度器每次只回答一个问题——下一个该轮到谁跑。至于 SCHED_FIFO 和 SCHED_RR 同时存在时怎么调核心有三层逻辑第一层比优先级数值大的先跑第二层同优先级下看策略语义FIFO 站着不动RR 到时间就让第三层才是数据结构和负载均衡等实现细节。这篇文章就从这三层往下拆适合正在做实时 Linux 系统、嵌入式控制或者单纯想搞懂内核调度器的人。1. 先把两种策略的家底摸清楚1.1 SCHED_FIFO排队上车不挪位置SCHED_FIFO 的全称是 First In First Out先入先出。它的行为特别像老式食堂排队打饭先来的人排前面后来的排后面但是先来的人只要不停下来不阻塞、不主动让出后面的人就一直等着。你几乎可以把它理解成“死占着 CPU 不放的实时任务”——只要它处于就绪状态同优先级甚至更低优先级的任务都插不进来。FIFO 任务只有四种情况会放弃 CPU自己阻塞了比如等锁、等 I/O、调用 sleep自己主动让出比如调用 sched_yield来了一个更高优先级的任务把它抢占被系统迁移到别的 CPU 上。这里面特别容易忽略的是第二点sched_yield。很多新手以为 FIFO 任务一旦跑起来就“无敌”其实它主动 yield 之后会被放到同优先级队列的队尾给其他人让路。但在实际项目里特别不建议依赖 sched_yield 来协调多任务因为它会让任务切换变得极度不确定你很难判断让出去之后谁会上来这属于拿调度器的确定性开玩笑。1.2 SCHED_RR时间片到了往后站SCHED_RR 全称 Round Robin时间片轮转。它跟 FIFO 的唯一本质区别就是在同优先级下加了“时间片”这个概念。RR 任务跑满一个时间片后会被挪到同优先级队列的队尾然后队头的下一个 RR 任务接着跑。默认时间片大约在 100ms 这个量级具体数值由内核里的 DEF_TIMESLICE 换算而来跟 HZ 配置有关。RR 的适用场景特别明确一批任务优先级相同谁都不能长时间霸占 CPU大家轮流来。比如多个数据采集通道共享一个 CPU 核心每个通道都需要周期性地运行但单个通道的运行时长不会超过一个时间片这种用 RR 就很合理。它跟 FIFO 之间不是替代关系而是互补关系——FIFO 保证极端确定性RR 保证同优先级下的公平性。1.3 两种策略的语义对比维度SCHED_FIFOSCHED_RR调度语义队列先入先出运行中不主动让出同优先级时间片轮转时间片无默认约 100ms可配置同优先级任务交互队头阻塞前面不跑后面别想跑时间片到期轮换抢占条件更高优先级任务到达更高优先级任务到达或时间片耗尽典型场景强实时控制、高频采集、不可中断逻辑多路数据处理、周期任务组、日志批量上报2. 共存时调度器到底怎么排队2.1 第一准则永远是优先级SCHED_FIFO 和 SCHED_RR 在 Linux 里都属于实时调度类RT优先级范围是 1 到 99数字越大优先级越高。注意这个方向容易搞反跟 nice 值完全相反。当两种策略的任务同时就绪调度器先做的事只有一件找当前所有就绪实时任务里优先级最高的那一个。优先级比较是不区分策略的。一个 SCHED_FIFO 优先级 1 的任务在一个 SCHED_RR 优先级 80 的任务面前没有任何翻身的机会。策略只决定“同优先级下怎么轮”优先级决定“谁先上桌”。这个概念有点像热词里那个“越急越优先”的排产逻辑紧急程度优先级先决胜负同紧急程度再谈先后策略。这里有个直觉误区要纠正“RR 能抢占 FIFO”是错的。RR 只是时间片到了把自己挪到队尾它并不能把正在运行的 FIFO 任务挤下去。真正能抢占 FIFO 的只有更高优先级任务而不是另一个同优先级的 RR 任务。2.2 同优先级下FIFO 和 RR 的奇怪关系当 SCHED_FIFO 和 SCHED_RR 任务优先级完全相同的时候调度秩序按以下规则走如果就绪队列里排在最前面的是一个 FIFO 任务它进入运行态后就不再挪位置。后面排着的 RR 任务虽然时间片会走动但只要 FIFO 不让出 CPURR 任务实际上根本没有机会运行。如果排在前面的是 RR 任务时间片耗尽后它会让位同优先级的 FIFO 任务和 RR 任务轮流按队列顺序走。FIFO 任务一旦运行它不会因为“RR 的时间片到了”就让出 CPU也基本不会感知到 RR 的存在。所以实际项目里有个反直觉的结果同优先级下一个不阻塞的 FIFO 任务可以把同优先级所有 RR 任务全部饿死。我见过线上系统出过这种事故底层采集线程用 FIFO上层处理线程用 RR两者都是优先级 80。结果采集线程因为一个 bug 卡在忙等循环里CPU 被它独占所有 RR 线程全部停了。查了半天才反应过来不是采集线程优先级过高而是“FIFO 在同优先级下的排他性”在作怪。2.3 运行队列的数据结构bitmap 加速查找Linux 的实时调度器为了做到快速选择下一个任务没有简单粗暴地线性扫描所有任务而是维护了一个按优先级分桶的运行队列。每个优先级对应一条链表FIFO 和 RR 任务都挂在对应链表中。另外维护一个优先级 bitmap哪个优先级上有任务就把相应 bit 置 1。调度器选择下一个任务时先通过 bitmap 的位运算找到最高非空优先级然后直接拿到该优先级的链表头取链表第一个任务运行。这个操作的时间复杂度是 O(1)跟系统里有多少任务完全无关。这也是为什么实时调度器敢在硬实时系统里用它——查找成本可预测不会随着负载增加而劣化。当你同时使用 FIFO 和 RR 时它们在数据结构层面没有区别都一样挂在rt_prio_array的链表里。区别体现在入队和出队的策略上FIFO 任务的出队只发生在任务状态变化时RR 任务的出队多了一个“时间片耗尽主动回到队尾”的动作。3. 内核源码路径怎么走一遍3.1 入队和出队时发生了什么当一个实时任务变成可运行状态内核会调用enqueue_task_rt。这个函数的核心逻辑并不复杂根据任务的优先级找到对应的活动链表把任务节点挂到链表尾部把优先级 bitmap 的对应位置 1。出队时对应dequeue_task_rt把任务从链表里摘掉如果链表空了就清掉 bitmap 位。这里有一个容易忽略的细节因为 FIFO 和 RR 任务在入队时都是挂到队尾所以从链表顺序来看同一个优先级的任务遵循的是“谁先就绪谁在前”的顺序。FIFO 任务会严格遵守这个顺序RR 任务在时间片耗尽后会人为挪到队尾。3.2 pick_next_task_rt怎么选下一个“选下一个”是整个调度决策的核心。调度器调用pick_next_task_rt时会做这么几件事检查当前 CPU 的运行队列里有没有实时任务没有就返回空调度器转向普通任务CFS有的话用 bitmap 找到最高优先级的链表返回链表头的任务作为下一个运行对象。如果选中的是一个 RR 任务并且它的时间片早就耗尽了比如刚从别的 CPU 迁移过来调度器会先给它重新分配时间片再运行。这就是 RR 任务在跨核迁移后仍然能保持轮转节奏的原因。FIFO 任务在这个函数里没有时间片概念拿到的任务直接运行。所以从pick_next_rt这个层面看FIFO 和 RR 的唯一分歧点就在于时间片处理逻辑其余完全一致。3.3 check_preempt_curr_rt什么情况允许抢占任务被唤醒后调度器不会立刻切换而是调用check_preempt_curr_rt判断是否需要打断当前任务。规则如下新任务的优先级高于当前实时任务则触发抢占优先级相同如果当前运行的是 FIFO 任务新任务不能抢占优先级相同当前运行的是 RR 任务新任务唤醒后也不立刻打断而是排到对应队列尾部等当前 RR 时间片耗尽后再自然切换。这个函数天然保证了 FIFO 的稳定性你不可能因为“又一个同优先级任务来了”而打断一个正在运行的 FIFO 任务。这也意味着 FIFO 任务一旦开始运行它的执行时间只受更高优先级任务和自身阻塞行为影响不受同优先级其他任务的打扰——这正是确定性所在。3.4 多核下的 push/pull 负载均衡单核下的逻辑很清晰但到了多核系统RT 调度器还有一套自己的负载均衡机制叫做 push/pull。当一个 CPU 的运行队列里实时任务数量超过 1 个而其他 CPU 上有空闲实时队列时调度器会尝试“push”把多余的任务推到空闲 CPU 上当一个 CPU 的实时队列为空而其他 CPU 有两个及以上实时任务时调度器会尝试“pull”从别的 CPU 拉一个任务过来运行。这个机制对混合使用 FIFO 和 RR 的系统影响很大。比如一个 FIFO 任务只允许在 CPU0 上运行通过 affinity 限制但它醒了之后被 push 到 CPU1有可能因为 affinity 不匹配导致迁移失败甚至出现任务反复在核间乒乓的现象。所以我一直建议实时任务数量少的时候直接用sched_setaffinity把关键 FIFO 任务绑死到一个核心彻底关掉 push/pull 对它的干扰。多核场景下还可以类比一下大小核调度体验普通桌面系统里 Windows 会倾向于把前台任务放到性能核上Linux RT 任务也一样需要你显式把最高优先级的 FIFO 任务绑定到性能核心否则调度器只看 CPU 空闲度并不知道哪个核跑得更快。4. 实战怎么设计一个 FIFO RR 混合实时系统4.1 任务策略分配表在混合调度系统里第一步不是写代码而是画一张任务表。我一般把任务分成三类硬实时、软实时、非实时。硬实时任务用 SCHED_FIFO 优先高优先级软实时任务用 SCHED_RR 或者 SCHED_FIFO 较低优先级非实时任务保持默认的 SCHED_OTHERCFS。一个参考模型是这样的任务角色调度策略优先级优先级数值说明高频采集SCHED_FIFO高88周期性硬实时绝不允许延迟核心控制SCHED_FIFO高85不可被同优先级打断数据融合SCHED_RR中70多个通道轮流处理日志上传SCHED_RR中65网络/磁盘阻塞时自动让出监控统计SCHED_OTHER低-非实时兜底执行这个表的分配逻辑是越紧急越不吃“排队亏”的任务用 FIFO允许互相轮转、缺一不可的任务用 RR。同优先级冲突被刻意避开——所有 FIFO 任务优先级都比 RR 任务高这样至少不会出现“FIFO 饿死 RR”的经典事故。如果你确实需要同优先级混用那就要接受 FIFO 可能长时间占用 CPU 这个事实并做好看门狗。4.2 设置调度策略的代码示例在 Linux 下设置线程的调度策略最常见的是用pthread_setschedparam也可以直接对进程调用sched_setscheduler。下面是一个把线程设为 SCHED_FIFO、优先级 85 的示例#include pthread.h #include sched.h #include stdio.h static void* task_fifo(void* arg) { while (1) { // 硬实时任务循环 usleep(1000); } return NULL; } int main(void) { pthread_t th; struct sched_param param { .sched_priority 85 }; pthread_attr_t attr; pthread_attr_init(attr); pthread_attr_setinheritsched(attr, PTHREAD_EXPLICIT_SCHED); pthread_attr_setschedpolicy(attr, SCHED_FIFO); pthread_attr_setschedparam(attr, param); if (pthread_create(th, attr, task_fifo, NULL) ! 0) { perror(pthread_create); return -1; } pthread_join(th, NULL); return 0; }关键细节有两个pthread_attr_setinheritsched必须设为PTHREAD_EXPLICIT_SCHED否则线程会继承创建者的调度策略属性里的设置不生效设置 SCHED_FIFO 或 SCHED_RR 需要 root 权限或者具备CAP_SYS_NICE能力。否则调用会返回 EPERM。如果要在运行时动态调整可以用sched_setscheduler这适合一些初始化阶段后才确定优先级的场景。4.3 别忘了实时预算和优先级反转这个坑我在最初做混合调度时踩得最痛。FIFO 任务一旦死循环整个系统几乎无法响应普通任务连一滴 CPU 都分不到。Linux 为此提供了实时预算机制通过/proc/sys/kernel/sched_rt_period_us和/proc/sys/kernel/sched_rt_runtime_us控制实时任务在单位周期内最多占用的时间。默认配置是周期 1s、预算 0.95s也就是说实时任务每 1 秒最多跑 950ms剩下 50ms 强行分给非实时任务。对硬实时系统来说这个机制有时候反而是负担。如果预算耗尽FIFO 任务会被推迟实时性被破坏。所以做真正硬实时的设备经常会把sched_rt_runtime_us设为-1关闭限制。但这意味着你必须对任务的执行时间负责一旦某个实时任务跑飞系统直接卡死。稳妥做法还是保留预算并加一个专门的监控任务检查预算剩余量真正做到“虽然给了你有力的武器但也给你套上缰绳”。优先级反转在混合调度下也特别常见。低优先级的 RR 任务拿着一个锁高优先级的 FIFO 任务在等这把锁就会导致高优先级任务被低优先级任务反向阻塞。解决方式是使用支持优先级继承的rtmutex在 pthread 层面对应的是设置PTHREAD_PRIO_INHERIT属性的互斥锁。这样低优先级任务临时继承高优先级任务的优先级尽快释放锁把“倒挂”的时间压缩到最小。4.4 验证cyclictest 和 /proc/sched_debug配置写完了凭什么说调度是符合预期的我会用两样东西验证。第一是cyclictest实时性测量工具能统计调度延迟的抖动情况。对不同优先级和策略组合分别跑一遍记录最大延迟。如果 FIFO 任务的最高延迟波动超过预期先怀疑是不是同一 CPU 上有其他中断或 RT 任务在干扰。第二是/proc/sched_debug这个文件会输出每个 CPU 的运行队列详情包括实时任务的链表顺序、优先级、时间片剩余等。排查“为什么我的 RR 任务没轮到”时直接看对应优先级的链表节点顺序一眼就能判断是谁占着位置不让。4.5 实测记录与经验我曾经在一台四核设备上测试一套方案两个 FIFO 任务优先级 90 和 80两个 RR 任务优先级 80。表面上看优先级 90 的 FIFO 任务和优先级 80 的 RR 任务冲突不大但问题出在第二个 FIFO 任务优先级 80和两个 RR 任务优先级 80之间。结果多次测试中RR 任务的总运行时间波动极大有时几乎吃不到 CPU原因就是那个优先级 80 的 FIFO 任务偶尔会进入一个忙等分支长时间霸占核心把同优先级的 RR 任务全部压住。后来我把那个 FIFO 任务的优先级提到 85让它的“紧迫程度”跟 RR 任务拉开一个档位同时把它绑到专用核心冲突立刻消失。从这次之后我的设计规则变成了绝不轻易让 FIFO 和 RR 同级共存除非有完备的阻塞兜底机制。5. 常见问题与排查技巧实录5.1 问题速查表现象可能原因快速定位手段解决方案RR 任务完全没执行同优先级 FIFO 忙等占住 CPU看 /proc/sched_debug 中对应优先级链表顺序调整 FIFO 优先级或加入阻塞点FIFO 任务周期性卡顿RT 预算耗尽被限流查看 /proc/sys/kernel/sched_rt_runtime_us 是否还有余量增大预算或关闭限流系统完全无法响应FIFO 任务死循环串口/看门狗确认 CPU 占用用 watch 监控任务运行时间设置 RT 预算多核下任务频繁迁移push/pull 负载均衡误操作查看 /proc/sched_debug 中任务所在 CPU用 sched_setaffinity 绑核高优先级任务等锁被拖死优先级反转观察持有锁的任务优先级是否临时提升启用 PTHREAD_PRIO_INHERIT普通任务延迟飙升RT 任务占用过高用 cyclictest 和 top 观察 RT 占用率调低 RT 预算或减少 RT 任务5.2 排查思路和工具组合遇到 RT 调度问题我个人的排查顺序是固定的先看top的实时线程优先级排序确认哪些任务在跑、优先级是不是按预期设置的。然后用/proc/sched_debug检查每个 CPU 的运行队列结构这一步能看出任务是不是被放到了错误的队列里。最后用trace-cmd录制调度事件分析具体的唤醒到运行时间差这种事件级数据能帮你判断延迟的引爆点是优先级反转、时间片耗尽还是跨核迁移。还有一个很容易被忽略的技巧实时任务启动之后去/proc/pid/task/tid/sched看policy和rt_priority确认生效的参数确实是你想设定的值。我遇到过线程属性设置被忽略、实际跑在 SCHED_OTHER 上的事故就是因为忘了setinheritsched。另外当你一开 trace 系统卡死时优先怀疑这个trace 本身加大了中断和调度开销实时任务又占着 CPU 不撒手整个系统处于瘫痪。遇到这种情况先用串口登录进去把所有 RT 任务优先级降下来再谈追踪。5.3 我踩过几次坑之后的总结做混合实时调度技术上最难的不是写代码而是预判“极端情况”。FIFO 的极端情况是死循环RR 的极端情况是同优先级下的饥饿二者叠加就会变成“一个 bug 拖垮整条实时链路”。所以我现在做系统设计永远多留一层给关键 FIFO 任务加超时看门狗给同优先级的 RR 任务留一个监控线程RIO 预算默认打开只有在性能确实不够时才考虑关闭。调度器的选择只是一切的起点真正的稳定性要靠整个系统的冗余设计来保证。最后再分享一个小技巧如果你在调试时想让某些 FIFO 任务临时让出 CPU不要直接改代码重新编译用chrt -p -r 70 pid把它的调度策略临时改成 SCHED_RR这比改代码快得多而且能立刻验证“是不是这个任务在霸占核”的假设。等定位清楚了再改回正式配置效率高很多。
返回列表