深夜十一点,城市急诊室灯火通明,可值班医生只有一位。120 不断送来病人:有人心脏骤停,有人骨折流血,有人只是轻微擦伤。分诊护士给每个人挂上不同颜色的牌子——红牌危重,黄牌较重,绿牌轻症。医生每次只能救一个,先救谁?救到什么程度算“告一段落”?这背后的一套秩序,恰好就是操作系统的日常:CPU 是那位唯一的医生,进程和线程是候诊的病人,而内核里的调度策略就是急诊室的接诊规则。Linux 提供两套实时接诊规则:SCHED_FIFO 像“上了手术台就必须把手术做完”,SCHED_RR 像“门诊每人限时三分钟,到点换下一位”。再加上一条铁律——只要推进来一位牌子更红的病人,医生必须立刻放下手里的活冲过去抢救,这就是“优先级抢占”。这套秩序决定了手术机器人会不会卡顿、自动驾驶能不能在 100 毫秒内刹停、股票交易能不能抢在对手前面成交。下面,我们就用急诊室的故事,把这三件事从头讲透。

岗位关联

实时调度是音视频、自动驾驶、工业控制等高确定性系统的地基。理解 SCHED_FIFO/SCHED_RR 与抢占机制,是内核工程师和高性能应用开发者的看家本领。

目录
  1. 可视化演示
  2. 实时调度概述
  3. 优先级机制
  4. FIFO运行策略
  5. RR轮转策略
  6. 优先级抢占
  7. 运行队列切换
  8. 实践配置工具
  9. 风险与优化
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

实时调度概述

急诊室的病人并不都是“能等”的。心脏骤停的患者,晚一分钟处理就可能丧命;而一个脚踝扭伤的年轻人,在候诊室多坐半小时也没关系。操作系统里也有这样两类任务:一类是普通进程,晚几毫秒执行只是体验变差;另一类是实时任务,必须在规定时间内拿到结果,否则就是事故。判断一个任务是否“等不得”,看它有没有截止期限(deadline)就一目了然。

有截止期限不代表一定强求。现实中,“手术必须成功”是硬实时,错过期限等于失败;“外卖 30 分钟内送到”是软实时,超时几分钟还能接受,只是体验打折。Linux 的实时调度器并不区分这两者,它只负责一件事:让所有挂了红牌的任务,尽可能在期限内完成。

但医生只有一个,病人却很多。能不能安排得过来,有一个经典的利用率判据:

所以调度器的一切设计,都是在“医生不够用”这个前提下展开的。剩下的问题只有一个:怎样保证最等不及的病人先被照顾到?这就引出了急诊室的第一个工具——优先级牌子。

∑i=1nCiTi≤1\displaystyle \sum_{i=1}^{n} \frac{C_i}{T_i} \le 1
意思是:所有实时任务占用 CPU 的时间比例加起来,不能超过 100%。就像夜班医生的可用时间只有 8 小时,所有“必须做的事”加起来超过 8 小时,无论怎么安排,都必然有病人等不到。
  1. C_i(治疗时长)第 i 个任务每次运行需要占用的 CPU 时间。比如心电监测程序每轮要算 10ms,记作 C_i。
    CiC_i
  2. T_i(复发周期)第 i 个任务的释放周期,也就是病人每隔多久来一次。比如每 100ms 必须跑一轮,记作 T_i。
    TiT_i
  3. C_i / T_i(占用率)单个任务占掉医生工作时间的比例,是一个介于 0 和 1 之间的小数。
    CiTi\frac{C_i}{T_i}
  4. 求和 Σ把所有实时任务的占用率加起来,得到系统总负荷。
    ∑i=1nCiTi≤1\sum_{i=1}^{n} \frac{C_i}{T_i} \le 1

优先级机制

