第二章 多核架构基础:SMP与AMP、共享内存与分布式内存、缓存一致性

各位同学,今天我们来聊聊多核架构的根基。说实话,这部分内容我当年刚接触时也觉得有点枯燥,但后来在实际项目中吃过亏,才明白这些基础概念有多重要。你想想看,如果连芯片内部怎么协同工作都搞不清楚,写出来的代码跑起来大概率会出问题。

2.1 对称多处理(SMP)与不对称多处理(AMP)

先说说SMP和AMP。这两个概念,说白了就是多核处理器之间怎么分工的问题。

2.1.1 对称多处理(SMP)

SMP架构下,所有核心都是平等的。每个核心都能访问所有内存和外设,操作系统把任务均匀分配到各个核心上。我最早接触的Intel Xeon平台就是典型的SMP架构。

SMP的核心特征:

  • 所有核心共享同一内存空间
  • 所有核心共享同一操作系统实例
  • 任务调度由操作系统统一管理
  • 核心间通过共享内存通信

我在项目中遇到过一个问题:某个网络处理任务在SMP系统上跑,流量一上来,CPU0的负载飙到90%,其他核心却闲着。为什么?因为操作系统不知道哪些数据是热点,调度策略不够智能。后来我们手动绑核,才把负载均衡了。

2.1.2 不对称多处理(AMP)

AMP架构就不同了。每个核心各司其职,有的跑控制面,有的跑数据面。我记得有个路由器项目,就是用AMP方案——一个核跑Linux做管理,另外几个核跑裸机程序做快速转发。

我的经验:AMP适合确定性要求高的场景。比如网络处理器中,转发面需要微秒级响应,控制面可以容忍毫秒级延迟。把它们分开,互不干扰。

AMP的缺点也很明显——编程复杂。你得自己管理核心间的通信,不像SMP那样有操作系统帮你搞定。我曾经在一个AMP项目上调试了整整两周,才发现是两个核的共享内存地址映射错了。

特性 SMP AMP
操作系统 单一OS实例 多个OS/裸机
编程难度 较低 较高
实时性 一般 优秀
负载均衡 自动 手动

2.2 共享内存与分布式内存模型

接下来聊聊内存模型。这玩意儿,嗯,直接决定了你的数据怎么在核心之间传递。

2.2.1 共享内存模型

共享内存模型下,所有核心看到的是同一个物理内存。一个核写数据,另一个核马上就能读到。听起来很美好,对吧?但问题来了——缓存一致性怎么保证?

我举个例子。假设Core0和Core1都在处理同一个网络报文描述符。Core0修改了描述符的状态位,Core1如果读的是自己缓存里的旧数据,那就出大问题了。

注意:共享内存不等于不需要同步。你仍然需要用锁、原子操作或者内存屏障来保证数据一致性。我曾经见过一个同事,以为共享内存就万事大吉了,结果线上出了数据错乱,排查了三天才发现是缺少内存屏障。

2.2.2 分布式内存模型

分布式内存模型就简单粗暴了——每个核心有自己的私有内存,核心之间通过消息传递来通信。这种模型在网络处理器中很常见,特别是那些追求极致性能的场景。

我参与过的一个NPU项目,每个处理引擎都有自己的本地SRAM,数据通过片上网络(NOC)传递。这样做的好处是:没有缓存一致性的烦恼,每个核只管自己的数据就行。

但代价呢?编程模型变了。你不能像共享内存那样直接读写,得显式地发送和接收消息。说白了,就是把复杂度从硬件转移到了软件。

2.3 缓存一致性协议

终于到了缓存一致性。这可能是多核编程中最让人头疼的问题之一。我刚开始做多核开发时,总觉得缓存一致性是硬件的事,跟我没关系。直到有一次,一个诡异的数据错乱bug让我连续加班一周……

2.3.1 MESI协议

MESI是最经典的缓存一致性协议。它定义了四种状态:Modified、Exclusive、Shared、Invalid。每个缓存行都处于其中一种状态。

MESI状态说明:

  • M (Modified):数据被修改,只在本核缓存中有效
  • E (Exclusive):数据未修改,只在本核缓存中
  • S (Shared):数据未修改,多个核都有副本
  • I (Invalid):数据无效,需要从内存或其他核获取

我建议你记住一个关键点:当某个核要写一个处于S状态的缓存行时,它必须先发送"读-修改"请求,让其他核的副本失效。这个过程叫"缓存一致性协议的事务"。

2.3.2 伪共享问题

伪共享(False Sharing)是个经典坑。两个核各自修改不同的变量,但这两个变量恰好在同一个缓存行里。结果呢?每次修改都触发缓存一致性协议,性能直接崩了。

我曾经在一个网络处理项目中遇到过。两个核分别处理收包和发包,各自维护一个计数器。这两个计数器在内存中紧挨着,结果性能只有预期的三分之一。后来我们用__attribute__((aligned(64)))把它们分开到不同缓存行,性能立马恢复正常。

// 伪共享的例子
struct counters {
    uint64_t rx_count;  // Core0 频繁修改
    uint64_t tx_count;  // Core1 频繁修改
} __attribute__((aligned(64)));

// 解决方案:用padding隔开
struct counters_fixed {
    uint64_t rx_count;
    char padding[56];  // 填充到64字节
    uint64_t tx_count;
} __attribute__((aligned(64)));

2.3.3 内存屏障

内存屏障(Memory Barrier)是另一个容易忽略的点。编译器或者CPU可能会重排指令,导致你期望的执行顺序和实际不一样。

我举个例子:Core0写了一个标志位,Core1看到标志位变了就去读数据。但如果写标志位的指令被重排到了写数据之前,Core1读到的是旧数据。这就是典型的"存储-加载"乱序问题。

我的建议:在涉及跨核通信时,该加屏障就加,别心疼那点性能开销。我曾经为了优化性能去掉了一个屏障,结果线上出了偶发性的数据错乱,排查成本远大于那点性能收益。

2.4 本章知识体系

下面这张图是我自己画的,把本章的核心知识点串起来了。你仔细看看,应该能对多核架构有个整体认识。

多核架构基础 - 知识体系 多核架构 对称多处理 (SMP) 非对称多处理 (AMP) 共享内存模型 分布式内存模型 缓存一致性协议 (MESI) 伪共享 (False Sharing) 内存屏障 (Memory Barrier) 缓存行对齐 (Cache Line)

这张图把SMP/AMP、内存模型、缓存一致性串起来了。你从顶层往下看,就能理解它们之间的关系。说白了,多核编程的难点就在于:既要利用多核的并行能力,又要处理好核心之间的数据一致性问题。

本章核心要点:

  • SMP适合通用场景,AMP适合确定性要求高的场景
  • 共享内存模型编程简单,但需要处理缓存一致性
  • 分布式内存模型避免了缓存一致性问题,但编程复杂
  • MESI协议是缓存一致性的基础,理解它才能写出正确的多核程序
  • 伪共享和内存屏障是实际开发中最容易踩的坑

好了,这一章的内容就到这里。记住我强调的那些坑,以后写多核程序时多留个心眼。下一章我们聊聊具体的多核编程模型和工具链,到时候我会分享更多实战经验。


公众号:蓝海资料掘金营,微信deep3321