3、系统内存布局:多操作系统(Android/QNX/RTOS)内存分区、静态分配与动态分配策略、共享内存机制

各位同学,今天我们来聊聊座舱系统里最核心、也最容易出问题的一块——内存布局

说实话,我刚开始做多系统座舱时,踩过不少坑。最惨的一次,QNX 和 Android 抢内存,导致中控屏黑屏,客户直接拍桌子。从那以后,我对内存布局就格外上心。

你想想看,一个座舱里同时跑着 Android(娱乐)、QNX(仪表/安全)、RTOS(MCU 控制),它们各自要多少内存?怎么分?怎么共享?搞不好就是系统崩溃的导火索。

3.1 多操作系统的内存分区策略

先说分区。我习惯把座舱的内存分成三大块:

  • 安全域(QNX/RTOS):仪表盘、ADAS 预警、车身控制。这部分要求绝对稳定,不能受娱乐系统影响。
  • 娱乐域(Android):导航、音乐、视频、游戏。这部分内存需求大,但可以容忍一定的抖动。
  • 共享域:用于跨系统通信的缓冲区,比如视频流、传感器数据。

分区方式有两种主流做法:

分区方式 说明 我见过的坑
硬件分区(TZC/MPU) 通过 TrustZone 或 MPU 物理隔离,QNX 和 Android 互不可见对方内存 配置错了,Android 直接访问 QNX 内存,系统秒挂
软件分区(Hypervisor) 通过虚拟机监视器分配固定内存给每个 Guest OS 动态调整困难,一旦分少了就得重启

我个人更推荐硬件分区 + 软件预留的组合。比如 QNX 独占 2GB 物理内存,Android 独占 4GB,中间留 512MB 作为共享池。这样安全域和娱乐域互不干扰。

重要原则:安全域的内存必须静态分配,且不能被娱乐域回收。这是底线。

3.2 静态分配 vs 动态分配策略

这里我要重点说说。很多刚入行的工程师喜欢「一刀切」——要么全静态,要么全动态。其实都不对。

3.2.1 静态分配:稳,但浪费

静态分配就是在系统启动时,把内存划好,谁也别动。比如 QNX 的仪表显示缓冲区,我习惯直接分配 64MB 固定空间。

// QNX 静态分配示例
// 在 buildfile 中预留 64MB 给仪表显示
[type=static]
/data/display_buffer = 0x80000000, 0x04000000

优点很明显:零碎片、零竞争、零延迟。缺点呢?浪费。你想想看,仪表显示平时只用 20MB,剩下 44MB 就空着,Android 那边却因为内存不够在杀后台。

我的经验:关键路径(仪表渲染、安全监控)用静态分配。非关键路径(第三方应用)用动态分配。

3.2.2 动态分配:灵活,但危险

动态分配就是按需申请、用完释放。Android 的 Dalvik/ART 堆就是典型。好处是内存利用率高,坏处是——碎片化

我曾经在一个项目里,Android 跑了一周后,系统明明还有 1GB 空闲内存,但就是分配不出一个 4MB 的连续缓冲区。为什么?碎片太多。

// Android 动态分配示例(JNI 层)
// 申请 4MB 连续内存用于视频解码
void* buf = malloc(4 * 1024 * 1024);
if (buf == NULL) {
    // 这里很容易失败,尤其是系统运行一段时间后
    // 我建议用 mmap 替代 malloc,减少碎片
}

注意:动态分配在高负载场景下(比如导航+音乐+视频同时运行),很容易触发 OOM(Out Of Memory)。我曾经遇到过 Android 直接杀掉前台应用,用户正在导航,突然没了。

3.2.3 我的混合策略

嗯,这里我直接给结论:

  • QNX/RTOS:90% 静态分配 + 10% 动态分配(仅用于临时任务)
  • Android:70% 动态分配 + 30% 静态预留(比如 SurfaceFlinger 的 BufferQueue 固定大小)
  • 共享内存:100% 静态分配(大小在启动时协商好)

3.3 共享内存机制

这是多系统通信的命脉。没有共享内存,Android 和 QNX 之间传数据就得走 Socket,延迟高得吓人。

3.3.1 共享内存的典型场景

我列举几个实际项目里用到的:

  • 视频流共享:Android 解码后的视频帧,通过共享内存传给 QNX 的仪表屏显示
  • 传感器数据:RTOS 采集的 CAN 信号(车速、转向角),通过共享内存传给 Android 做导航
  • 音频路由:Android 的媒体音频和 QNX 的导航提示音,通过共享内存混音

3.3.2 实现方式

目前主流方案有两种:

方案 原理 延迟 我踩过的坑
物理地址映射 QNX 和 Android 各自将同一段物理内存映射到自己的虚拟地址空间 极低(< 1us) Cache 一致性没处理好,数据读出来是脏的
Hypervisor 共享页 通过 Hypervisor 提供的共享内存通道 较低(< 10us) 配置复杂,不同 Hypervisor 接口不统一

我个人更倾向物理地址映射。虽然要自己处理 Cache 同步,但延迟最低,可控性最强。

// QNX 侧:共享内存初始化
// 物理地址 0x90000000,大小 16MB
shm_handle = shm_open("/shared_video", O_RDWR | O_CREAT, 0666);
ftruncate(shm_handle, 16 * 1024 * 1024);
shared_addr = mmap(0, 16 * 1024 * 1024, PROT_READ | PROT_WRITE, 
                   MAP_SHARED, shm_handle, 0);

// 注意:这里要手动刷新 Cache
// 我曾经忘了加 msync,结果 Android 写的数据 QNX 读不到
msync(shared_addr, 16 * 1024 * 1024, MS_SYNC);

关键点:共享内存必须使用 环形缓冲区(Ring Buffer)双缓冲(Double Buffer),避免读写冲突。我习惯用双缓冲 + 原子操作标志位,简单可靠。

3.3.3 避坑指南

我曾经在一个量产项目里,共享内存频繁出现数据错乱。排查了三天,最后发现是 Cache 一致性 的问题。

QNX 和 Android 的 CPU 可能共享 L2 Cache,也可能不共享。如果不共享,一个核写了数据,另一个核读到的可能是旧数据。解决方案:

  • 在写完后调用 cache_flush()
  • 在读之前调用 cache_invalidate()
  • 或者直接使用 非 Cache 内存(Non-cacheable),但性能会下降 30% 左右

警告:千万不要在共享内存里传递指针!QNX 和 Android 的虚拟地址空间不同,传指针等于传废纸。只传偏移量或索引。

3.4 总结一下

好了,这一章的内容就这些。我帮你理一下重点:

  1. 内存分区:安全域静态、娱乐域动态、共享域预留
  2. 分配策略:关键路径用静态,非关键路径用动态,混合使用最稳妥
  3. 共享内存:物理地址映射 + 双缓冲 + Cache 同步,缺一不可

下一章我会讲 内存带宽的测量与建模,到时候会教你怎么用 perf 和 trace 工具找出带宽瓶颈。嗯,那个也是实战干货,别错过。

小提示:如果你现在正在做多系统集成,建议先画一张内存布局图,标清楚每个域的大小、用途、分配方式。画完你就知道哪里可能出问题了。