急诊室不会让所有病人排成一条长队。分诊护士会先给每个人挂上牌子:红牌是危重,立刻抢救;黄牌是较重,尽快处理;绿牌是轻症,排队等着。操作系统也一样,每个进程和线程都有一块“紧急牌”,名字叫优先级(priority)。调度器每次选人,先看牌子再看顺序。

在 Linux 中,实时调度的优先级是 1 到 99 之间的整数,数值越大,紧急程度越高,99 就是最高级别的红牌。这与普通进程的 nice 值正好相反——nice 的取值范围是 -20 到 19,数值越小优先级越高。两套规则方向不同,初学者最容易在这里栽跟头。

给病人挂红牌,在代码里是通过系统调用来完成的:

牌子挂对了,医生才能在关键时刻认出谁最该救。可是“先救谁”只是第一步,另一个问题是:救到什么程度才换人?这就轮到 SCHED_FIFO 出场了。

sched_setscheduler(pid, SCHED_FIFO, sched_priority=80)\displaystyle \text{sched\_setscheduler}(pid,\ \text{SCHED\_FIFO},\ \text{sched\_priority}=80)
这句代码的意思是:把指定进程的“紧急牌”挂成 80 号红牌,并启用 FIFO 接诊规则。普通用户没有权限随便挂红牌,需要 root 身份或 CAP_SYS_NICE 能力——就像只有护士长才有权发红牌一样。
  1. pid(病人编号)指定要给哪位病人挂牌,也就是哪个进程或线程。
    pidpid
  2. SCHED_FIFO(接诊规则)选择用哪套调度策略,本文主角之一,后面会详细展开。
    SCHED_FIFO\text{SCHED\_FIFO}
  3. sched_priority=80(红牌号码)在 1 到 99 之间选一个数,越大越紧急。
    sched_priority=80\text{sched\_priority}=80
  4. 权限(挂牌资格)调用需要 root 或 CAP_SYS_NICE,否则会被内核拒绝。
    CAP_SYS_NICE\text{CAP\_SYS\_NICE}

FIFO运行策略

想象一位主刀医生:只要上了手术台,就必须把整台手术做完才下台。中途既不会因为“到点了”就换人,也不会因为来了个同级别的病人就停手。这种“一鼓作气”的风格,就是 SCHED_FIFO(First In First Out,先来先服务)的写照。

严格来说,FIFO 根本没有“时间片”这个概念。一个 FIFO 任务只要还能运行,就会一直霸占 CPU,直到发生以下三种情况之一:任务自己调用 sched_yield() 主动让出;任务阻塞,比如等待键盘输入或磁盘数据;来了一个更高优先级的任务,把它强行抢走。除此之外,没有任何机制会打断它。

用公式描述它“一口气能跑多久”:

FIFO 最适合那些“要么一次做完、要么出错”的紧要活儿。但凡事有两面:几个同样紧急的病人同时在场时,医生总抓着一个人不放,其他人会不会有意见?于是就有了第二套规则——SCHED_RR。

Trun=Tneed(直到  sched_yield, 阻塞或被更高优先级抢占)\displaystyle T_{run} = T_{need}\quad (\text{直到} \; sched\_yield,\ \text{阻塞或被更高优先级抢占})
一个 FIFO 任务的运行时长没有上限,唯一的“天敌”是更高优先级的抢占。所以 FIFO 任务必须写得短小精悍,否则就像占着手术台不走的医生,活活把别的病人拖死。
  1. T_run(实际连续运行时间)任务真正获得 CPU 的时间有多长。
    TrunT_{run}
  2. T_need(所需总时间)任务把活儿干完总共需要的执行时间。
    TneedT_{need}
  3. sched_yield(主动让出)任务自己举手说“医生,我暂时不看了”,把 CPU 让给下一位。
    sched_yield()\text{sched\_yield}()
  4. 抢占(被强行打断)更高优先级任务就绪,医生的注意力被强制转移。
    抢占\text{抢占}

RR轮转策略

SCHED_RR(Round Robin,时间片轮转)像门诊叫号:每位病人限时 3 分钟,不管看完没看完,到点必须起身换下一位。所有拿着同色牌子的病人在一个圈里轮流坐庄,谁也别想独占医生。

RR 与 FIFO 的唯一区别,就是给同优先级任务加了一个时间片。Linux 中 SCHED_RR 的默认时间片是 100ms,代码里可以用 sched_rr_get_interval() 查到实际值。时间片用完而任务还没结束,它就被挪到同优先级队列的末尾,等下一圈。注意一个关键点:低优先级任务不会因此受益——RR 只在同一个优先级组内轮转,红牌病人的圈子里不会出现绿牌病人。

时间片模型用公式表达就是:

RR 的价值是“雨露均沾”:几个同等紧急的任务,谁也别想独占医生。但急诊室还有最后一条铁律没讲——如果抢救进行到一半,突然推进来一个牌子更红的病人呢?

tslice=100 ms,轮转周期=n×tslice\displaystyle t_{slice} = 100\,\text{ms},\quad \text{轮转周期} = n \times t_{slice}
同一优先级下有 n 个任务,每个任务每隔 n×100ms 就能轮到一次。n 越大,两次被服务之间的等待就越久,就像候诊的人越多,叫号间隔越长。
  1. t_slice(时间片)每次轮到任务时,它最多能使用 CPU 的时长,Linux 默认 100ms。
    tslice=100 mst_{slice} = 100\,\text{ms}
  2. n(同组人数)同一优先级下处于就绪状态的任务数量。
    nn
  3. 轮转周期(一圈时长)n 个任务各拿一次时间片的总耗时。
    n×tslicen \times t_{slice}
  4. 查询接口用 sched_rr_get_interval() 查看当前系统的实际时间片。
    sched_rr_get_interval()\text{sched\_rr\_get\_interval}()

优先级抢占

急诊室的墙上贴着一条铁律:无论医生正在给谁看,只要推进来一位牌子更红的病人,医生必须立刻放下手里的一切冲过去。不许说“等我做完这台手术”,也不许说“等下一个整点”。这条铁律,在操作系统里就是“抢占”(preemption)。

抢占的条件只有一条:新就绪任务的优先级 > 当前运行任务的优先级。反过来也成立一条推论:相同优先级的任务之间互不抢占。FIFO 任务之间靠“自觉”——前面的做完或让出,后面的才能上;同组 RR 任务之间靠时间片来换手。谁也不会去抢平级同事的活儿。

抢占这个动作可以用逻辑式表示:

有了抢占铁律,“红牌优先”才真正落地。但这里还有一个隐藏问题:医生换人很快,可医生是怎么在几十个候诊病人里“一眼锁定”最红牌子的呢?答案就藏在急诊室后台的分诊台里。

Pnew>Pcur⇒保存现场,切换至新任务\displaystyle P_{new} > P_{cur} \quad \Rightarrow \quad \text{保存现场,切换至新任务}
更高优先级的任务一就绪,调度器立刻暂停当前任务,把它的寄存器、程序计数器等“手术记录”保存好,然后切到新任务。整个过程在微秒级完成,快到病人几乎感觉不到医生换过人。
  1. P_new(新病人牌号)新到达任务的优先级,相当于120刚送来的危重病人的红牌号。
    PnewP_{new}
  2. P_cur(当前病人牌号)正在运行任务的优先级。
    PcurP_{cur}
  3. 保存现场(记录手术进度)把当前任务的寄存器、栈指针等状态存档,相当于记录“刀切到第几层”。
    保存现场\text{保存现场}
  4. 上下文切换(换人动作)从存档恢复新任务的现场继续运行,这是抢占的主要开销。
    context switch\text{context switch}

运行队列切换

急诊室后台有一张巨大的分诊台,上面按红、黄、绿分区陈列着所有等待的病人。操作系统里这张台子叫运行队列(runqueue),每个 CPU 一张,专门维护“就绪但还没轮上”的实时任务。台子的设计直接决定了叫号的速度。

在 Linux 中,实时运行队列由“优先级位图 + 链表数组”构成:每个优先级对应一条队列,位图的每一位标记该队列是否为空。要找到下一个该谁上场,只需从最高位往下扫,找到第一个“非空”的位。这个操作只需一条 CPU 指令,复杂度是 O(1),与任务总数无关——无论候诊室挤了 10 个人还是 1000 个人,找最紧急病人的时间都一样。

查找动作的公式化表示:

分诊台的效率保证了“叫号”几乎不浪费时间。但理论归理论,真实系统里怎么给进程挂红牌、怎么配置调度策略?下一个环节,我们进入实操。

next=find_first_set_bit(rt_rq[ prio ])\displaystyle \text{next} = \text{find\_first\_set\_bit}\big(\text{rt\_rq}[\, \text{prio}\,]\big)
find_first_set_bit 是一条位运算指令,一次就能找到“当前最高优先级的非空队列”。这正是实时调度确定性的来源之一:找人的时间恒定,不会因为人多而卡顿。
  1. rt_rq(分诊台抽屉)实时运行队列数组,按下标 0~99 对应各个优先级。
    rt_rq\text{rt\_rq}
  2. prio(抽屉编号)把优先级当作数组下标,同牌号的病人排进同一个抽屉。
    prio\text{prio}
  3. find_first_set_bit(从高位找起)从最高优先级位往下找第一个为 1 的位,就是最紧急的非空队列。
    find_first_set_bit()\text{find\_first\_set\_bit}()
  4. O(1)(恒定时间)查找耗时与队列长度无关,始终是常数时间。
    O(1)O(1)

实践配置工具

挂红牌不是喊口号,得有趁手的工具。Linux 命令行里最常用的是 chrt 命令:chrt -f -p 80 1234 表示把 PID 为 1234 的进程设为 SCHED_FIFO、优先级 80;chrt -r -p 50 5678 则表示设为 SCHED_RR、优先级 50。一条命令,立竿见影。

在程序代码里设置实时属性,通常通过 pthread 线程属性完成。创建线程之前,先设置一个“挂号单模板”:模板上写好接诊规则和优先级,再把这套模板交给 pthread_create。设置动作发生在创建之前,创建出来的线程才是实时线程,这一点非常容易踩坑。

线程属性的设置过程:

工具和 API 是把调度理论搬进生产系统的“挂号窗口”。但窗口排对了,急诊室依然可能翻车——下一节我们看看最著名的几起“医疗事故”。

pthread_attr_setschedpolicy(&attr, SCHED_RR),pthread_attr_setschedparam(&attr, &param)\displaystyle \text{pthread\_attr\_setschedpolicy}(\&attr,\ \text{SCHED\_RR}),\quad\text{pthread\_attr\_setschedparam}(\&attr,\ \&param)
attr 就像一张空白挂号单,第一句勾选接诊规则为 SCHED_RR,第二句填上优先级数值。两张都填好后,创建线程时直接套用,新线程就成了挂着红牌的实时病人。
  1. attr(挂号单模板)线程属性对象,保存策略与优先级等配置。
    &attr\&attr
  2. SCHED_RR(勾选规则)第一句调用指定的调度策略。
    SCHED_RR\text{SCHED\_RR}
  3. param(优先级结构体)包含 sched_priority 字段,填 1~99。
    &param\&param
  4. 调用顺序(先挂号后就诊)必须在 pthread_create 之前设置,否则新线程不继承实时属性。
    pthread_create\text{pthread\_create}

风险与优化

急诊室也会翻车。最经典的翻车场面叫“优先级反转”:低优先级病人 A 手里紧紧攥着一把手术钳(锁),高优先级病人 B 正等着这把钳子做手术,中优先级病人 C 却不停来抢医生。结果 B 这个最该先救的,反而被 C 拖得最惨——这就是反转。

