面灵AI→

星宸科技 AE 岗面经:自研 RTOS 任务调度与优先级继承

时间
2026-10
来源
牛客网

《面试题目》

  1. 请做一下自我介绍。
  2. 本科阶段的数学建模比赛做了什么?
  3. 你自己写的 RTOS 中,任务是怎么创建的?
  4. 任务怎么调度?
  5. 上下文切换怎么实现的?
  6. SysTick 是做什么的?
  7. PendSV 是什么?
  8. TCB 里放了什么?
  9. 互斥锁怎么实现的?
  10. 为什么要加优先级继承?
  11. 你的优先级继承是怎么实现的?
  12. 在什么时候提高优先级?
  13. 什么时候恢复优先级?
  14. 拿锁的任务是有等待还是没有等待?
  15. 有等待的是怎么实现的?

《参考解析》

任务创建、调度与上下文切换

创建任务通常做三件事:申请任务控制块和独立栈空间,按目标 CPU 的异常/调用约定在栈顶伪造一份「刚被抢占过」的现场(返回地址指向任务入口、状态寄存器给初始值),再把 TCB 挂进就绪链表并从栈顶位置记录好 pxTopOfStack;这样第一次切过去时,恢复现场的代码就等价于「从入口函数开始跑」。调度就是「在就绪集合里挑出优先级最高的那一个」:常见做法是按优先级建就绪链表、再用一个位图记录哪些优先级非空,查最高优先级就是找位图里最低的那一位,与任务数无关;同优先级再按时间片轮转。上下文切换分两步理解最清楚——保存是把当前任务用到的寄存器按约定压进它自己的栈(Cortex-M 上硬件自动压 R0–R3、R12、LR、PC、xPSR,软件只需补压 R4–R11),恢复是切到新任务的栈指针再按相反顺序弹出。关键设计是这一步交给 PendSV 做:它优先级最低、可挂起,于是所有中断都能先跑完,切换被推迟到最后一个异常退出时才发生,避免在中断服务程序里做切换带来的嵌套与竞态。SysTick 是内核的周期性节拍源,负责给延时、超时和时间片计数提供时间基准,它自己到期后置起 PendSV,真正的切换仍在 PendSV 里完成。

TCB 里到底该放什么

TCB 是「一个任务的全部身份」,至少要放:栈顶指针(切换时最先读、最后写的那一项)、任务状态、优先级与原始优先级(优先级继承要改的正是前者、恢复靠后者)、就绪/事件/延时链表的节点(同一个 TCB 常被同时挂在多条链表上,所以链表节点要能共存而不是复用同一个字段)、等待的事件或信号量指针、栈的起止地址(做栈溢出检测和高水位统计)、任务名等调试信息,以及一句上下文相关的记录——它当前持有哪些互斥锁。回答这题不要只背字段名,讲清「切换路径上必须最快拿到栈顶指针」和「等待与超时要求 TCB 能同时属于多条链表」这两条约束,比罗列更有说服力。顺便要说清就绪链表与延时链表的区别:前者是「随时可被调度」,后者是「在等到期或事件到来」,任务阻塞就是在两者之间搬移。

互斥锁与优先级继承

互斥锁本质上是一个「初始可用、持有者为 1 / 空为 0」的二值信号量,外加归属者信息。它的难点不在加解锁本身,而在优先级翻转:低优先级任务持锁时被一个中优先级任务抢占,高优先级任务来抢同一把锁只能排队等,于是高优先级被中优先级无限期地挡住。优先级继承就是解这个问题的:持锁者被临时提升到「当前正在等这把锁的最高优先级」,把中优先级任务挤下去,让它尽快跑完、尽快放锁。实现上,获取锁失败时把当前任务挂到该锁的等待列表并做阻塞,同时把持锁任务的优先级提到请求者的优先级(若更高),并触发一次重调度;恢复的时机是释放锁的那一刻——把优先级降回它自己的原始优先级,而不是简单降一级。这里有个容易答错的细节:如果持锁者同时持有几把被更高优先级任务等待的锁,恢复时应该取所有等待者里最高的那个,或者按原来继承的优先级逐把回退。另外「拿锁的任务有没有等待」要分开说:第一次拿锁是不等待的(CAS/关中断判断一下计数即可),失败才进入阻塞等待——正因为它会真正阻塞而不是自旋,优先级继承才有意义,否则持锁者的优先级改了也不会让 CPU 更早地转给它。

阻塞等待是怎么实现的

等待不是死循环,而是「从就绪集合里摘掉、挂到锁的等待队列上、状态置为阻塞」,同时把 TCB 挂进延时链表登记一个超时时间,然后主动触发一次调度让出 CPU。等持锁者释放锁时,从等待队列里挑优先级最高的那个(优先级继承保证了这个值已经被记账)、把它从延时链表摘掉、清超时、重新放回就绪链表,必要时立刻抢占当前任务。超时到点则由 SysTick 驱动的延时链表扫描负责唤醒,并按超时分支返回失败——这也是「阻塞等待要带超时」的原因:没有超时,一次锁泄漏或死锁就会把任务永久挂死。把这条链路讲全(失败入队 → 阻塞 → 唤醒或超时 → 回到就绪),比单说一句「用信号量实现」能体现出你真的写过这个内核。