星宸科技 AE 岗面经:自研 RTOS 任务调度与优先级继承
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 请做一下自我介绍。
- 本科阶段的数学建模比赛做了什么?
- 你自己写的 RTOS 中,任务是怎么创建的?
- 任务怎么调度?
- 上下文切换怎么实现的?
- SysTick 是做什么的?
- PendSV 是什么?
- TCB 里放了什么?
- 互斥锁怎么实现的?
- 为什么要加优先级继承?
- 你的优先级继承是怎么实现的?
- 在什么时候提高优先级?
- 什么时候恢复优先级?
- 拿锁的任务是有等待还是没有等待?
- 有等待的是怎么实现的?
《参考解析》
任务创建、调度与上下文切换
创建任务通常做三件事:申请任务控制块和独立栈空间,按目标 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 驱动的延时链表扫描负责唤醒,并按超时分支返回失败——这也是「阻塞等待要带超时」的原因:没有超时,一次锁泄漏或死锁就会把任务永久挂死。把这条链路讲全(失败入队 → 阻塞 → 唤醒或超时 → 回到就绪),比单说一句「用信号量实现」能体现出你真的写过这个内核。