SCHED_FIFO与SCHED_RR混合调度:优先级与时间片机制深度解析
刚接触 Linux 实时调度的人几乎都会在同一个地方犯迷糊手头有两个实时任务一个用SCHED_FIFO一个用SCHED_RR优先级也设置得差不多结果发现其中一个任务像“失踪”一样死活不跑。有人怀疑内核调度器有 bug有人以为是自己代码写崩了。其实真相没那么玄乎关键在于搞清楚这两种策略共存时调度器到底按什么顺序挑任务、什么时候让出 CPU、时间片又对谁起作用。这篇文章就把这件事彻底讲明白顺带附上可复现的实验代码帮你从“背概念”变成“看得懂调度结果”。SCHED_FIFO和SCHED_RR是 Linux 实时调度策略的双生子一个讲究“先来后到独占到底”一个讲究“同优先级下轮流上岗”。两者可以同时存在于同一个系统中甚至同一优先级下。它们之间的调度规则主要由三个要素决定优先级、运行队列中的位置、以及当前任务会不会主动让出 CPU。搞懂这三者的先后顺序你基本就掌握了实时调度的核心。这篇文章适合嵌入式开发、工控系统优化、服务端低延迟调优的工程师。我不打算给你堆内核源码而是用“决策链条 队列视角 可运行实验”的方式把混合调度讲清楚。你不需要提前熟悉内核调度器细节跟着思路走就行。1. 基本盘两种实时调度策略到底在争什么1.1 SCHED_FIFO 的“包场”逻辑SCHED_FIFOFirst In First Out是一种没有时间片概念的实时调度策略。同一个优先级下任务按进入运行队列的顺序排队一旦某个 FIFO 任务开始运行只要它不主动退出、不阻塞、不被更高优先级任务抢占它就可以一直霸占 CPU。你可以把它理解成饭店里的“包间客人”坐下了就不走服务员优先伺候直到他吃完任务退出或者睡着阻塞或主动让位sched_yield。这种特性决定了它适合什么场景对延迟极度敏感、需要行为确定性的任务。比如无人机飞控、电机闭环控制、工业总线主站这类任务宁可让其他任务少跑也不能让自己被中途打断。我曾经在一个机器人项目里把位置环控制线程配成SCHED_FIFO99效果立竿见影抖动从几百微秒降到了几十微秒。但代价是同一优先级下其他任务很容易被饿死。1.2 SCHED_RR 的“轮流坐庄”逻辑SCHED_RRRound Robin在概念上是给同一优先级的多个任务分配固定的 CPU 时间片用完一个时间片就换下一个如此循环。时间片长度可以通过sched_rr_get_interval()查询Linux 默认通常是 100ms。RR 适合同一个优先级下需要相对公平地共享 CPU 的场景典型如多路数据采集、多路通信协议栈轮询。RR 和 FIFO 的差别只体现在“同一优先级任务之间”。一旦牵涉到不同优先级两者遵循的规则完全一样高优先级永远先跑。换句话说RR 只是在 FIFO“排队”的基础上增加了“时间片到了就退到队尾”的机制。这个理解非常重要后面所有混合调度问题都可以从这个视角推导。1.3 实时任务面前普通任务先靠边站SCHED_FIFO和SCHED_RR都属于实时调度类。Linux 内核调度器在挑选任务时是有“阶级”顺序的大致如下stop 调度类 deadline 调度类 实时调度类RT 公平调度类CFS idle 调度类SCHED_FIFO和SCHED_RR都属于实时调度类。实时调度类里只要有可运行的任务公平调度类也就是普通SCHED_OTHER/SCHED_BATCH任务基本就没机会上 CPU——除非内核开启了实时带宽限制后面会讲。所以不要用“普通任务 nice 值调到 -20”这种思路去和实时任务抢 CPU它俩不在一个竞争维度。你可能听过“实时优先级范围 1~99数字越大优先级越高”。这句话要牢记因为它的逻辑和普通任务的 nice 值刚好相反。普通任务 nice 值越小优先级越高实时任务是越大越高搞反了会出大问题。2. 两种策略共存时调度器的完整决策链条2.1 决策顺序先比优先级再比队列位置最后才看策略当 CPU 需要挑下一个任务执行时调度器实际上在做这样一件事第一步在实时运行队列中找到当前优先级最高的那个队列。 第二步从该队列的队首取出一个任务开始运行。 第三步运行过程中 - 如果来了更高优先级的任务立刻抢占 - 如果当前任务是 SCHED_RR且时间片耗尽把它挪到队尾并重新开始计算时间片 - 如果当前任务是 SCHED_FIFO时间片耗尽也不会挪它它继续跑直到自己让出 CPU。这里的关键点是优先级比较永远在策略比较之前。换句话说调度器不会因为“它是 FIFO、它是 RR”而改变优先级判断它只是用一个统一优先级尺度排任务。策略只决定同优先级任务之间怎么轮流。举个例子你有一个SCHED_RR任务优先级设为 80另一个SCHED_FIFO任务优先级设为 50。即便 RR 任务正在运行只要 FIFO 50 就绪RR 80 也得靠边站。因为 80 是比 50 高的。反过来如果两个任务优先级相同比如都是 50那么规则就开始看队列位置和策略了。2.2 同一优先级下FIFO 会让 RR 饿死吗很多人实测时第一个困惑就是同一优先级FIFO 和 RR 混用RR 几乎不跑为什么我们来推演一个队列。假设优先级 50 的运行队列当前状态如下队首在左边[FIFO_A, RR_B, RR_C]调度器拿到这个队列后会先运行队首的FIFO_A。如果FIFO_A是个无限循环而且从不调用sched_yield、从不休眠那么RR_B和RR_C就只能在后面排队完全得不到执行。这时候不是SCHED_RR机制失效了而是它根本没有机会上场——队首的大哥不让位。如果FIFO_A每跑一小段就主动sched_yield()它会退到队尾队列变成[RR_B, RR_C, FIFO_A]接下来RR_B运行最多跑一个时间片默认 100ms时间片耗尽后被移动到队尾[RR_C, FIFO_A, RR_B]然后RR_C运行 100ms 后退到队尾。如此循环三个任务都能获得 CPU。这时候你能明确观察到RR_B和RR_C之间以约 100ms 的节奏交替。看到没RR 的时间片机制只在它自己“顶到队首”之后才生效。所以回答标题里最核心的问题当SCHED_FIFO和SCHED_RR同时存在时调度器并不是“FIFO 永远优先于 RR”也不是“RR 每过 100ms 自动抢一次”。真正决定一切的是优先级和队列位置。FIFO 在混合队列里的唯一特权是它没有时间片约束所以它排队排在队首时可以让后面所有人无限等待。2.3 不同优先级下高优先级任务拥有“插队权”上面讨论的都是同优先级竞争。一旦牵涉不同优先级事情就变得简单多了高优先级任务一旦就绪不管当前跑的是 FIFO 还是 RR都会被强制抢占。注意这里的抢占是指调度器在当前任务的下一次调度机会通常是时钟中断、系统调用返回、唤醒路径把 CPU 交给高优先级任务。假如你有一个SCHED_RR99 任务一个SCHED_FIFO50 任务。后者正跑得欢前者被某个事件唤醒那么调度器会立即打上“需要调度”的标记RR 99 会以极快的速度抢到 CPUFIFO 50 被挂起直到 RR 99 再次阻塞或退出。策略在跨优先级竞争时完全不参与决策。这种“插队权”在实际系统中是一把双刃剑。用它保护关键任务没问题但如果你把两个不同功能模块的实时任务优先级设得太接近又没有一个清晰的优先级规划系统行为会变得特别难测。我曾经在调试一台设备时看到仪表数据周期性卡顿花了半天才发现是一个SCHED_FIFO97 的任务频繁占用 CPU把SCHED_RR95 的数据打包任务挤得七零八落。所以调度策略不重要优先级规划才重要这句话怎么强调都不过分。3. 内核实现里的关键细节队列、时间片与抢占3.1 实时运行队列按优先级组织的链表数组要看懂“两种策略共存”的行为最好对内核实时调度器的数据结构有个概念。Linux 在 CFS 调度器引入之后实时调度类被统一为rt_sched_class。它维护了一个按实时优先级索引的数组每个优先级对应一个任务链表rt_prio_array.queue[0] - 最高优先级任务链表 rt_prio_array.queue[1] - ... ... rt_prio_array.queue[99] - 最低优先级任务链表调度器挑选任务时不需要遍历所有任务而是用一个“位图”快速判断哪个优先级的队列里有任务。即位图中第一个非空位对应的优先级最高直接从这个队列取队首任务。用伪代码表示for_each_set_bit(i, rq-rt.active.bitmap, MAX_RT_PRIO) { return list_first_entry(rq-rt.active.queue[i], struct task_struct, rt.run_list); }用户空间看到的是“优先级 1~99数字越大越优先”内核内部会做一次映射把数值小的内部优先级对应到用户空间数值大的优先级。所以当你设置sched_priority 99时内核里这个任务的优先级索引是 0排在最前面。这套数据结构带来的工程意义是无论系统里堆积了多少实时任务调度器挑选最高优先级任务的时间复杂度基本是常数级这也是实时任务延迟可控的重要基础。3.2 时间片到底是怎么消耗的SCHED_RR的时间片不是靠一个全局定时器去“打断”当前任务而是在每次时钟中断tick到来时当前运行任务的运行时间统计会被累加。当累计时间达到时间片上限调度器发现当前任务是SCHED_RR便把它从运行队列的队首摘下来重新挂到同优先级队列的尾部然后重新分配一个新的时间片。这里有一个容易误解的点SCHED_FIFO任务也有“时间片”这个概念吗严格说没有。SCHED_FIFO只有在 CPU 被抢占或主动让出时才会切换。如果你用chrt或者/proc查看 FIFO 任务有时能看到“剩余时间片”之类的字段但从调度语义上讲它不需要关心时间片耗尽。另外实时任务的时间片值可以通过sched_rr_get_interval()查询。在一个普通的 Linux 系统上你可能会看到返回 0.100000000 秒也就是 100ms。新一些的内核里/proc/sys/kernel/sched_rr_timeslice_ms可以改变这个默认值。但请注意改这个值会影响所有SCHED_RR任务不能只给某一个任务单独配时间片。3.3 抢占的触发时机没有“立刻”只有“尽快”很多人对“抢占”有个误解以为高优先级任务一唤醒低优先级任务瞬间被打断。实际上不是瞬间而是“尽快”。内核在唤醒高优先级实时任务时会设置当前 CPU 的TIF_NEED_RESCHED标志然后在以下时机之一执行真正的上下文切换时钟中断返回用户态系统调用返回用户态中断处理完成返回当前任务主动调用schedule()。换句话说如果当前任务正运行在内核态的临界区、关中断的代码段或者持有自旋锁即使一个优先级 99 的任务已经就绪也得等临界区退出后再切换。实时系统里常说的“调度延迟”主要就来自这些不可抢占点。这也是为什么很多高性能实时方案会对内核做 PREEMPT_RT 补丁把“几乎不可抢占”的内核路径变成“抢占点足够多”。3.4 实时带宽限制防止实时任务把整机拖死如果某个SCHED_FIFO99 任务写了一个死循环它理论上可以永久霸占 CPU让普通任务永无天日。为了避免这种事故内核默认开启了实时带宽控制。典型参数如下/proc/sys/kernel/sched_rt_period_us默认 10000001 秒/proc/sys/kernel/sched_rt_runtime_us默认 9500000.95 秒。含义是在每 1 秒的周期内所有实时任务总共最多只能运行 0.95 秒。超出部分会被内核“节流”强制让出 CPU 给普通任务下一个周期再恢复。所以你跑一个 RT 死循环时观察 CPU 占用可能不是 100%而是 95% 上下就是这个原因。在开发实时系统时这个概念值得警惕尤其是当你的实时任务确实需要超过 95% 的 CPU 时间时你会看到莫名的抖动。生产环境更合理的做法是绑定专用 CPU 核并配合isolcpus隔离而不是粗暴关掉实时带宽限制。4. 实操验证写一个 FIFO RR 的混合调度实验4.1 实验环境准备与权限检查要设置实时调度策略需要CAP_SYS_NICE权限。最简单的方式是使用 root 或sudo运行测试程序。另外为了让实验现象干净我建议把实验绑定到单个 CPU 上否则多核环境下三个任务可以各占一个核根本观察不到竞争。实验前先用nproc看一下 CPU 数量然后通过taskset或程序内的亲和性设置固定到 CPU0。准备内容一个 Linux 环境普通发行版即可不依赖实时内核补丁GCC 编译器最好有一台可以随便折腾的虚拟机或开发板避免误设高优先级后系统卡死影响工作。4.2 一个可复现的 C 实验三个同优先级实时线程这里我写了一个简单的 C 程序。它创建三个线程线程 ASCHED_FIFO优先级 50线程 BSCHED_RR优先级 50线程 CSCHED_RR优先级 50。三线程都被绑定到 CPU0。每个线程只做一件事疯狂累加自己的计数器。程序运行 3 秒后打印计数结果。如果某一个线程几乎没涨说明它被饿死了。#define _GNU_SOURCE #include pthread.h #include sched.h #include stdio.h #include stdlib.h #include string.h #include unistd.h static volatile unsigned long long cnt[3]; static int g_fifo_yield; static void *worker(void *arg) { long idx (long)arg; while (1) { cnt[idx]; if (g_fifo_yield idx 0 (cnt[idx] % 1000000 0)) sched_yield(); } return NULL; } static void create_rt(pthread_t *t, int policy, int prio, long idx) { pthread_attr_t attr; struct sched_param sp; memset(sp, 0, sizeof(sp)); pthread_attr_init(attr); /* 关键不要继承创建者的调度策略而是使用显式配置 */ pthread_attr_setinheritsched(attr, PTHREAD_EXPLICIT_SCHED); pthread_attr_setschedpolicy(attr, policy); sp.sched_priority prio; pthread_attr_setschedparam(attr, sp); if (pthread_create(t, attr, worker, (void *)idx)) { perror(pthread_create); exit(1); } pthread_attr_destroy(attr); } int main(int argc, char **argv) { pthread_t t[3]; cpu_set_t set; if (argc 1 strcmp(argv[1], yield) 0) g_fifo_yield 1; /* 整个进程及子线程都绑定到 CPU0确保竞争可观察 */ CPU_ZERO(set); CPU_SET(0, set); sched_setaffinity(0, sizeof(set), set); create_rt(t[0], SCHED_FIFO, 50, 0); create_rt(t[1], SCHED_RR, 50, 1); create_rt(t[2], SCHED_RR, 50, 2); sleep(3); printf(FIFO[50] cnt %llu\n, cnt[0]); printf(RR_B[50] cnt %llu\n, cnt[1]); printf(RR_C[50] cnt %llu\n, cnt[2]); return 0; }编译并运行gcc -O2 -o rt_mix rt_mix.c -lpthread sudo ./rt_mix预期输出大致是FIFO[50] cnt 123456789 RR_B[50] cnt 0 RR_C[50] cnt 0两个 RR 线程的计数接近 0说明它们被排在队首的 FIFO 任务死死压住。这不是调度器故障而是SCHED_FIFO不主动让位时的正常表现。接着运行带yield参数的版本sudo ./rt_mix yield输出会变成三个计数都有明显增长。因为 FIFO 线程每累加 100 万次就主动sched_yield()把自己排到队尾让 RR 线程轮流上台。可以看到RR_B和RR_C的计数接近而 FIFO 仍然会占一部分比例。这段代码里的一个关键点是PTHREAD_EXPLICIT_SCHED。新手最容易踩这个坑明明设置了pthread_attr_setschedpolicy却忘了把继承方式改成PTHREAD_EXPLICIT_SCHED结果子线程还是继承主线程的普通策略。如果你发现chrt -p看到的线程策略不是预期的 FIFO/RR先检查这一项。4.3 用现成工具观察调度状态除了写 C 程序Linux 自带的一些工具可以快速观察和设置实时调度策略。查看当前进程的调度策略和优先级ps -eo pid,class,rtprio,comm | grep rt_mixclass列会显示FFFIFO、RRRR、TS普通等。rtprio列显示实时优先级。如果看不到 class可以用chrt -p pid要把某个运行中的进程改成 FIFO 99sudo chrt -f -p 99 pid改成 RR 50sudo chrt -r -p 50 pid我在调试嵌入式设备时经常这样干先跑一个普通优先级程序确认它的 PID再用chrt动态提升为实时策略观察任务响应变化避免了每次都要重新编译程序的麻烦。不过要注意chrt对正在运行的关键进程下手要格外小心别把系统的 shell 或者 sshd 改成 FIFO 99 还不带退出条件否则系统可能瞬间失去响应。perf sched工具也很有用。你可以用perf sched record录制一段调度事件再用perf sched timehist或者perf sched latency分析每个任务的运行间隔和延迟。这个手段对定位“哪个任务抢占了 CPU、抢了多久”非常直观比靠猜靠谱得多。4.4 实验延伸同一优先级下多个 FIFO 的表现你可以把实验稍作改动把RR_B和RR_C都改成SCHED_FIFO优先级仍然是 50。运行后你会看到一个更有意思的现象三个 FIFO 任务里大概率只有第一个任务在轮转另外两个同样被饿死即使第一个任务偶尔让出 CPU第二个 FIFO 会接管并一直跑到让出为止。第三个 FIFO 必须等前面所有 FIFO 都让出后才有机会。这个现象能帮你形成直觉同优先级下FIFO 和 RR 混合本质上是把一部分“主动让出”的责任交给了程序员。如果不想让某个 FIFO 任务饿死其他同优先级任务要么给它降优先级要么让它在关键路径上主动sched_yield要么干脆把它改成 RR。实时系统里没有免费的午餐。5. 实际项目中最容易踩的坑与排查思路5.1 明明设置了实时策略线程却显示普通策略这个坑出现频率极高尤其是用 pthread 写实时程序时。大多数情况下是因为缺少pthread_attr_setinheritsched(attr, PTHREAD_EXPLICIT_SCHED)。Linux 线程创建时默认继承父线程的调度属性如果你只在 attr 里设置了 policy 和 param却没有显式声明“不要继承”子线程会继续使用创建者自己的策略。另一个原因是权限不足。普通用户进程设置SCHED_FIFO/SCHED_RR时会直接报EPERM。可以通过ulimit -r查看当前用户的实时优先级上限如果为 0普通用户就只能用 root 或者配置相应的 capability。还有一种隐藏问题某些容器或安全模块会禁止实时调度导致返回值EINVAL或者EPERM需要逐层排查。5.2 实时任务频繁抖动出现周期性“卡顿”如果你发现实时任务每 1 秒左右出现一次明显的延迟尖峰大概率是实时带宽限制在起作用。默认配置下实时任务每个周期最多运行 0.95 秒剩下的 0.05 秒会被强制让给普通任务。如果你的任务确实需要接近 100% 的 CPU 时间这种强制让出就会表现为规律性抖动。我个人的处理经验是先区分是带宽限制还是其他抢占源。用perf sched timehist看一下实时任务是否在某个固定周期被强切。看内核参数cat /proc/sys/kernel/sched_rt_period_us cat /proc/sys/kernel/sched_rt_runtime_us如果确认是带宽限制且业务上必须长期占用一个 CPU 核正确的做法是把该核从普通调度域里隔离出来通过内核启动参数isolcpus和nohz_full避免中断、内核线程等干扰而不是直接修改sched_rt_runtime_us -1。后者虽然可以关闭限制但也让系统失去了最后一道“安全绳”一不留神实时任务死循环就能把整机拖到不可用。5.3 高优先级任务面前仍然存在优先级反转很多人以为实时优先级高就万事大吉其实还有一把隐藏的刀优先级反转。简单说一个高优先级实时任务等一个低优先级任务持有的锁而低优先级任务又被中等优先级任务抢占导致高优先级任务被“卡”在中间。Linux 提供了实时互斥锁PTHREAD_PRIO_INHERIT来缓解但要记得显式设置属性pthread_mutexattr_t attr; pthread_mutexattr_init(attr); pthread_mutexattr_setprotocol(attr, PTHREAD_PRIO_INHERIT); pthread_mutex_init(lock, attr);在混合SCHED_FIFO和SCHED_RR的任务之间共享数据时一定要认真选择锁协议。否则你在测试单任务延迟时一切正常一旦多个实时任务并发运行这些“看不见”的锁竞争就会在延迟数据上暴露出来。5.4 多核环境下“明明有核空着实时任务却不迁移过去”实时任务的负载均衡与普通任务不太一样。普通任务追求 CPU 使用率均衡实时任务更关心高优先级任务能不能尽快找到可用的 CPU。内核有一套 push/pull 机制当某个 CPU 上出现更高优先级任务而当前 CPU 因为亲和性等原因无法运行它时会尝试把它推到其他空闲 CPU其他 CPU 空闲时也会主动从别的 CPU 拉取高优先级实时任务。但亲和性设置不当会导致高优先级任务滞留在一个很忙的核上看起来像是“实时任务没有获得应有的优先权”。排查时先确认taskset -pc pid的亲和性范围。在需要低延迟的实时工作中常见的做法是把实时线程绑到专用核同时把中断都挪到别的核上。千万不要指望调度器在所有核之间替你做完美的实时平衡。5.5 排查思路速查表下面这个表是我在排查实时调度问题时的快速入口分享给你现象优先检查可能的结论同优先级下 RR 任务几乎不跑队列中是否存在长时间运行的 FIFOFIFO 不主动让出RR 无机会高优先级实时任务仍然延迟大是否被关中断/自旋锁/内核临界区阻塞抢占不是瞬时的考虑 PRREMPT_RT 或换核隔离实时任务周期性卡顿sched_rt_runtime_us/sched_rt_period_us实时带宽限制触发节流线程策略设置后不生效pthread_attr_setinheritsched是否设置子线程继承了旧策略多个实时任务频繁互相牵制锁协议是否为PTHREAD_PRIO_INHERIT优先级反转实时任务没有在预期 CPU 上运行亲和性掩码、CPU 隔离配置push/pull 受阻或被隔离说到最后分享一个我自己的判断习惯在看到任何“实时任务不跑、跑得怪、延迟高”的现象时先别急着怀疑调度器先把两件事查清楚——它们的优先级数值是多少同一优先级队列里谁排在最前面把这两个问题用chrt或代码打印出来80% 的伪 bug 当场就能定位。剩下 20% 再去考虑内核抢占、中断、锁这些更深的问题。混合使用SCHED_FIFO和SCHED_RR并不是什么复杂玄学它只是同一套优先级规则下两种“同优先级内部管理策略”的搭配。把队列、优先级、时间片和抢占时机这四个词吃透你已经比大多数面试者更理解 Linux 实时调度了。希望这篇实战笔记能帮你省下几个小时的排查时间。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →