1. 从一次线上抖动说起为什么 FIFO 和 RR 值得你花时间Linux 进程调度里实时调度策略是绕不开的一块。很多同学第一次接触chrt、sched_setscheduler这些词是在排查某个采集程序抖动、某个控制回路延迟忽高忽低的时候。你可能会发现明明代码里没写什么复杂逻辑进程却偶尔被别的任务挤下去几百毫秒。这时候问题往往不在你的业务代码而在调度策略上。Linux 把进程大致分成两类普通进程走 CFS完全公平调度实时进程走 FIFO 或 RR。FIFO 和 RR 都属于实时调度策略优先级范围是 1 到 99数字越大优先级越高。FIFO 的特点是「一条道跑到黑」——同优先级下先跑的进程不主动让出 CPU后面的同优先级进程就没机会RR 则是同优先级之间按时间片轮转Linux 上这个时间片默认是 100ms 左右。这篇文章面向的是需要在 Linux 上做实时性调优、或者想搞清楚chrt命令到底改了什么的读者。我会用chrt和sched_setscheduler两条路径演示配置再给一套可复制的验证脚本让你亲眼看到 FIFO 和 RR 在ps、top以及上下文切换上的差异。整个过程不需要改内核普通 root 权限就能跑。需要说明的是实时进程优先级高到一定程度确实可能把普通进程饿死所以实验时建议在虚拟机或测试机上做别在生产环境随手把关键进程设成 99。2. 前置准备TaoToken 与实验环境2.1 实验环境要求我用的是一台 4 核的 Linux 机器内核版本 5.x 以上都行。你需要root 权限设置实时调度策略需要CAP_SYS_NICEgcc编译环境chrt、ps、top这些基础工具一般发行版自带可选systemtap或perf用来观察上下文切换先确认一下当前内核支持的调度策略chrt -m输出会列出SCHED_OTHER、SCHED_FIFO、SCHED_RR、SCHED_BATCH、SCHED_IDLE等以及各自的优先级范围。实时策略的 min/max 通常是 1 和 99。2.2 为什么这里会提到 TaoToken做调度实验时我习惯把测试代码、观测脚本、以及跑出来的日志统一放到一个能随时调用的环境里。TaoToken 提供了一套模型对话和 API 接入能力适合在写脚本、查报错、生成测试用例时随手用。比如你不确定sched_setscheduler的返回值含义或者想快速生成一个多进程压测脚本可以直接在模型对话里问。如果你要长期跑编码类任务或者 Agent 实验Coding Plan 会更合适只是临时验证模型行为用模型对话就够了。接入相关的 Key 在 API Keys 页面管理文档在接入文档里。下面进入正题先看chrt怎么用。3. 可复制配置chrt 命令与 sched_setscheduler 代码3.1 用 chrt 设置 FIFO 和 RRchrt是最直接的命令行工具。语法是chrt -f priority command chrt -r priority command-f对应 FIFO-r对应 RR。比如启动一个 FIFO 优先级 50 的进程chrt -f 50 ./my_realtime_task启动一个 RR 优先级 30 的进程chrt -r 30 ./my_realtime_task也可以修改已经在跑的进程。先拿到 PID然后chrt -f -p 60 pid chrt -r -p 40 pid查看某个进程当前的调度策略和优先级chrt -p pid输出里policy会显示SCHED_FIFO或SCHED_RRpriority就是实时优先级。3.2 用 sched_setscheduler 在代码里设置命令行适合临时验证真正做产品还是要在代码里设置。核心结构体和函数#include sched.h struct sched_param { int sched_priority; }; int sched_setscheduler(pid_t pid, int policy, const struct sched_param *sp);policy取值SCHED_FIFO、SCHED_RR、SCHED_OTHER。实时策略的sched_priority必须在 1 到 99 之间普通策略只能是 0。下面是一个最小可运行示例设置当前进程为 FIFO 优先级 50并绑定到 CPU 1#define _GNU_SOURCE #include stdio.h #include stdlib.h #include sched.h #include unistd.h #include errno.h #include string.h int main(int argc, char *argv[]) { struct sched_param param; int policy SCHED_FIFO; int prio 50; if (argc 3) { policy atoi(argv[1]); prio atoi(argv[2]); } param.sched_priority prio; if (sched_setscheduler(0, policy, param) -1) { fprintf(stderr, sched_setscheduler failed: %s\n, strerror(errno)); return 1; } cpu_set_t mask; CPU_ZERO(mask); CPU_SET(1, mask); if (sched_setaffinity(0, sizeof(mask), mask) -1) { fprintf(stderr, sched_setaffinity failed: %s\n, strerror(errno)); } printf(pid%d policy%d prio%d\n, getpid(), policy, prio); while (1) { /* 模拟计算 */ volatile unsigned long x 0; for (int i 0; i 1000000; i) x i; usleep(1000); } return 0; }编译gcc -O2 -o rt_task rt_task.c运行 FIFO 版本sudo ./rt_task 1 50运行 RR 版本sudo ./rt_task 2 50这里1是SCHED_FIFO2是SCHED_RR。注意sched_setscheduler的第二个参数直接传策略常量不要传字符串。3.3 优先级范围校验设置之前最好校验一下范围避免返回EINVALint min sched_get_priority_min(policy); int max sched_get_priority_max(policy); if (prio min || prio max) { fprintf(stderr, priority out of range [%d, %d]\n, min, max); return 1; }实时策略的 min 是 1max 是 99。普通策略 min 和 max 都是 0。4. 验证请求与成功结果观察 FIFO 与 RR 的行为差异4.1 用 ps 观察优先级和时间先启动几个不同优先级的实时进程。写一个启动脚本#!/bin/bash # start_mix.sh sudo ./rt_task 1 99 sleep 0.1 sudo ./rt_task 1 70 sleep 0.1 sudo ./rt_task 1 70 sleep 0.1 sudo ./rt_task 1 70 sleep 0.1 sudo ./rt_task 1 50 sleep 0.1 sudo ./rt_task 1 30 sleep 0.1 sudo ./rt_task 1 10 然后循环观察#!/bin/bash # watch_ps.sh for i in $(seq 1 40); do ps -C rt_task -o pid,pri,cmd,time,psr psinfo.log 21 sleep 2 done跑 FIFO 时你会看到优先级 99 的进程一直占着 CPU直到它退出70 的进程才开始跑三个 70 的进程里先启动的那个不结束后面两个根本没机会。这就是 FIFO 的「一条道跑到黑」。把启动脚本里的1换成2再跑一遍 RR。这次三个 70 优先级的进程会轮流出现在ps输出里每个进程的TIME字段增长得比较均匀。ps的PRI列显示的是内核视角的优先级实时进程用户态 99 对应内核态 0用户态 70 对应内核态 29这个映射关系在排查时容易看懵记住「用户态数字大优先级高内核态数字小优先级高」就行。4.2 用 top 看实时进程top里按P按 CPU 排序实时进程会排在前面。你也可以在top里按f添加PRI和NI列。RR 策略下同优先级的几个进程 CPU 占用会交替上升FIFO 下则是一个进程吃满、其他同优先级进程几乎不动。4.3 用上下文切换验证时间片如果想看得更细可以用perf或者systemtap抓sched_switch事件。简单一点用perfsudo perf record -e sched:sched_switch -a sleep 10 sudo perf script | grep rt_taskRR 策略下你会看到同优先级的rt_task之间每隔大约 100ms 切换一次FIFO 下同优先级进程之间几乎没有切换只有高优先级抢占或者进程退出时才切。一个更轻量的办法是读/proc/pid/schedstatcat /proc/$(pgrep rt_task | head -1)/schedstat三个字段分别是在 CPU 上运行的时间ns、等待调度的时间ns、被切换的次数。RR 下同优先级进程的切换次数会明显高于 FIFO。5. 本篇常见错排查5.1 sched_setscheduler 返回 EPERM最常见的原因是权限不够。实时调度需要CAP_SYS_NICE普通用户默认没有。用sudo跑或者给可执行文件加 capabilitysudo setcap cap_sys_niceep ./rt_task注意加了 capability 之后普通用户也能设置实时优先级测试完记得setcap -r去掉。5.2 返回 EINVAL优先级超出范围或者策略和优先级不匹配。比如给SCHED_OTHER设了非 0 优先级或者给 FIFO 设了 0 或 100。用sched_get_priority_min/max先查范围。5.3 设置了 FIFO 但进程还是被抢占检查是不是有更高优先级的实时进程存在。FIFO 只保证同优先级不切换高优先级来了照样抢占。另外如果进程调用了阻塞系统调用进入睡眠也会让出 CPU。5.4 RR 时间片看起来不是 100ms时间片受内核配置和负载影响。/proc/sys/kernel/sched_rr_timeslice_ms可以查看和调整 RR 时间片cat /proc/sys/kernel/sched_rr_timeslice_ms echo 50 | sudo tee /proc/sys/kernel/sched_rr_timeslice_ms改完再跑 RR 实验切换间隔会跟着变。5.5 实时进程把系统卡死这是最危险的坑。FIFO 优先级 99 的进程如果死循环且不睡眠普通进程包括你的 shell 都可能没机会跑。实验时建议用timeout限制运行时间留一个高优先级看门狗进程在虚拟机里做方便强制重启sudo timeout 10 ./rt_task 1 995.6 ps 里 PRI 和预期对不上前面说过用户态和内核态优先级含义相反。ps的PRI是内核视角实时进程用户态 99 显示为 0用户态 1 显示为 98 左右。别拿用户态的 99 去ps里找 99。6. 继续深入把实验脚本和模型辅助结合起来到这里FIFO 和 RR 的配置与验证链路已经跑通了。你可以把上面的rt_task.c、start_mix.sh、watch_ps.sh存成一个实验目录每次调参后重新跑一遍对比psinfo.log和schedstat的变化。如果实验过程中遇到报错看不懂或者想快速生成一个多核绑定的变体脚本可以直接在模型对话里贴上报错和代码片段让它帮你分析。需要长期做这类调度实验、写观测脚本的话Coding Plan 的额度更适合反复调用只是偶尔查一下 API 用法用 API Keys 配合接入文档就够了。把实验环境固定下来比每次临时搭一遍要省事得多。