Linux 的经典解法是“优先级继承”:当 B 在等待 A 的锁时,A 临时升级到 B 的红牌号,先把手术做完交还钳子再恢复原级别。C 看到 A 的牌子变红,就不敢再来抢了。除了反转,还要警惕“饥饿”:一个 99 级的任务永远就绪,其他任务就永远饿着。这两种风险都是实时系统的“慢性病”。

反转场景可以浓缩成一行公式:

急诊室的智慧不是“见人就救”,而是有秩序地救。理解了 FIFO、RR 和抢占这三套规则的边界,你就能设计出真正可靠的实时系统。

PH>PM>PL,PL→继承PH\displaystyle P_H > P_M > P_L,\qquad P_L \xrightarrow{\text{继承}} P_H
H 在等 L 手里的锁,L 临时升到 H 的优先级,挡住 M 插队。等 L 释放锁后再恢复原本的低优先级。这是用“临时红牌”化解“绿牌拖垮红牌”的经典操作。
  1. P_H / P_M / P_L(三个牌号)高、中、低三个优先级,满足 P_H > P_M > P_L。
    PH>PM>PLP_H > P_M > P_L
  2. 反转(顺序倒挂)H 等锁期间 M 抢跑,导致 H 反而最后完成。
    反转\text{反转}
  3. 继承箭头(临时升级)L 临时继承 H 的优先级,压住 M。
    PL→继承PHP_L \xrightarrow{\text{继承}} P_H
  4. 设计建议(防患未然)实时任务保持短小,避免持锁阻塞,不在实时线程里做动态内存分配和打印。
    设计建议\text{设计建议}

AI 应用场景

音视频推流中的线程调度

阶段:系统部署与性能调优

解决的问题:直播推流要求采集线程每 33ms 拿到一帧画面,否则就会卡顿掉帧。传统 CFS 调度器按权重分配 CPU,后台的转码、日志任务可能随时抢走 CPU,造成采集延迟。把关键线程设为 SCHED_FIFO 后,采集任务一旦就绪就立即抢占,帧率稳定性大幅提升。

具体案例:某直播平台将摄像头采集线程设为 SCHED_FIFO 优先级 80,将编码线程设为 SCHED_RR 优先级 60。在线人数峰值时,丢帧率从 2.1% 降到 0.3%,弹幕区“画面卡成 PPT”的投诉基本消失。

自动驾驶控制闭环

阶段:实时推理链路优化

解决的问题:自动驾驶的感知→规划→控制链路必须在 100ms 内闭环,传感器融合线程稍有抖动,控制指令就会迟到,影响行车安全。把控制线程设为最高优先级 SCHED_FIFO,感知融合线程设为 SCHED_RR,就能保证关键路径的确定性。

具体案例:某自动驾驶团队在 ROS2 节点中,将控制节点绑定到独立 CPU 核并设为 SCHED_FIFO 优先级 90,传感器融合节点设为 SCHED_RR。实车测试中,控制指令的最大延迟从 38ms 降到 12ms,且抖动明显收敛。

高频量化交易低延迟引擎

阶段:延迟关键型系统优化

解决的问题:量化交易的订单撮合与风控检查,拼的是微秒级延迟。普通调度下,系统中断和其他进程会随机打断交易线程,产生“调度抖动”。将交易核心线程设为 SCHED_FIFO 并固定在专用 CPU 上,能消除大部分抖动。

具体案例:某量化私募把行情解析线程设为 SCHED_FIFO 优先级 99,订单执行线程设为 SCHED_RR 优先级 90,同时把这两个线程绑核。优化后盘中延迟的 P99 从 180μs 降到 95μs,抢单成功率明显提升。

Python 演示

