2、多核处理器基础:SMP与AMP、缓存一致性、内存屏障、NUMA架构简介
好,咱们进入第二章。多核处理器,这词儿你肯定听过无数遍了。但说真的,在QNX这种硬实时系统里,多核不是用来跑分的,是用来保命的。我见过太多人把Linux上的多核编程经验直接搬到QNX上,结果系统跑着跑着就崩了。嗯,咱们今天就把这些基础概念掰扯清楚。
2.1 对称多处理(SMP) vs 非对称多处理(AMP)
先说说这两个最基础的概念。说白了,就是CPU核心之间怎么分工。
SMP(对称多处理):所有核心地位平等。操作系统统一管理,哪个核心闲着,任务就扔给谁。QNX默认就是SMP模式。你写一个pthread_create(),系统自动给你分配到某个核心上跑。
AMP(非对称多处理):每个核心跑独立的操作系统,或者同一个OS的不同实例。核心之间各管各的,通过共享内存或硬件通道通信。
我个人习惯:做汽车仪表盘这种强实时、高可靠的项目,我倾向于用AMP。把安全相关的任务锁在一个核心上,不让其他任务干扰它。但代价是开发复杂度高,你得手动管理核心间的通信。
| 特性 | SMP | AMP |
|---|---|---|
| 操作系统 | 单一OS实例 | 多个OS实例或裸机 |
| 任务调度 | 系统自动负载均衡 | 开发者手动分配 |
| 实时性 | 受其他核心干扰 | 可做到硬实时隔离 |
| 开发难度 | 低,像单核编程 | 高,需处理核间通信 |
| 典型场景 | 通用计算、安卓 | 基站、汽车域控制器 |
你想想看,QNX牛在哪儿?它支持混合模式。你可以让4个核心跑SMP,另外2个核心跑AMP。这在其他RTOS里很少见。我在做ADAS项目时就这么干过——3个核心跑SMP处理感知算法,1个核心跑AMP专门做决策控制,确保刹车指令永远不被延迟。
2.2 缓存一致性——多核编程的第一道坎
为什么多核编程比单核难?核心原因之一就是缓存一致性。
每个CPU核心都有自己的L1/L2缓存。核心A修改了变量x,但修改后的值还躺在A的缓存里,没写回内存。核心B去读x,读到的还是老数据。这就出问题了。
硬件上,ARM和x86都提供了缓存一致性协议(比如MESI协议)。但协议不是万能的。我记得有一次,一个同事在QNX上写共享变量,没加任何保护,结果两个核心轮流写同一个变量,数据乱得一塌糊涂。他跑来问我:“不是有缓存一致性吗?”
注意:缓存一致性保证的是单个内存地址的最终一致性,不是操作顺序的一致性。两个核心同时写同一个变量,硬件只能保证最后谁写进去,但不能保证你的程序逻辑正确。
所以,多核共享数据,该加锁还得加锁。QNX提供了pthread_mutex、spinlock、rwlock这些机制。别偷懒。
2.3 内存屏障——看不见的指令重排
这个坑,我踩过不止一次。编译器为了优化,会重排你的指令顺序。CPU为了乱序执行,也会重排。单核下没问题,多核下就炸了。
举个例子:
// 核心A
data = 42;
flag = 1;
// 核心B
while (flag == 0);
print(data); // 可能打印出0,而不是42!
为什么会这样?因为核心A的写操作可能被重排——flag先被写回内存,data还在缓存里。核心B看到flag为1,就认为data也准备好了,结果读到了旧值。
解决办法就是内存屏障。在QNX里,你可以用:
__sync_synchronize()—— GCC内置的全屏障atomic_thread_fence()—— C11标准InterlockedXxx—— QNX提供的原子操作
我的建议:能用原子操作就别自己写内存屏障。原子操作内部已经包含了必要的屏障。自己写屏障,十有八九会写错。我曾经在一个网络驱动里手写了几个屏障,结果跑了一周才复现出bug,查了三天才发现是屏障位置不对。
2.4 NUMA架构简介——别让你的数据跑太远
NUMA(非统一内存访问)是服务器级多核系统的标配。简单说:每个CPU有自己的本地内存,访问本地内存快,访问远端内存慢。
在QNX里,如果你跑在NUMA机器上(比如某些ARM服务器或x86多路系统),要注意:
- 内存分配策略:默认是“本地分配”,即哪个核心申请内存,就从该核心的本地内存分配。
- 线程迁移:如果一个线程从核心A迁移到核心B,它之前分配的内存还在A的本地内存里。访问这些内存就会变慢。
我记得有一次做视频编解码优化,发现性能忽高忽低。查了半天,原来是QNX的线程被迁移到了另一个NUMA节点上,访问内存的延迟从80ns飙到了300ns。解决办法是绑定线程到特定核心:
// 绑定当前线程到核心0和1
int cpuset = (1 << 0) | (1 << 1);
ThreadCtl(_NTO_TCTL_RUNMASK, &cpuset);
避坑指南:我曾经在NUMA系统上做性能测试,发现同样的代码,跑在不同核心上,性能差了一倍。后来用QNX的showmem命令一看,内存分配在远端节点。从那以后,我养成了习惯——做性能测试前,先用numactl --hardware看看拓扑结构。
2.5 小结
这一章内容不少,但都是基础中的基础。总结一下:
- SMP:简单,但核心间互相干扰。适合通用场景。
- AMP:复杂,但隔离性好。适合硬实时场景。
- 缓存一致性:硬件帮你兜底,但别指望它解决所有并发问题。
- 内存屏障:多核编程的必修课。能用原子操作就别自己写。
- NUMA:关注内存访问延迟,绑定线程到合适核心。
下一章,咱们会深入QNX的任务调度器,看看怎么给不同任务分配不同的调度策略。到时候我会分享一个真实项目里的调度参数调优案例,保证让你有收获。