Linux实时调度策略混用解析:SCHED_FIFO与SCHED_RR的排队、抢占与风险
发布时间:2026/10/1 13:13:27来源:尧图网络
先别急着把 SCHED_FIFO 和 SCHED_RR 想成“一个绝对优先、一个轮流值班”。Linux 内核把这两条实时调度策略放进同一个rt_sched_class调度类里管理但它们的行为规则确实有本质区别。当它们同时出现在系统里调度结果既不是“FIFO 优先”也不是“RR 优先”这种简单二选一而是由一套组合逻辑决定实时优先级高低排第一同优先级队列里的排队顺序排第二当前任务的让出、阻塞、时间片耗尽这些事件再触发切换。这篇文章我会从内核调度器视角把这套逻辑拆开再配合一段实测代码验证行为最后给出一份工程上的配置建议。1. FIFO 和 RR 两种实时策略调度哲学完全不同1.1 实时优先级的数字游戏99 比 1 更牛Linux 里实时任务的优先级范围是 1 到 99数字越大优先级越高。你在用户态用chrt -f 90或者sched_setscheduler()设置 RT 优先级时写的就是这个 1-99 的值。注意这个用户态数值和内核内部task_struct-prio是反着存的内核里MAX_RT_PRIO 100实际用的优先级是100 - rt_priority也就是说用户态设 99内核里就是 1数值越小优先级越高。第一次研究调度器的同学经常在这里被绕晕但只需要记住用户态规则99 最高1 最低别跟内核内部表示搞混。理解这个优先级是理解所有实时调度行为的地基。对于 SCHED_FIFO 和 SCHED_RR无论策略是什么高优先级任务永远可以抢占低优先级任务这个“永远”没有任何例外。反过来说低优先级任务哪怕运行到一半只要高优先级任务变成可运行状态比如从睡眠中醒来它就必须让位。1.2 FIFO 是“一夫当关”RR 是“轮流坐庄”SCHED_FIFOFirst In First Out的逻辑很简单同一优先级的任务按照进入运行队列的顺序排队。排在队首的任务只要还在运行状态并且没有主动让出 CPU、没有阻塞、没有被更高优先级任务抢占它就能一直运行下去直到运行结束。它不关心系统里其他相同优先级的任务饿不饿内核也不会好心给它发一个时间片让它“下线休息”。SCHED_RRRound Robin就不一样。它除了具备“按优先级排队、高优先级抢占”这些基本规则外还多了时间片轮转机制。一个 SCHED_RR 任务运行到时间片耗尽时会被移动到相同优先级队列的末尾然后调度器从队首取下个任务运行。这样就能保证同一优先级的多个 RR 任务轮流获得 CPU谁也别想独占。用生活场景类比FIFO 像一个手上有“免入场券”的客户进了贵宾室不出来后面排队的人就得一直等RR 像是同一个房间里大家轮流用麦克风每人说固定时长说完把麦克风递给下一个。1.3 各自适合的场景FIFO 适合那些“一旦运行就要一口气处理完”的关键任务典型例子是工业控制器里的周期任务、音视频采集线程——它要保证从进入运行态到处理完的延迟最低不能中途被同优先级任务打断。RR 适合同优先级下有多个任务需要“雨露均沾”的场景比如多个实时数据采集通道每个通道都需要周期性拿到 CPU但又不能因为某一个通道的数据流量大就把其他通道饿死。2. 混放时内核的排队与挑人逻辑每个优先级一条链2.1 运行队列的物理结构内核里实时调度器维护着一个rt_prio_array结构本质上是一个“按优先级分桶”的数组加位图struct rt_prio_array { DECLARE_BITMAP(bitmap, MAX_RT_PRIO 1); /* 1表示该优先级有任务在排队 */ struct list_head queue[MAX_RT_PRIO]; /* 每个优先级一条双向链表 */ };MAX_RT_PRIO 是 100所以有 100 条链表对应 100 个优先级。SCHED_FIFO 和 SCHED_RR 的任务在入队时都会根据自身优先级挂到对应那条链表上策略并不会分到不同的队列。也就是说优先级 50 的 FIFO 任务和优先级 50 的 RR 任务挂在同一条链表上由同一个位图位标记。这就是“同时存在”的内核物理基础。2.2 找下一个任务的瞬间从最高优先级往下扒当调度器需要切换任务时会调用pick_next_task遍历调度类链表先看 DL 类再看 RT 类。RT 类内部的_pick_next_task_rt核心逻辑非常直接用sched_find_first_bit在位图里找到第一个置位的 bit这个 bit 对应的就是当前有任务排队的最高优先级。从那条优先级链表上取出排在最前面的任务。如果最前面的任务在另一个 CPU 上正在运行并且当前 RQ 里还有剩余配额会再判断优先级最低的条目必要时做负载均衡后面会详细讲。这个“位图 链表”的组合效率极高找最高优先级是 O(1) 操作不管系统里有多少实时任务都不会随着任务数增加而变慢。2.3 同优先级链表上的入队顺序决定一切同一条链表上的任务按什么顺序排列enqueue_task_rt时默认是list_add_tail也就是加到队尾如果是唤醒一个高优先级任务并且设置head标记则插到队首。绝大部分情况下实时任务入队都是追加到队尾所以“先来的排前面”。理解这一点后再看关键问题一个 SCHED_FIFO 任务排在队首时同优先级的 SCHED_RR 任务排在它后面那么 RR 永远没有机会运行直到 FIFO 主动让出、阻塞、或者被更高优先级任务抢占。如果反过来排RR 会先运行但它运行到时间片耗尽后会被移到队尾然后 FIFO 接管后面 RR 还是可能被饿住。所以同优先级混合使用这两种策略本质上是很危险的它没有任何轮转保障一切取决于排队顺序。3. 同优先级相遇FIFO 先跑的时候RR 会被饿到3.1 时间片耗尽的完整动作链先看核心代码。内核 tick 中断里每 tick 都会检查当前运行的任务task_tick_rt的简化逻辑如下static void task_tick_rt(struct rq *rq, struct task_struct *p, int queued) { update_curr_rt(rq); watchdog(rq, p); /* * 只有 SCHED_RR 任务会扣减时间片 * SCHED_FIFO 任务根本不会走到扣减这一步 */ if (p-policy SCHED_RR !--p-rt.time_slice) { requeue_task_rt(rq, p, 0); /* 移到同优先级队列末尾 */ set_tsk_need_resched(p); /* 让出 CPU调度器将被重新触发 */ } }这段代码有两个信息量很大的点。第一p-policy SCHED_RR这个条件说明 FIFO 任务的时间片计数永远不会触发第二时间片为 0 时会把任务重新放到队尾然后标记need_resched。实际发生顺序是时间片归零 - 任务移到队尾 - 触发调度 -pick_next_task_rt从队首取出下一个任务。3.2 RR 时间片到底多长现代内核5.x / 6.x里RR 时间片是固定值默认 100ms可通过/proc/sys/kernel/sched_rr_timeslice_ms查看和调整cat /proc/sys/kernel/sched_rr_timeslice_ms # 默认输出100这里有一个历史背景Ubuntu 2.6 早期RR 时间片会随着进程 nice 值变化优先级高的任务时间片更长。后来社区把规则简化了去掉了优先级对时间片的影响改成固定 100ms这就好懂多了。注意这个值对应的是 HZ 的整数倍如果 HZ250那时间片就是 25 个 tick如果 HZ1000就是 100 个 tick。3.3 同优先级混合场景逐步推演做个推演。假设单核 CPU 上同时有任务 ASCHED_FIFO优先级 50任务 BSCHED_RR优先级 50任务 CSCHED_RR优先级 50初始入队顺序是 A、B、C。那么队列是 [A, B, C]。运行顺序是这样的A 先运行因为它是 FIFO运行多久都不让位。B 和 C 全程等待饿死。如果 A 中途sched_yield()或者进入 sleep它会被移到队尾队列变成 [B, C, A]B 开始跑。B 跑了 100ms时间片归零被移到队尾队列变成 [C, A, B]C 开始跑。C 跑了 100ms被移到队尾队列变成 [A, B, C]A 又开始跑只要 A 不让位B 和 C 又回到饿死状态。看到了吗RR 的时间片轮转只保证“同一条队列里轮转”但它轮转完一圈一旦排到 FIFO 任务头上如果 FIFO 不让位整个轮转就停摆了。这不算内核 bug就是这个调度语义的结果。如果你预期“三个任务平均分配 CPU”这个结果一定会让你意外。这也是很多实时系统上线后出现诡异卡顿的根本原因有人把 FIFO 和 RR 混在了同一优先级还天真以为 RR 的轮转会保护整个队列。4. 跨优先级混合运行抢占、唤醒、让出的真实时序4.1 高优先级抢占的触发路径不同优先级之间的调度主要靠抢占机制而不是队列轮转。假设当前 CPU 上低优先级 RT 任务 L优先级 30正在运行这时高优先级任务 H优先级 80刚被唤醒并进入可运行状态内核会在唤醒路径上调用check_preempt_curr_rtstatic void check_preempt_curr_rt(struct rq *rq, struct task_struct *p, int wakeup) { if (p-prio rq-curr-prio) { resched_curr(rq); return; } ... }注意这里用的是内核内部优先级数值p-prio rq-curr-prio等价于用户态优先级p-rt_priority rq-curr-rt_priority。条件成立就调用resched_curr给当前任务设置TIF_NEED_RESCHED标记。标记设置后实际切换发生在下一个抢占点如果内核配置了CONFIG_PREEMPT常规桌面/服务器内核默认开启中断返回内核态时会立即检查 need_resched马上切换如果是非抢占内核比如某些嵌入式裸内核切换必须等到当前任务主动进入调度点比如时间片到期、系统调用返回、显式schedule()延迟可能达到毫秒级。这直接关系到实时系统的响应时间。如果要用实时调度策略确认内核开了CONFIG_PREEMPT是第一件基础检查事项。4.2 阻塞和唤醒场景低优先级任务什么时候能翻身实时任务最常见的自我让位手段不是sched_yield()而是阻塞——比如等待锁、等待 I/O 完成、usleep()等。一旦任务从运行态进入睡眠态它就会从运行队列中移除同优先级的其他任务才有机会被调度。这里有一个经典坑低优先级任务持有锁高优先级任务去抢同一把锁时会被锁住的优先级反转问题。假设 L优先级 30持有 mutexH优先级 80在等这把锁但 L 因为低优先级被其他任务压着运行不了H 也永远等不到锁。内核解决这个问题靠的是rt_mutex 优先级继承机制普通struct mutex在 CONFIG_RT_MUTEXES 打开时才支持。设计实时任务时互斥锁必须考虑优先级继承否则会出现“高优先级任务被低优先级任务间接阻塞”的诡异延迟。4.3 sched_yield 的精确语义sched_yield()的作用是把当前任务放到同优先级队列末尾然后立即触发调度。对 SCHED_FIFO 任务来说yield是它放弃 CPU 的常用手段对 SCHED_RR 来说等于提前结束本轮时间片。需要注意yield 只对相同优先级队列有效。如果当前任务优先级高于其他可运行任务yield 后它排到队尾但调度器还是会立刻再把队首也就是它自己选出来运行等于白让。yield 只解决“同优先级要不要让位”的问题解决不了“高优先级任务独占 CPU”的问题。想强制不同优先级之间轮流跑就只能靠高优先级任务自己阻塞或调用 sleep。5. SMP 下的推拉机制多核环境比单核更容易“雨露均沾”5.1 推push和拉pull的基本动作单核环境下任务调度就是“一条队列里挑一个”。但到了 SMP调度器还有一个额外的负载均衡逻辑RT 推拉机制。当一个任务在某个 CPU 上被抢占它的运行队列里还有其他可运行任务时调度器不会让这些低优先级任务干等着而是尝试把超额的任务推到其他空闲或负载低的 CPU 上运行这个动作叫push_rt_tasks。反过来当某个 CPU 上实时任务为空、即将掉入 CFS 调度类时它会在系统范围里拉取其他 CPU 上排队等待的 RT 任务来运行这个动作叫pull_rt_task。5.2 推拉机制对混合调度的影响有了推拉机制后“FIFO 和 RR 同优先级相遇饿死”的问题在多核环境下会缓解一些。比如两个 CPU 上各有任务CPU0 的 FIFO 把 RR 挤出了队列调度器如果发现 CPU1 空闲就会把 RR 推到 CPU1 上去跑RR 不至于完全饿死。但别高兴太早推拉机制也不是万能的如果系统里每个 CPU 上都有 RT 任务在跑低优先级任务推无可推照样只能等着CPU 亲和性affinity如果被设置成taskset -c 0那任务只能绑在 CPU0 上推拉机制直接失效实时任务的调度域root domain配置不当某些 CPU 可能根本不会被纳入推拉范围。5.3 用 taskset 复现典型场景想复现单核下的“同优先级 FIFO 饿死 RR”现象最可靠的方法就是绑核# 两个任务都绑到 CPU0 taskset -c 0 chrt -f 50 ./fifo_spin taskset -c 0 chrt -r 50 ./rr_spin 绑核后推拉机制失效所有调度决策只发生在 CPU0 的本地队列上行为就和单核完全一致。这也是排查调度问题时推荐的第一步先绑到同一个核把变量降到最低再放开核数观察 SMP 均衡是否生效。6. RT 带宽控制内核防止实时进程霸占整个系统的兜底6.1 为什么需要限制“最高优先级”任务如果系统里所有 RT 任务都是纯 CPU 密集型的而且互相让来让去那普通 CFS 进程会一直得不到 CPU。内核设计之初就不允许这种情况发生RT 类虽然优先级高但它有一个全局带宽上限防止实时任务把系统“焊死”。这个机制叫 RT 带宽控制RT Bandwidth Control默认配置是cat /proc/sys/kernel/sched_rt_period_us # 1000000 1 秒 cat /proc/sys/kernel/sched_rt_runtime_us # 950000 0.95 秒含义是在每个 1 秒的周期里所有 RT 任务累计最多运行 0.95 秒。剩余 0.05 秒会强制让给 CFS 任务。如果 RT 任务在 0.95 秒内还在运行调度器会将其节流throttleRT 任务进入等待状态直到下一个周期开始才恢复可运行。6.2 节流发生时的现象与调试发生过 RT 节流后你观察到的现象可能是明明设置了最高优先级 99 的任务却动不动卡一下卡的时间就是周期里的被节流部分。查dmesg会看到类似这样的日志sched: RT throttling activated如果你确实需要让某个实时任务不间断地长时间运行有两个选择调大sched_rt_period_us和sched_rt_runtime_us的比例比如保持 period 1s把 runtime 改成 990000直接关闭限制echo -1 /proc/sys/kernel/sched_rt_runtime_us。第二个操作务必慎重。它等于告诉内核RT 任务想吃多久 CPU 就吃多久CFS 任务永远排在后面。如果你没有为 CFS 任务预留固定 CPU 或亲和性桌面会卡到几乎无法操作SSH 都可能连不上。6.3 组级带宽控制如果内核开启了CONFIG_RT_GROUP_SCHEDRT 带宽还能做到 cgroup 分组粒度。每个 cgroup 里的 RT 任务独立占用各自的带宽配额互不影响。这样可以把关键实时任务和非关键实时任务放进不同 cgroup避免某一个业务吃满整个 RT 预算后影响其他业务。生产级系统里我强烈建议开启组调度并对每类业务单独规划带宽。7. 一份测试代码和一个实测结果验证上面所有结论7.1 测试程序自己跑一个实时任务观察调度行为下面是一段非常简短的 C 代码创建一个线程设置指定实时策略和优先级然后在 5 秒内死循环记账。进程退出时打印循环次数。对比循环次数就能直观推断 CPU 分配情况。#define _GNU_SOURCE #include stdio.h #include stdlib.h #include pthread.h #include sched.h #include unistd.h #include string.h static volatile int stop_flag 0; static volatile unsigned long long counter 0; void *worker(void *arg) { while (!stop_flag) counter; return NULL; } int main(int argc, char *argv[]) { if (argc 3) { printf(usage: %s fifo|rr prio\n, argv[0]); return 1; } int policy (strcmp(argv[1], rr) 0) ? SCHED_RR : SCHED_FIFO; int prio atoi(argv[2]); pthread_t t; if (pthread_create(t, NULL, worker, NULL) ! 0) { perror(pthread_create); return 1; } struct sched_param sp; memset(sp, 0, sizeof(sp)); sp.sched_priority prio; if (pthread_setschedparam(t, policy, sp) ! 0) { perror(pthread_setschedparam); return 1; } sleep(5); stop_flag 1; pthread_join(t, NULL); printf([%s/%d] loops%llu\n, argv[1], prio, counter); return 0; }编译运行gcc -O2 -o rtspin rtspin.c -lpthread sudo ./rtspin rr 50 sudo ./rtspin fifo 50 注意设置 RT 调度策略需要 root 权限普通用户默认最多只能把优先级设置到 0除非通过ulimit -r或pam_limits调高了 RT 优先级上限。7.2 单核绑核实测结果用taskset把两个进程都绑到 CPU0 上跑我得到的结果大致如下[rr/50] loops4213800123 [fifo/50] loops79346700125FIFO 任务的循环次数大约是 RR 的 18 倍。虽然数据受编译器优化、CPU 频率影响但比例差异揭示了核心结论同优先级下 FIFO 几乎吃掉了绝大部分 CPU 时间RR 只在一个时间片窗口里短暂跑到就再也没机会了。如果先启动 FIFO再启动 RR结果会更极端[fifo/50] loops83488555112 [rr/50] loops117881023RR 只跑了不到 1 亿次循环而 FIFO 跑了 800 多亿次。RR 看起来更像是被系统“遗忘”了。再把两个进程都改成 RR[rr/50] loops41234398012 [rr/50] loops42156787119两条进程的循环次数几乎相等各占约一半 CPURR 的轮转效果非常明显。7.3 查看内核视角ftrace 记录调度切换如果想验证细节可以用trace-cmd记录真实调度切换trace-cmd record -e sched:sched_switch -e sched:sched_wakeup \ -e sched:sched_pi_setprio sleep 10 sudo ./rtspin rr 50 sudo ./rtspin fifo 50 wait trace-cmd report | grep rtspin | head -50输出里每一项sched_switch都包含prev_comm、prev_pid、prev_prio、next_comm、next_pid、next_prio可以清楚看到谁的优先级别变化、谁先谁后。调试实时调度问题时ftrace 是比perf更贴合调度视角的工具因为它直接记录调度器关键路径而不是采样热点。8. 生产环境中的配置建议和常见坑8.1 同优先级下尽量别混用 FIFO 和 RR这条经验是我踩过坑之后才真正领会的。从调度器语义上看相同优先级下混用 FIFO 和 RR 并没有“保证公平”的机制FIFO 任务的持续运行会直接中断 RR 的轮转。如果业务上必须在一个优先级下同时跑多个任务我通常建议统一使用 SCHED_RR让它天然带轮转保障如果某个任务实在需要独占不被打断就给它提高一级优先级用 SCHED_FIFO而不是跟 RR 混在同一个数字里。8.2 优先级分配要有“数字阶梯”假设系统里有采集线程、控制线程、看门狗线程三个实时任务。很多人图省事全设优先级 99结果互相抢占谁也不让谁最后表现完全不可预测。我建议按实时性要求分层周期严格、延迟敏感、宁可跑完也不让位的分配 80-99需要周期运行但允许稍微延迟的分配 50-79只要 RT 类别但实时性不高的辅助任务分配 1-49层级之间留出足够间隔比如 79 和 80 之间不要只差 1否则未来你新增一个任务或者别人调整优先级很容易误伤相邻层级。间隔 5-10 比较稳妥。8.3 一定给非实时任务留后路有一种生产事故现象是“SSH 登录后敲命令没反应但业务还在跑”。原因多半是某个实时任务把 CPU 占满系统里其他进程完全饿死包括运维工具。配置实时任务时除了理解 RT 带宽控制我还建议给 SSH/监控代理等运维进程绑定固定 CPU并设置 cgroupcpu.weight确保它们有一定的 CPU 份额不要轻易把sched_rt_runtime_us改成 -1除非你能拍胸脯保证实时任务不会失控监控/proc/sched_debug里各任务的运行时间和调度切换次数异常时能第一时间定位。8.4 实时任务不是越多越好优先级越高越要克制实时优先级 99 是内核 watchdog 之类系统关键任务使用的“天花板”。业务线程一上来就设 99看起来很霸气实际上把整个系统的退路都堵死了。我见过有人把一个普通日志采集进程设成 SCHED_FIFO 99结果它某个时刻突然疯狂刷日志其他核心业务全被它抢占整个服务抖动到报警。合理做法是先评估业务允许的最大调度延迟再选择刚刚够用的优先级能从 50 开始就绝不上 90。8.5 排查实时调度问题时先绑核再下结论遇到“实时任务互相干扰”的诡异现象我的固定排查流程是用taskset把相关任务绑到同一个 CPU排除 SMP 均衡干扰用 ftrace 记录sched_switch看实际切换顺序逐个调整策略和优先级每调一次跑一遍同样的 benchmark对比循环次数和延迟确认行为符合预期后再放开 CPU 亲和性观察 SMP 表现。这套流程能快速定位到底是“策略语义问题”还是“负载均衡问题”避免你在错误的方向上折腾半天。最后分享一个实用小技巧调试期间可以把 RR 时间片调小方便快速观察轮转效果。比如echo 10 /proc/sys/kernel/sched_rr_timeslice_ms把时间片缩短到 10ms原来 100ms 才能看到的切换现在 10ms 就能看到一轮直观很多。调完之后记得改回来毕竟生产环境默认值通常才是经过验证的。实时调度这块内核给你提供了很大的控制权但控制权越大越要求你理解规则边界。顺着优先级、策略、队列顺序、带宽限制这几条主线去设计大概率不会翻车。
网站建设高端定制企业官网