python73 行
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""模拟 Linux 实时调度 SCHED_FIFO 与 SCHED_RR,并演示高优先级抢占。"""  class Task:    def __init__(self, name, priority, need, arrive=0):        self.name = name        self.priority = priority      # Linux 实时优先级 1~99,越大越紧急        self.need = need              # 还需的 CPU 时间(以时间片为单位)        self.arrive = arrive          # 到达(变为就绪)的时刻  def simulate(tasks, policy, quantum=3):    """逐步模拟调度,返回 [(时刻, 任务名), ...] 的切换序列。"""    state = {t.name: [t.need, t.priority, t.arrive] for t in tasks}    total = sum(t.need for t in tasks)    clock, served, cur, used = 0, 0, None, 0    timeline = []     def ready():        return sorted(            (n for n, d in state.items() if d[0] > 0 and d[2] <= clock),            key=lambda n: (-state[n][1], n))     while served < total:        pool = ready()        if not pool:            clock += 1            continue        nxt = pool[0]        if cur is None or nxt != cur:   # 切换(可能是高优先级抢占)            timeline.append((clock, nxt))            cur, used = nxt, 0        state[cur][0] -= 1        served += 1        clock += 1        used += 1        if policy == "RR" and state[cur][0] > 0 and used >= quantum:            same = [n for n in pool if state[n][1] == state[cur][1]]            if len(same) > 1:           # 同优先级内轮转到下一位                cur = same[(same.index(cur) + 1) % len(same)]                timeline.append((clock, cur))                used = 0        elif policy == "FIFO" and state[cur][0] == 0:            cur = None                  # 当前任务完成,重新挑选    return timeline  def show(title, timeline):    seg, start, buf = None, 0, []    for t, name in timeline:        if name != seg:            if seg is not None:                buf.append("%s@%d-%d" % (seg, start, t - 1))            start, seg = t, name    buf.append("%s@%d-%d" % (seg, start, timeline[-1][0]))    print(title + ": " + " -> ".join(buf))  def main():    tasks = [        Task("P1A", 95, 6),        Task("P1B", 95, 6),        # 与 P1A 同优先级,观察 RR 轮转        Task("P2", 50, 8),        Task("Alarm", 99, 3, 4),   # 第 4 时刻到达的危重任务,演示抢占    ]    show("SCHED_FIFO", simulate(tasks, "FIFO"))    show("SCHED_RR  ", simulate(tasks, "RR", quantum=3))  if __name__ == "__main__":    main()
目的:用一个小型调度模拟器,对比相同任务集在 SCHED_FIFO 与 SCHED_RR 下的运行序列,并演示高优先级任务到达时的抢占行为。
  1. key=lambda n: (-state[n][1], n)把就绪任务按实时优先级从高到低排序,同优先级按名字取稳定顺序,模拟分诊台“先挑最红牌子”。
  2. if cur is None or nxt != cur:发现候选人不是当前任务,就是一次调度切换;可能是正常换班,也可能是高优先级任务到达触发抢占。
  3. timeline.append((clock, nxt))记录每次切换的时刻与任务名,最后用 show() 输出分段运行序列。
  4. if policy == "RR" and state[cur][0] > 0 and used >= quantum:RR 的关键:时间片耗尽且任务没做完,就要在同优先级组内换人。
  5. same[(same.index(cur) + 1) % len(same)]在同优先级组里取“下一位”,构成轮转环。
  6. elif policy == "FIFO" and state[cur][0] == 0:FIFO 的关键:任务没做完就一直占用 CPU,只有做完了才重新挑选。

常见误区

误区“SCHED_FIFO 就是时间片很长的轮转吧?反正每个任务最后都能跑到,只是 FIFO 等待更久而已。”

正解FIFO 根本没有时间片。任务一旦获得 CPU,只有三种情况会离开:主动调用 sched_yield()、阻塞等待 I/O、或者被更高优先级任务抢占。把 FIFO 当成“长时间片 RR”,会低估它霸占 CPU 的风险——一个写坏的 FIFO 任务可以让整个系统“冻住”其他实时任务。

为何常见:“先来先服务”这个名字实在太像排队机制,很容易让人联想到时间片轮转,忽略了它“运行到完成”的本质。

误区“Linux 里优先级都是数值越小越高吧?nice 值 -20 最优先,那实时优先级应该也是 1 最紧急。”

正解实时优先级恰好反过来:1~99,数值越大越紧急,99 是最高。数值越小越优先的是普通进程的 nice 值(-20~19),它属于完全公平调度器 CFS 的规则。把 nice 的规则套到实时调度上,会写出“高优先级却是小数字”的错误配置,甚至可能让关键任务饿死。

为何常见:nice 值是大多数教程最早讲的优先级概念,“越小越优先”先入为主,很多人没有意识到实时优先级是另一套体系。

误区“用了 SCHED_RR,所有实时任务都会轮流获得 CPU,低优先级的也能定期跑一跑。”

正解RR 只在同一个优先级组内轮转。只要存在一个更高优先级的任务持续就绪,低优先级任务就永远不会轮到 CPU,这叫“饥饿”。RR 面向的是“多个同等紧急任务”的场景,比如三个同优先级的传感器线程按时间片轮流采集,而不是全员大锅饭。

为何常见:“轮转”(Round Robin)这个词太有迷惑性,让人误以为是一个包含所有任务的循环队列。

误区“把普通进程的 nice 调到 -20,优先级拉满,不就能获得实时效果了吗?”

正解nice 属于 CFS 调度类,只影响 CPU 分配权重,它既没有时间片保障,也没有抢占保证,更不存在截止期限的概念。实时必须显式使用 chrt 或 sched_setscheduler 指定 SCHED_FIFO/SCHED_RR 和 1~99 的实时优先级,并具备 root 或 CAP_SYS_NICE 权限。把 nice 当成万能旋钮,在实时场景下是无效的。

为何常见:nice 是接触 Linux 时最早学会的调优工具,“调到最小就最优先”的直觉容易被错误泛化到实时调度领域。

面试题

Q1. SCHED_FIFO 的同优先级任务之间如何调度?

单选题 · easy

  • A.所有任务轮流各跑一回合
  • B.先运行的任务运行完才换人← 正确答案
  • C.由 nice 值大小决定先后顺序
  • D.按随机顺序挑选下一个任务

答案:B。FIFO 的语义就是先来先做,同优先级之间没有时间片,前一个不主动让出或阻塞,后一个就一直等,就像急诊室同级别病人按挂号顺序一单一单地看。A 是 RR 的行为,C 是 CFS 的数值规则,D 毫无确定性可言。

Q2. Linux 实时优先级的取值范围和大小关系是?

单选题 · easy

  • A.0 到 139 数值越大越紧急
  • B.1 到 99 数值越大越紧急← 正确答案
  • C.-20 到 19 数值越小越优先
  • D.100 到 139 数值越小越优先

答案:B。实时任务用 1~99,数值越大优先级越高,99 是红牌中的红牌。选项 C 是 nice 值的规则,属于 CFS 范畴,方向恰好相反。A 和 D 的数字范围都是编造的,记住“RT 就是 1 到 99 越大越急”即可。

Q3. SCHED_RR 与 SCHED_FIFO 最核心的区别是什么?

单选题 · mid

  • A.RR 的实时优先级取值范围更大
  • B.RR 增加时间片,耗尽后同组换人← 正确答案
  • C.FIFO 不支持高优先级任务抢占
  • D.RR 只能用于普通进程的调度

答案:B。RR 可以理解为“FIFO + 时间片”:同优先级任务之间,时间片一到就排到队尾等下一轮。两者都支持高优先级抢占,优先级范围也完全相同,都是 1~99。所以 B 是对的,C 和 D 都是对 FIFO/RR 的错误描述。

Q4. 当更高优先级的实时任务变为就绪时,调度器会怎么做?

单选题 · mid

  • A.等当前任务主动让出 CPU 再上
  • B.立即抢占当前正在运行的进程← 正确答案
  • C.等当前时间片耗尽后再切换
  • D.把新任务放到队尾等待下一轮

答案:B。抢占是实时调度最核心的铁律:新任务优先级大于当前任务时,立即保存现场并切换,正如急诊室来了更红的牌子医生立刻放下手里的活。等让出、等时间片都是非实时的妥协行为,实时系统等不起。

Q5. 优先级反转的经典场景是哪个?

单选题 · hard

  • A.低优先级持锁中优先级抢跑拖住高优先级← 正确答案
  • B.高优先级任务一直运行饿死低优先级任务
  • C.两个任务互相等待对方释放自己所需锁
  • D.时间片设置过小导致频繁切换开销过大

答案:A。反转的核心是“顺序倒挂”:高优先级 H 等低优先级 L 的锁,中优先级 M 趁机抢跑,H 反而最后完成。B 是饥饿,C 是死锁,D 是时间片抖动,都不是反转。

Q6. 将进程设置为实时调度,需要满足哪些条件?

多选题 · mid

  • A.拥有 CAP_SYS_NICE 权限← 正确答案
  • B.指定合法的实时优先级 1 到 99← 正确答案
  • C.进程隶属于 root 用户组来运行
  • D.内核编译时必须关闭抢占功能

答案:AB。设置实时调度需要双重条件:一是权限(root 或 CAP_SYS_NICE),二是合法优先级(1~99)。拥有 CAP_SYS_NICE 的非 root 用户也可以设置,所以 C 说“必须属于 root 用户组”是错误的;抢占是内核的既有能力,D 与设置条件无关。

Q7. 关于“优先级继承”的正确描述是?

单选题 · mid

  • A.高优先级任务自动降低自己的优先级别
  • B.持锁任务临时继承等待者的优先级← 正确答案
  • C.所有实时任务的优先级最终都相同
  • D.内核关闭所有中断来保护临界资源锁

答案:B。优先级继承让低优先级持锁者临时升到等待者的高优先级,相当于给 L 发一张临时红牌,挡住 M 插队,用完锁再还原。A 和 C 方向完全反了,D 是关闭中断保护临界区的另一种做法但并非优先级继承。

Q8. 一个 SCHED_FIFO 任务离开 CPU 的合法途径有哪些?

多选题 · hard

  • A.主动调用 sched_yield 让出处理器← 正确答案
  • B.阻塞等待某个 I/O 操作完成返回← 正确答案
  • C.同优先级的任务被调度器选中运行
  • D.更高优先级的任务就绪立刻抢占← 正确答案

答案:ABD。FIFO 没有时间片,同优先级任务永远不会主动抢走 CPU,所以 C 不会发生。合法的离开途径只有三条:自己让出(sched_yield)、阻塞(等 I/O)、被更高优先级抢占,也就是 A、B、D。

Q9. Linux 实时运行队列查找下一个任务通常用什么技巧?

单选题 · mid

  • A.遍历链表逐个比较找出最大优先级
  • B.优先级位图配合 O(1) 位操作查找← 正确答案
  • C.红黑树按优先级排序后进行查找
  • D.哈希表将任务编号映射到优先级

答案:B。Linux 的实时运行队列把优先级映射到位图,用 find_first_set_bit 从最高位查找非空队列,一次指令完成,耗时恒定 O(1)。遍历链表是 O(n),红黑树是 O(log n),都不符合“确定性优先”的设计目标。

Q10. 编写实时线程时,哪些是推荐的防御性做法?

多选题 · extreme

  • A.避免在实时线程中动态申请内存← 正确答案
  • B.避免长时间持有锁阻塞实时任务← 正确答案
  • C.不在实时线程中打印日志到控制台← 正确答案
  • D.把全部业务逻辑都放进实时线程

答案:ABC。动态内存分配可能触发页错误和锁竞争,printf 会导致阻塞,长时间持锁会引发优先级反转,都会破坏实时线程的确定性。实时线程应该只保留最关键路径,D 正好相反——把全部逻辑塞进实时线程会让风险失控。