4、软件架构的陷阱:软件分区不当导致故障传播,以及如何实施基于虚拟化或MPU的强隔离
各位做功能安全的同行,咱们今天聊一个很实在的话题——软件分区。
说实话,我在这个坑里栽过跟头。有一次,一个ASIL B的项目,明明每个模块都测得好好的,结果集成测试一跑,整个系统直接挂了。查了三天,最后发现是底层的一个内存越界,把安全关键任务的堆栈给踩了。你说冤不冤?
这就是典型的「分区不当导致故障传播」。你想想看,一个非安全模块的bug,居然能把安全功能给拖下水。这在功能安全里是绝对不能接受的。
为什么分区这么重要?
ISO 26262里有个核心原则——免于干扰。说白了,就是ASIL A的模块不能影响ASIL D的模块。怎么保证?靠分区。
我个人的理解是,分区就像给每个模块建了个独立的小房间。你在这个房间里怎么折腾都行,但不能跑到隔壁去搞破坏。如果房间之间没有墙,或者墙是纸糊的,那故障就会像病毒一样传播。
常见的分区陷阱有哪些?我列几个典型的:
- 共享内存不加保护——两个任务共用一个全局变量,一个写坏了,另一个直接读脏数据
- 堆栈溢出无检测——非安全任务把栈撑爆了,安全任务的栈空间被覆盖
- 中断优先级混乱——低安全等级的中断把高安全等级的中断给阻塞了
- 时间分区形同虚设——一个任务跑超时,导致其他任务没时间执行
嗯,这里要注意,很多团队觉得「我们代码写得小心点就行了」。我告诉你,这想法很危险。人都会犯错,代码审查再严格也防不住所有问题。我们需要的是架构层面的强制隔离。
两种主流的强隔离方案
目前业界常用的方案有两种:基于MPU的隔离和基于虚拟化的隔离。我分别说说我的经验。
1. 基于MPU的隔离
MPU(内存保护单元)是MCU上常见的硬件模块。它可以在运行时检查每个内存访问的合法性。说白了,就是给每个任务发一张「通行证」,只允许它访问特定的内存区域。
我在一个基于Cortex-R的ECU项目里用过这个方案。当时我们分了三个区:安全关键区(ASIL D)、非安全控制区(QM)、诊断区(ASIL B)。每个区都有自己的MPU配置。
代码实现大概是这样的:
/* 配置MPU区域 - 安全关键任务 */
void MPU_Config_SafetyTask(void) {
MPU->RNR = 0; // 区域0:安全任务代码区
MPU->RBAR = (uint32_t)&SafetyTask_CodeStart | MPU_REGION_VALID;
MPU->RASR = MPU_AP_PRIV_RW | MPU_REGION_SIZE_64KB | MPU_REGION_ENABLE;
MPU->RNR = 1; // 区域1:安全任务数据区
MPU->RBAR = (uint32_t)&SafetyTask_DataStart | MPU_REGION_VALID;
MPU->RASR = MPU_AP_PRIV_RW | MPU_REGION_SIZE_32KB | MPU_REGION_ENABLE;
/* 其他区域配置... */
MPU->CTRL = MPU_CTRL_ENABLE | MPU_CTRL_PRIVDEFENA;
}
这里有个关键点——MPU配置本身必须是受保护的。我曾经见过一个项目,MPU配置是在启动时一次性写死的,结果运行过程中被一个QM任务给改了。嗯,这相当于把门锁拆了。
2. 基于虚拟化的隔离
对于更复杂的系统,比如需要运行多个OS或者混合关键性系统,虚拟化是更好的选择。ARM的V8-M架构就支持虚拟化扩展。
虚拟化的好处是——每个分区运行在自己的虚拟机上,有独立的地址空间、中断控制器、定时器。一个虚拟机崩溃了,其他虚拟机完全不受影响。
我记得有个客户做ADAS域控制器,需要同时运行Linux(用于感知算法)和RTOS(用于控制执行)。Linux那边动不动就OOM,但RTOS这边必须7x24小时稳定。最后我们用了Xen或者KVM的轻量级虚拟化方案,把两个系统完全隔离开。
虚拟化配置的要点:
- 内存直通——安全关键分区直接分配物理内存,避免虚拟化开销
- 中断路由——安全分区的中断直接路由到对应的虚拟CPU
- 时间预算——给每个虚拟机分配固定的时间片,防止一个分区饿死其他分区
避坑指南:我曾经踩过的三个坑
第一个坑——MPU区域重叠。有一次我配置了两个MPU区域,一个覆盖了0x20000000-0x2000FFFF,另一个覆盖了0x20008000-0x2000FFFF。结果两个区域都生效了,权限冲突,系统直接进HardFault。后来我学乖了,每个区域之间留至少4KB的间隙。
第二个坑——中断向量表没隔离。虚拟化方案里,每个虚拟机都有自己的中断向量表。但我当时偷懒,让所有虚拟机共享同一个向量表。结果一个非安全虚拟机把中断向量给改了,安全虚拟机的中断全乱了。嗯,这个教训让我多加了两个星期的班。
第三个坑——时间分区没做看门狗。虚拟化方案里,每个分区有固定的时间片。但有个分区因为死循环占用了所有CPU时间,其他分区完全没机会运行。后来我给每个分区加了独立的硬件看门狗,超时就直接复位那个分区。
总结一下
软件分区不是可选项,而是功能安全的必选项。我个人习惯是,在架构设计阶段就把分区方案定下来,而不是等到编码阶段再补。
最后送大家一句话:隔离做得好,故障跑不了。别等到出了问题再后悔,那时候代价就大了。
核心要点回顾:
- 分区是为了实现「免于干扰」,防止故障跨模块传播
- MPU隔离适合MCU场景,配置简单但要注意权限锁定
- 虚拟化隔离适合复杂系统,隔离性强但配置复杂
- 无论哪种方案,都要考虑内存、中断、时间三个维度的隔离
- 启动阶段就要完成隔离配置,运行过程中不要修改