4. 多核调度基础:OS-Application、Core与Task的映射关系

好,咱们今天聊聊多核调度里最基础、也最容易搞混的一块——OS-Application、Core 和 Task 到底怎么映射的。

说实话,我刚接触 AUTOSAR 多核时,也被这堆概念绕晕过。OS-Application 是个啥?跟 Core 有啥区别?Task 又该绑到哪个核上?嗯,今天咱们就把这层窗户纸捅破。

4.1 三个核心概念,先理清楚

我个人习惯,学任何新东西前,先把概念定义搞明白。这三个东西,说白了就是三层不同的抽象:

  • OS-Application:逻辑上的“安全域”或“功能分区”。你可以把它理解成一个容器,里面装着 Task、ISR、Counter 这些 OS 对象。每个 OS-Application 有自己的内存保护、时间保护策略。
  • Core:物理上的 CPU 核心。比如 TC397 有 6 个核,那就是 6 个 Core。每个 Core 独立运行,有自己的寄存器、缓存。
  • Task:可调度的执行单元。说白了就是一段代码,OS 负责决定它什么时候跑、跑多久。

你想想看,这三者的关系像什么?我打个比方:

OS-Application 就像公司里的不同部门(研发部、测试部、生产部)。Core 就像不同的办公室(A 栋 3 楼、B 栋 5 楼)。Task 就像一个个员工,每个员工属于某个部门,平时在某个办公室干活。

关键点:一个 OS-Application 可以跨多个 Core,一个 Core 也可以运行多个 OS-Application 里的 Task。但一个 Task 只能属于一个 OS-Application,并且只能在一个 Core 上执行。

4.2 映射关系,一张表说清楚

我在项目中遇到过不少同事,把“Task 属于哪个 App”和“Task 跑在哪个核”混为一谈。其实这是两个完全不同的维度。

维度 定义 配置方式 典型场景
Task → OS-Application Task 归属哪个应用分区 OsTask 中指定 OsTaskOsApplicationRef 安全相关 Task 放在 Trusted App,非安全 Task 放在 Normal App
Task → Core Task 在哪个物理核上调度 OsTask 中指定 OsTaskCoreRef,或通过 OsApplicationCoreRef 间接指定 高实时 Task 绑在 Core0,后台任务绑在 Core1
OS-Application → Core App 可以关联一个或多个 Core OsApplication 中指定 OsApplicationCoreRef 一个 App 的 Task 可以分散到多个核上并行执行

嗯,这里要注意:OS-Application 本身不直接决定 Task 跑在哪个核。它只是定义了一个“允许范围”。最终 Task 的核归属,由 Task 自己的配置决定。

4.3 实际配置,代码说话

光说理论没意思,咱们看个实际例子。假设我们有个双核系统(Core0 和 Core1),需要配置两个 OS-Application:

  • App_Safety:安全相关,只允许跑在 Core0
  • App_Com:通信相关,可以跑在 Core0 和 Core1

对应的 XML 配置大概长这样:

<!-- 定义 OS-Application -->
<OsApplication>
  <ShortName>App_Safety</ShortName>
  <OsApplicationCoreRef>Core0</OsApplicationCoreRef>
  <Trusted>true</Trusted>
</OsApplication>

<OsApplication>
  <ShortName>App_Com</ShortName>
  <OsApplicationCoreRef>Core0</OsApplicationCoreRef>
  <OsApplicationCoreRef>Core1</OsApplicationCoreRef>
  <Trusted>false</Trusted>
</OsApplication>

<!-- 定义 Task,并指定归属 App 和 Core -->
<OsTask>
  <ShortName>Task_Safety_1ms</ShortName>
  <OsTaskOsApplicationRef>App_Safety</OsTaskOsApplicationRef>
  <OsTaskCoreRef>Core0</OsTaskCoreRef>  <!-- 显式指定 Core -->
  <Schedule>1</Schedule>  <!-- 1ms 周期 -->
</OsTask>

<OsTask>
  <ShortName>Task_Com_10ms</ShortName>
  <OsTaskOsApplicationRef>App_Com</OsTaskOsApplicationRef>
  <OsTaskCoreRef>Core0</OsTaskCoreRef>  <!-- 也可以绑在 Core1 -->
  <Schedule>10</Schedule>
</OsTask>

我的小技巧:如果 Task 没有显式指定 OsTaskCoreRef,OS 会从它所属的 OS-Application 关联的 Core 列表里选一个。但我不建议这么干——显式指定 Core 会让调度行为更可预测,调试时少掉头发。

4.4 避坑指南:我曾经踩过的三个坑

做多核项目这些年,我在映射关系上栽过好几次。分享出来,你们别重蹈覆辙。

坑一:Task 绑了 Core,但 App 没绑那个 Core

我曾经配置过一个 Task,显式指定了 OsTaskCoreRef = Core1,但它所属的 App 只关联了 Core0。结果编译不报错,运行时 Task 永远不执行。为什么?因为 OS 启动时,Core1 根本没加载那个 App 的代码和数据。

解决办法:Task 的 Core 必须在 App 的 Core 列表里。这是基本约束,但很容易忘。

坑二:多个 App 共享同一个 Core,时间保护失效

我记得有个项目,把安全 App 和普通 App 都绑在 Core0 上。安全 App 配了时间保护(比如 1ms 内必须执行完),但普通 App 里有个长 Task 占用了 CPU。结果安全 Task 超时,触发了保护机制,系统直接复位。

教训:如果要做严格的时间隔离,不同安全等级的 App 最好分到不同 Core 上。实在分不开,就得用 OS 的预算(Budget)机制来限制每个 App 的 CPU 占用。

坑三:跨核 Task 依赖,数据一致性翻车

这个跟映射关系也有关。我有一次把生产者 Task 放在 Core0,消费者 Task 放在 Core1,共享一个全局变量。没加任何保护机制,结果跑起来数据时对时错。后来才发现,两个核的缓存不一致,Core1 读到的可能是旧数据。

嗯,这个咱们后面章节会详细讲。这里先提个醒:跨核共享数据,必须用 Spinlock 或者 Ioc 机制,别偷懒。

4.5 映射策略,怎么选?

说了这么多,实际项目里到底怎么分配?我个人总结了几条经验:

  1. 高实时 Task 独占一个 Core:比如 1ms 的控制循环,最好绑在 Core0,别跟其他 Task 抢 CPU。
  2. 同功能域的 Task 放同一个 App:比如所有通信协议栈的 Task 放 App_Com,方便统一做内存保护。
  3. 跨核通信尽量少:如果两个 Task 频繁交换数据,最好放在同一个 Core 上。实在不行,用 Ioc 或者共享内存加锁。
  4. 利用 OS-Application 做故障隔离:一个 App 崩了,别影响其他 App。这需要硬件 MMU 或 MPU 支持。

重要提醒:映射关系不是配完就不动了。项目后期做性能调优时,经常需要调整 Task 的 Core 归属。所以设计初期,留好扩展性,别把配置写死。

4.6 小结

好,今天的内容就这些。总结一下:

  • OS-Application 是逻辑分区,Core 是物理核心,Task 是执行单元。
  • Task 必须属于一个 App,并且只能在一个 Core 上运行。
  • App 可以跨多个 Core,但 Task 的 Core 必须在 App 的 Core 列表里。
  • 配置时显式指定 Task 的 Core,别依赖隐式继承。
  • 跨核数据一致性是个大坑,后面章节会深入讲。

下一章,咱们聊聊多核启动流程——Core0 怎么把其他核叫醒,每个核初始化时要注意什么。到时候见。