3. 任务模型与状态机:任务状态与切换机制

好,咱们今天聊聊多核调度里最基础、也最绕不开的一个话题——任务状态机。

说实话,我刚开始接触AUTOSAR那会儿,觉得状态机这东西不就是几个状态来回跳嘛,有啥好研究的?后来真正做项目才发现,状态定义得清不清晰,直接决定了你的调度器稳不稳。尤其是多核环境下,一个任务在哪个核上跑、它现在能不能被抢断、它是不是在等某个资源——这些信息全得靠状态机来管理。

3.1 任务的四个核心状态

在AUTOSAR的OS规范里,任务一共就四个状态。不多不少,刚刚好。我个人的习惯是,把这四个状态想象成一个十字路口:

  • Running(运行态):CPU正在执行这个任务的代码。说白了,就是任务真正在干活。
  • Ready(就绪态):任务啥都准备好了,就等CPU给它分配时间片。它不抢、不闹、乖乖排队。
  • Waiting(等待态):任务因为某个条件没满足,暂时歇着了。比如等一个信号量、等一个事件、或者等一个I/O操作完成。
  • Suspended(挂起态):任务还没被激活,或者已经被终止了。它不在调度队列里,也不参与任何竞争。

关键点:多核环境下,每个核都有自己的Ready队列。但Waiting和Suspended状态是全局的——一个任务在核0上等信号量,核1上的调度器也得知道这事儿。

3.2 状态之间的转换路径

状态不是随便跳的。我见过不少新手画状态图,箭头满天飞,结果代码里一堆bug。记住,合法的转换只有这么几条:

当前状态 触发事件 下一状态 说明
Suspended ActivateTask() Ready 任务被激活,进入就绪队列
Ready 调度器选中 Running 获得CPU使用权
Running 被抢占 Ready 更高优先级任务来了,让出CPU
Running 等待资源 Waiting 比如调用WaitEvent()或获取信号量失败
Waiting 资源就绪 Ready 事件到达或信号量释放
Running 任务执行完毕 Suspended 调用TerminateTask()或ChainTask()

嗯,这里要注意:从Waiting不能直接回到Running。必须经过Ready,重新参与调度。为什么?你想想看,如果Waiting直接切回Running,那调度器就失去了对CPU分配的控制权。这在多核系统里是灾难性的——两个核可能同时认为同一个任务在Running。

我曾经踩过的坑:有一次做TC397的项目,我在中断服务函数里直接调用了SetEvent(),然后想当然地以为任务会立刻恢复执行。结果呢?任务确实从Waiting变成了Ready,但因为没有触发调度点,它一直待在Ready队列里,直到下一个tick才被调度。后来我养成了一个习惯:在释放资源后,显式调用Schedule()或者设置一个调度点。

3.3 任务切换机制——到底谁说了算?

任务切换,说白了就是保存当前任务的上下文,然后加载下一个任务的上下文。这事儿在单核上已经够复杂了,到了多核上,更是要命。

我个人的理解是,任务切换分两种:

  • 协作式切换:任务主动让出CPU。比如调用Schedule()、WaitEvent()、或者任务执行完毕。这种方式的优点是可控,缺点是如果某个任务死循环了,整个系统就挂了。
  • 抢占式切换:由外部中断(比如OS tick)触发。调度器在中断返回前检查是否需要切换任务。这种方式响应快,但需要处理好临界区保护。

在AUTOSAR里,我们通常用抢占式。为什么?因为汽车电子对实时性要求太高了。你想想看,一个刹车信号来了,你还等当前任务主动让出CPU?黄花菜都凉了。

3.4 多核环境下的切换细节

好,重点来了。多核环境下,任务切换不再是「一个调度器管所有」那么简单。每个核都有自己的调度器实例。但任务状态是全局共享的。

举个例子:

// 伪代码:多核任务切换示意
// 核0正在运行TaskA,核1正在运行TaskB

// 假设TaskC(优先级最高)被激活
// 核0的调度器发现TaskC优先级高于TaskA
// 核0执行:
//   1. 保存TaskA的上下文到TaskA的栈
//   2. 将TaskA状态从Running改为Ready
//   3. 将TaskC状态从Ready改为Running
//   4. 加载TaskC的上下文
//   5. 开始执行TaskC

// 注意:核1完全不受影响,继续跑它的TaskB

这里有个隐藏问题:任务亲和性。如果TaskC被配置为只能跑在核1上,那核0就不能抢这个任务。核0只能干瞪眼,等核1自己调度。我在项目中就遇到过这种配置错误——把高优先级任务绑在了负载已经爆满的核上,结果低优先级的核闲着,高优先级的核忙死。

我的建议:在设计阶段,先用表格把每个任务的优先级、周期、亲和性列清楚。别等到集成测试了才发现任务分配不合理。我一般会画一个「核-任务」矩阵,横轴是核,纵轴是任务,交叉点标注优先级和状态。一目了然。

3.5 状态机实现的几个关键点

最后,聊几个实现层面的细节。这些是我在实际写代码时总结出来的:

  1. 状态切换必须是原子操作。多核环境下,两个核可能同时修改同一个任务的状态。不加锁?等着数据竞争吧。我习惯用硬件比较并交换指令(CAS)来实现。
  2. Waiting状态的超时处理。任务不能无限期等下去。AUTOSAR里每个等待调用都有超时参数。超时后任务回到Ready状态,并返回一个超时错误码。这个机制救过我一次——某个外设挂了,信号量永远释放不了,要不是超时机制,整个系统就死锁了。
  3. Suspended状态的资源清理。任务进入Suspended时,它占用的栈空间、寄存器内容要不要清?我的建议是:不清。因为下次激活时还要重新初始化,清了反而浪费性能。但要注意,如果任务动态申请了内存,必须在TerminateTask()之前释放,否则就内存泄漏了。

好了,任务状态机这部分就聊到这儿。说白了,状态机就是调度器的「交通规则」。规则定好了,车(任务)才能跑得顺。下一节咱们聊聊具体的调度算法——优先级怎么定、时间片怎么分、多核之间怎么协同。到时候我会拿一个实际项目里的调度配置出来,咱们一起分析分析。