写多线程程序的朋友应该都遇到过这种场景代码里有三个线程在跑一个负责音频播放一个写日志还有一个做后台统计。跑起来之后音频偶尔卡一下日志线程倒是欢快得很。你直觉上知道要给音频线程提优先级于是去翻了sched_setscheduler、pthread_attr_setschedpolicy的资料结果发现一堆调度策略、优先级参数之间互相纠缠设置了半天还是不生效。这篇就围绕 Linux 线程优先级设置这件事把调度策略和参数层面的门道一次说清。内容适配写过 pthread、但对实时调度不熟的开发者也适合准备做音视频、工业控制、机器人等对延迟敏感项目的朋友。我尽量把原理、代码、踩坑经验放在一起讲你能直接照着抄也能明白为什么这么写。1. 调度策略选型先搞清楚内核到底优先让谁跑很多教程一上来就教你怎么调pthread_attr_setschedpolicy但如果你不清楚每种调度策略的语义设完大概率是玄学调参。Linux 的调度器在用户态能看到的基本策略就这么几个选错了优先级设了也白搭。1.1 三种主流调度策略的适用边界默认情况下你写的所有线程跑在SCHED_OTHER上。这是完全公平调度器CFS管理的策略核心思路是让所有普通线程按照虚拟运行时间均匀获得 CPU。你可能听到过nice 值越大优先级越低的说法对但又不够准确——nice 值在 CFS 里并不是直接决定谁先跑它只是改变了线程在 CPU 上的权重一个 nice 值差一级权重大约差 1.25 倍。换句话说nice 只影响普通线程之间分享 CPU 的比例没有抢占的语义。SCHED_FIFO和SCHED_RR是两种实时调度策略。这两类线程有一个共同点它们的优先级是一个 1 到 99 的整数数值越大优先级越高而且只要有一个实时线程处于可运行状态所有SCHED_OTHER线程都会被完全压住。是的实时线程优先级最低的 1也比普通线程里优先级最高的要优先。这不是文档里的空话是真的会发生的抢占。SCHED_FIFO和SCHED_RR的差别在于时间片。SCHED_FIFO没有时间片的概念一个线程只要不主动让出 CPU、不阻塞、不被更高优先级的实时线程抢占它就可以一直跑下去。SCHED_RR则给相同优先级的实时线程分配时间片用完了会排到同优先级队列的末尾避免同级线程之间饿死。调度策略调度方式优先级范围适用场景SCHED_OTHERCFS 按权重分配0配合 nice -20~19绝大多数普通业务线程SCHED_FIFO实时、无时间片1~99需要严格低延迟的控制线程如音频实时处理SCHED_RR实时、同优先级轮转1~99多个实时任务需要轮流执行的场景SCHED_BATCH普通策略变体0适合 CPU 密集型的批处理任务SCHED_IDLE最低优先级普通策略0后台极低优先级任务这里有个最常见的误解音频播放线程应该用SCHED_FIFO但如果你把日志线程也设成实时策略两个实时线程之间反而要纠结优先级关系日志线程如果优先级设得比音频高一样会抢音频。所以实时策略要省着用只给真正需要低延迟的少数几个线程。1.2 nice 值与实时优先级的两套体系我见过不少新手把 nice 和实时优先级混用比如先setpriority把 nice 调到 -20又用pthread_setschedparam把sched_priority设成 50然后问为什么行为不对。因为这两套体系是不同调度策略下的参数互相不通用。nice只对SCHED_OTHER和SCHED_BATCH生效取值范围是 -20 到 19数值越小越积极地争取 CPU但它是按权重比例来影响的不会立刻抢占其他线程。实时优先级只对SCHED_FIFO和SCHED_RR生效而且数值越大越优先。这个方向和 nice 正好相反很多人在代码里把实时优先级写成 1以为最小最快结果它反而是实时线程里最弱的那个。还要注意一点同一个线程只能有一个调度策略设置SCHED_FIFO时传入的sched_priority才有意义如果策略是SCHED_OTHERsched_priority必须填 0否则会返回错误。实际开发中我建议先明确一个原则普通线程用 nice 做整体调配实时线程用 rtprio 做硬性抢占两者不要混着用。2. 线程优先级设置 API从属性对象到系统调用Linux 的调度相关系统调用不少但针对线程优先级设置实际高频用到的就几个函数。我先从创建线程阶段讲起因为这是最推荐的做法在线程还没出生之前就把它的调度策略定好避免它用默认策略跑了才去改。2.1 创建线程阶段pthread_attr_t 的三个关键函数使用pthread_create创建线程时可以通过pthread_attr_t携带调度信息。核心代码如下#include pthread.h #include sched.h #include stdio.h #include errno.h static void *worker(void *arg) { while (1) { /* 业务逻辑 */ } return NULL; } int main(void) { pthread_t tid; pthread_attr_t attr; struct sched_param param; pthread_attr_init(attr); /* 指定调度策略为 SCHED_FIFO */ pthread_attr_setschedpolicy(attr, SCHED_FIFO); /* 设置实时优先级FIFO 的有效范围通常是 1~99 */ memset(param, 0, sizeof(param)); param.sched_priority 80; pthread_attr_setschedparam(attr, param); /* 关键让新线程显式使用 attr 中的调度参数 */ pthread_attr_setinheritsched(attr, PTHREAD_EXPLICIT_SCHED); if (pthread_create(tid, attr, worker, NULL) ! 0) { perror(pthread_create); pthread_attr_destroy(attr); return 1; } pthread_attr_destroy(attr); pthread_join(tid, NULL); return 0; }三个函数的顺序没有强制要求但逻辑上建议按上面这样写。先定策略再定参数最后明确继承方式。sched_priority的具体有效范围可以用系统查询接口拿到不要靠记忆硬编码int min sched_get_priority_min(SCHED_FIFO); // 通常为 1 int max sched_get_priority_max(SCHED_FIFO); // 通常为 99在SCHED_RR下范围同样是 1~99。是不是 1 到 99 取决于内核配置某些实时内核或嵌入式环境可能不同所以“先查再设”是好习惯。2.2 运行期修改pthread_setschedparam 与 sched_setscheduler如果线程已经创建了再想调整策略和优先级可以用pthread_setschedparamstruct sched_param param; memset(param, 0, sizeof(param)); param.sched_priority 50; if (pthread_setschedparam(tid, SCHED_RR, param) ! 0) { perror(pthread_setschedparam); }这个函数要求你传入一个pthread_t类型的线程 ID它修改的是指定线程的调度属性。这里有个容易被绕晕的坑Linux 提供了sched_setscheduler(pid_t pid, int policy, const struct sched_param *param)它接收的是进程 ID但内核真正调度单元的粒度是线程task所以这个接口实际作用于传入 pid 对应的内核 task。如果你传入getpid()它只会影响主线程并不会把整个进程的所有线程全部改成实时策略。想精确控制某一个线程老老实实用pthread_setschedparam(pthread_self(), ...)或在线程内用pthread_setschedparam(pthread_self(), ...)。运行期修改的典型使用场景是程序启动时先以普通策略运行完成初始化、加载资源等耗时步骤等核心工作线程准备好之后再把某个线程提升到实时优先级。这样既避免了启动阶段拖慢系统又保证了运行阶段的关键延迟。2.3 很容易被忽略的 inheritsched 陷阱我见过很多次这样的案例明明调用了pthread_attr_setschedpolicy和pthread_attr_setschedparam新线程创建出来却还是SCHED_OTHER代码检查半天没问题最后发现是没调用pthread_attr_setinheritsched(attr, PTHREAD_EXPLICIT_SCHED)。这背后是 POSIX 线程的属性继承机制。pthread_attr_t里有一个inheritsched属性默认值是PTHREAD_INHERIT_SCHED含义是新线程不直接采用attr里设置的调度参数而是继承创建者线程当前的调度策略和优先级。这个设计有它的道理因为你可能只想把attr里的部分内容用于其他属性并不希望它覆盖调度行为。但这也意味着如果你只设置了schedpolicy和schedparam忘了把inheritsched改成PTHREAD_EXPLICIT_SCHED那么一切设置都不会生效。正确姿势就是上面代码里的那一行。这个坑在官方 man page 里写得很清楚但实际踩坑的人非常多。建议你把pthread_attr_setinheritsched当作设置优先级时的“第四件套”和其他两个函数一起出现。3. 完整实操 Demo给音频线程预留 CPU 时间片理论讲完上实际例子。这个 Demo 模拟一个播放器进程主线程负责启动和监控音频线程用实时调度保证低延迟日志线程用普通调度跑着写文件。3.1 场景设计音频线程做的事情可以简化为“从缓冲区取数据、模拟处理、然后继续循环”。这类线程对延迟敏感理想情况下应该保证可运行状态时立刻拿到 CPU。日志线程则无所谓它晚跑几毫秒没人关心。设计目标就是音频线程用SCHED_FIFO优先级 80日志线程保持默认SCHED_OTHER。另外我还给音频线程设置了 CPU 亲和性把它绑到一个独立核上减少跨核迁移带来的延迟抖动。这一步不是必须但和优先级配合起来效果更好。3.2 代码实现完整代码如下我是按可直接编译运行的标准写的#define _GNU_SOURCE #include pthread.h #include sched.h #include stdio.h #include string.h #include errno.h #include unistd.h static void *audio_loop(void *arg) { struct sched_param param; int policy; cpu_set_t set; /* 绑定到 CPU 2避免线程在核间迁移 */ CPU_ZERO(set); CPU_SET(2, set); pthread_setaffinity_np(pthread_self(), sizeof(set), set); /* 回读当前调度信息用于验证 */ pthread_getschedparam(pthread_self(), policy, param); printf(audio thread: policy%d, rtprio%d\n, policy, param.sched_priority); while (1) { /* 模拟实时音频处理循环 */ volatile unsigned int x 0; for (int i 0; i 1000; i) { x i; } sched_yield(); /* 主动让出 CPU给同级或低优先级线程机会 */ } return NULL; } static void *log_loop(void *arg) { while (1) { /* 模拟写日志 */ usleep(1000); } return NULL; } int main(void) { pthread_t audio_tid, log_tid; pthread_attr_t attr; struct sched_param param; /* 创建音频线程使用 SCHED_FIFO优先级 80 */ pthread_attr_init(attr); pthread_attr_setschedpolicy(attr, SCHED_FIFO); memset(param, 0, sizeof(param)); param.sched_priority 80; pthread_attr_setschedparam(attr, param); pthread_attr_setinheritsched(attr, PTHREAD_EXPLICIT_SCHED); if (pthread_create(audio_tid, attr, audio_loop, NULL) ! 0) { perror(pthread_create audio); return 1; } pthread_attr_destroy(attr); /* 日志线程用默认策略创建即可 */ pthread_create(log_tid, NULL, log_loop, NULL); pthread_join(audio_tid, NULL); pthread_join(log_tid, NULL); return 0; }编译命令gcc -O2 -o rt_demo rt_demo.c -lpthread注意pthread_setaffinity_np和CPU_SET都需要_GNU_SOURCE宏编译时别漏了。别小看sched_yield()这个调用在SCHED_FIFO下如果循环体里没有任何让出 CPU 的动作音频线程会一直占着一个核日志线程会被压到别的核上或者迟迟得不到调度这在多核机器上可能不明显在单核环境就是灾难。3.3 运行结果解读由于SCHED_FIFO需要权限直接运行大概率报EPERM。先用 sudo 试一次sudo ./rt_demo输出类似audio thread: policy1, rtprio80policy1对应的正是SCHED_FIFO。如果你看到policy0说明线程并没有按你的设置跑起来最可能的原因就是上一章说的 inheritsched 忘了设或者是权限问题导致设置静默失败。为什么要用pthread_getschedparam回读因为有些错误在pthread_create阶段不会直接暴露回读是验证设置是否生效最快的手段。4. 设置失败排查为什么你的优先级没生效优先级设置不生效排在第一位的原因不是代码逻辑而是权限。其次是查错了线程 ID再次是错误地使用了sched_setscheduler。我按排查链路一条条说。4.1 权限问题CAP_SYS_NICE 与 RLIMIT_RTPRIOLinux 对实时调度策略有严格的权限控制。非 root 用户想把线程调度策略设置为SCHED_FIFO或SCHED_RR默认情况下会被拒绝错误码是EPERM。内核看的是线程是否具备CAP_SYS_NICE能力而普通进程没有这个能力。常见的解决办法有三种# 方式一直接用 root 运行程序 sudo ./rt_demo # 方式二提升当前 shell 的实时优先级上限 sudo bash -c ulimit -r unlimited; exec bash ulimit -r 80 ./rt_demo # 方式三给二进制文件添加 cap_sys_nice 能力 sudo setcap cap_sys_niceep ./rt_demo ./rt_demo方式二的原理是RLIMIT_RTPRIO资源限制允许非特权用户把实时优先级设置到这个上限值以内。如果你在 shell 里执行ulimit -r 80那么该 shell 启动的进程就能把线程实时优先级设到 80 及以下不需要 root。getcap ./rt_demo可以查看二进制文件的能力位。方式三有个坑setcap设置的 capability 在文件被重新编译后会被清除必须重新设置。另外如果这个二进制文件后面被 strip某些场景下 capability 也会丢失所以 CI 流程里要把setcap步骤写进去。4.2 排查链路与常见错误码我建议按照下面的顺序排查优先级不生效的问题用pthread_getschedparam回读策略和优先级确认线程当前真实状态。检查pthread_create和pthread_setschedparam的返回值打印errno。检查进程是否处于容器环境容器内通常需要额外权限配置才能使用实时调度。如果用了sched_setscheduler(getpid(), ...)请确认你确实在操作目标线程而不是主线程。常见的错误码和处理方向如下表错误码含义处理方向EPERM没有权限设置该策略/优先级提权、提升 RLIMIT_RTPRIO 或 setcapEINVAL策略无效或优先级超出范围用 sched_get_priority_min/max 查询范围ESRCH找不到对应线程确认 pthread_t 有效线程未被回收还有一个容易被忽略的线程池里的线程一旦被回收pthread_t就可能被复用。如果你保存了一个旧线程 ID在它退出后再用它设置优先级会拿到ESRCH甚至可能误伤到一个新线程。我的习惯是尽量把优先级的设置放在创建阶段或者由线程自己调用pthread_setschedparam(pthread_self(), ...)避免跨线程持有 ID。5. 实时线程的隐藏风险优先级翻转与CPU占满设置实时优先级不是“调到 99 就万事大吉”。实时调度是一把快刀用不好会割伤自己。这一章必须聊透因为新手最容易在这里翻车。5.1 优先级翻转一个真实事故发生的过程先描述一个非常经典的现场。进程里有三个线程T1 是实时优先级 80 的高优先级线程T2 是普通优先级的中等线程T3 是实时优先级 30 的低优先级线程但仍然是实时类。T1 和 T3 共享一把锁并且某个时刻 T3 拿到了锁还没释放。此时 T1 进入临界区时发现锁被占用于是睡眠等待。T2 虽然是普通线程但因为 T1 在等锁、T3 还没被调度T2 得到了 CPU 并且开始长时间运行。结果就是最高优先级的 T1 反而被中等优先级的 T2 间接压制实时性完全丧失。这不是理论推导我实际调试过这样的现场表现就是高优先级控制线程偶尔出现几百毫秒的响应延迟抓包都抓不到原因。解决优先级翻转的标准手段是优先级继承协议。在 pthread 编程里对应的是给互斥锁设置PTHREAD_PRIO_INHERIT协议pthread_mutexattr_t mattr; pthread_mutexattr_init(mattr); pthread_mutexattr_setprotocol(mattr, PTHREAD_PRIO_INHERIT); pthread_mutex_init(mutex, mattr);设置之后当 T1 等待 T3 持有的锁时内核会把 T3 的优先级临时提升到 T1 的级别让 T3 尽快跑完并释放锁T1 随后恢复运行。glibc 底层用到的是内核的 PI-futex 机制实际效果非常可靠。所有和实时线程一起使用的互斥锁我都强烈建议加上这个协议。5.2 互斥锁协议与实时线程的写法约束就算加了 PI 协议实时线程也不能乱写。核心约束有这么几条都是血泪教训第一实时线程里不要做可能阻塞太久的操作比如大文件写入、内存分配、网络请求。内存分配在缺页时可能触发磁盘 IO这个延迟对实时线程是致命的。我一般会在初始化阶段把实时线程需要的内存一次性分配好运行期不再 malloc。第二不要写没有让步的无限循环。SCHED_FIFO下一个空转的 while(1) 会永久霸占 CPU导致同优先级甚至更低优先级的所有线程都没有机会执行。如果必须轮询用sched_yield()、nanosleep或阻塞式等待替代忙等。第三实时线程的栈不要设得太小。实时调度切换相比普通线程更快但栈溢出的后果一样是段错误。优先级越高栈空间越要留足。操作是否建议在实时线程中执行原因互斥锁 PTHREAD_PRIO_INHERIT建议避免优先级翻转普通互斥锁不建议可能引发优先级翻转malloc/free不建议可能缺页阻塞文件 IO / 网络 IO不建议不可控延迟sched_yield / 阻塞等待建议主动让出 CPU无让步忙等循环禁止导致同级/低级线程饿死6. 验证与调优让优先级真正落在你的线程上代码写完了、权限也过了怎么确认线程优先级设置没问题这一步很容易被忽略但恰恰是定位疑难杂症最有效的手段。6.1 工具链chrt、ps 与 /proc 接口chrt是 util-linux 自带的工具既可以查询也可以设置调度策略。查询某个进程或线程的实时调度信息chrt -p 12345输出一般长这样pid 12345s current scheduling policy: SCHED_FIFO pid 12345s current scheduling priority: 80如果想在程序不退出时动态修改某个线程的调度参数也可以用 chrt# 把 pid 为 12345 的线程改成 SCHED_FIFO 优先级 50 chrt -f -p 50 12345但注意chrt 操作的是内核的任务多线程程序里线程 ID 和进程 ID 是两回事你需要通过ps -T -p pid找到线程 ID再对线程 ID 执行 chrt。ps 查看调度类别和优先级更好用ps -eLo pid,tid,cls,rtprio,ni,pri,comm | grep rt_demo其中cls列的含义TS表示SCHED_OTHERFF表示SCHED_FIFORR表示SCHED_RR。rtprio列显示实时优先级ni列显示 nice 值。我看到过很多次rtprio列显示-这就是没设置成功。proc 文件系统也能看/proc/pid/task/tid/sched里有调度策略、优先级、运行时间等详细信息适合写监控脚本抓取。6.2 与CPU亲和性、cgroup配合的进阶用法实时线程最好配合 CPU 亲和性一起使用。原因很简单内核负载均衡器会在核之间迁移线程每次迁移都可能带来缓存失效和额外的调度延迟。绑定核之后线程的缓存局部性更好延迟抖动也小不少。代码层面就是前面 Demo 里用到的pthread_setaffinity_npcpu_set_t set; CPU_ZERO(set); CPU_SET(2, set); pthread_setaffinity_np(pthread_self(), sizeof(set), set);命令行对应的工具是tasksettaskset -c 2 ./rt_demo如果你在容器里跑这类程序还有一层需要注意容器默认的 seccomp 配置和 capabilities 可能不允许sched_setscheduler系统调用即使宿主机是 root 也一样报EPERM。需要显式给容器加CAP_SYS_NICE或调整 seccomp 策略。这个细节在做嵌入式边缘计算的同学身上经常出现排查时可以优先想到。cgroup 层面cpu.weight旧版本叫cpu.shares影响的是 CFS 普通线程之间的权重分配对实时线程不生效。也就是说无论怎么调 cgroup 配额实时线程都会优先于其他任务获得 CPU。所以如果你在 cgroup 里把某个服务的 CPU 权重调得再高也压不过一个设置了SCHED_FIFO的外部线程。反过来你也别指望用 cgroup 限制一个失控的实时线程去“惩罚”它——配额对它无效只能用亲和性、优先级下调、或者强制绑核来限制。验证优先级是否真的配合上了还有一个小经验把SCHED_FIFO优先级从 10 调到 90再观察目标线程在竞争激烈时的调度延迟变化。如果延迟曲线没有明显差别说明真正限制它的不是调度优先级而是锁等待、IO 阻塞这类因素往这个方向排查才是正解。我在实际项目中踩过不少次关于inheritsched的坑几乎每个接手实时调度代码的同事都犯过同样的错误。现在我的习惯是所有涉及优先级设置的代码都在创建线程后立刻用pthread_getschedparam回读并打日志确认设置生效再进入正式逻辑。另外不要把优先级数字散落在代码各处最好集中在配置里线上调参时直接改配置重启比临时改代码高效得多。