
1. AMP架构是什么从一个真实项目说起先聊一个我早些年做的实际项目。一颗SoC里边集成了两个Cortex-A7核和一个Cortex-M4核跑业务系统用Linux跑实时控制用裸机程序。最开始的想法很简单A7上跑应用M4上跑中断密集的电机控制两边各干各的互不干扰。但真正联调起来才发现两个核之间如果不通信整个系统就是一堆各自为战的孤岛——A7不知道M4当前在跑什么控制任务M4不知道A7什么时候给它下发新的目标参数。最后我们决定在AMPAsymmetric Multi-Processing非对称多处理架构下搭建一条完整的核间通信链路使用的核心手段正是RPMSGRemote Processor Messaging和共享内存。如果你也在做异构多核开发大概率会遇到跟我一样的困惑AMP架构和SMPSymmetric Multi-Processing到底什么关系为什么Linux和RTOS组合时直接把一块内存划给对方用还不够RPMSG消息框架和内核自带的sockt机制有什么区别这一篇我把实践过程中踩过的坑、梳理过的原理、以及最终跑通的方案做一个比较完整的整理。适合正在做异构多核、AMP架构或者刚接触RPMSG和共享内存的嵌入式开发者。先明确一下AMP架构的定位在同一个芯片上有多个处理器核心每个核心或核心组运行各自独立的操作系统可能是多个Linux也可能是Linux搭配RTOS或裸机每个核心通常跑应用场景不同、实时性要求不同的任务。和SMP最大的区别在于AMP下核心之间不是共享一个内核调度视图而是各自独立运行。所谓“非对称”指的恰恰是这种职责分工的不对称性并不单纯是硬件能力的不对等。在AMP系统里核间通信IPCInter-Processor Communication是一条命脉。没有IPC多核就是摆设。IPC的载体最常见的两种就是共享内存和基于共享内存抽象出的消息框架。我这次重点讲RPMSG与共享内存也是因为这是当前AMP实践中最常用、最通用的组合。2. 为什么AMP核间通信首选共享内存原理与边界2.1 共享内存的基本模型共享内存从名字就能看出来是把一段物理内存区域同时映射到多个CPU核心的地址空间中。在同一个SoC内部所有核心通常都挂在同一套总线或互联矩阵上天然具备访问同一段物理内存的能力所以“共享”在硬件上并不费劲。但是这里有一个关键细节AMP架构下两个核心运行两个独立的操作系统或裸机它们对内存的管理方式完全不同。Linux侧有完整的MMU和虚拟地址空间M4侧可能根本没有MMU直接物理寻址。于是“共享一段内存”实际要做的事情是选定一段固定物理地址的内存区域在Linux侧的设备树或内存映射表中把这段物理内存保留出来不让内核正常的内存管理子系统占用它在M4侧把同一个物理地址对应到M4可见的地址空间有时需要做地址偏移换算两边约定好这一段内存里数据结构怎么排布比如哪一块放数据、哪一块放控制信息这个过程听起来简单但边界很多。最常见的一个问题就是缓存一致性。现代ARM处理器普遍带有较复杂的缓存层次如果一个核写入了数据但数据还停留在自己的Cache里另一个核直接读内存就会读到旧值。解决方式有几种一种是硬件层面存在一致性互连比如某些SoC对特定内存区域做强一致属性另一种就是软件层面在关键点做Cache的clean和invalid操作。xilinx的Zynq平台在AMP开发里反复强调用OCMOn-Chip Memory或在设备树里给DDR区域标注noallocate、non-cacheable就是这个原因。2.2 共享内存与串行链路/总线外设的对比也有工程师会问为什么不用串口、SPI、以太网这类外设做核间通信从功能上讲这些外设确实可以在核与核之间传递数据早年也真有人这么干过比如用UART在两个核心之间传业务报文。但实际做项目下来这类方案有很明显的短板串口波特率上限摆在那常用的1.5Mbps或3Mbps级别传几个字节控制指令还可以一传大量数据就完全不够看。外设链路往往需要协议封装例如串口有帧格式、以太网有TCP/IP栈额外的协议开销在实时控制场景中是不可接受的。有些外设需要经过操作系统驱动栈中断延迟和调度延迟叠加之后核间通信延迟可能到毫秒级对于电机控制、飞控这类环路这是灾难级的。共享内存则完全绕开了这些瓶颈。它的带宽只受内存总线和处理器访存能力的限制大批量生产数据搬运时可以达到几百MB甚至上GB的吞吐量而单向通信延迟可以做到微秒级别甚至不到微秒——前提是不要引入过重的高层协议。但反过来共享内存并不是银弹。它本身不提供任何“消息边界”“优先级”“阻塞/非阻塞”之类的语义。给两个核共用一大块裸内存如果双方没有约定好数据结构和访问时序那和两个人同时在一个黑板上写字没什么区别写乱了谁也不知道。于是RPMSG出现了。它做的事情是在共享内存之上抽象出一套简洁而相对完整的消息传递语义让开发者不用处理那些和业务无关的竞态与同步细节。3. RPMSG框架到底做了什么从欢迎使用到channel建立3.1 什么是RPMSGRPMSGRemote Processor Messaging最初是TI在OMAP和Keystone系列多核处理器上推行的核间通信协议后来在OpenAMPOpen Asymmetric Multi-Processing项目中获得了非常广泛的应用。它的核心模型是将一个远程处理器remote processor看作一个可以通信的对端宿主侧通常是Linux可以创建多个逻辑通道channel每个通道上可以收发消息。一句话概括RPMSG的设计思路它采用一种轻量级的、基于共享内存的“邮箱环形缓冲区”机制不需要复杂的物理中断线作为消息通知的唯一途径。实际使用中RPMSG消息的收发通常还需要借助IPIInter-Processor Interrupt来触发接收端及时处理。中断在这里扮演的角色是“通知你去取快递”而共享内存中的队列才是“放快递的货架”。有的平台上RPMSG被封装成内核里的rpmsg总线设备驱动开发时你把它当成类似spi、i2c那样的总线接口来用在RTOS或裸机侧OpenAMP又提供了相应的库比如libmetal加open-amp的配合。所以这并不仅仅是一个框架还是一种跨系统边界的“协议契约”。3.2 virtio、vring与buffer的关系谈到RPMSG实现细节绕不开virtio/vring。virtio是虚拟IO设备的标准它定义了设备与驱动之间通过共享内存上的环形队列进行批量数据传输的方法。RPMSG正是借助这一套virtio机制来管理共享内存的。三者之间的关系比较像下边这样共享内存底层真正的物理内存区域横跨在通信双方的地址空间中。vringvirtio规范里的描述符环形队列存放缓冲区的描述信息。发送方把数据放进一个buffer再把这个buffer的地址和长度写入vring接收方从vring里拿到描述信息再去对应的地址读取数据。buffer真正承载消息内容的负载区域由一组固定大小或可变大小的内存块构成每个消息占用一块或者多块。我自己在第一次接触这些概念时被“三个环节各管一摊”搞得有点晕。后来用一套比喻理清了思路共享内存像一栋大楼vring是这栋楼里的快递柜屏幕上面记录了哪一格放了包裹buffer是实际的一个个储物格。发件人放好包裹在快递柜屏幕上登记一下然后按铃IPI中断通知收件人收件人看到屏幕登记按编号去取包裹。这一层抽象看着多了一层“绕路”实际上是必要的。如果没有vring沟通双方就必须自己约定“到底这次收发用哪一个地址、长度是多少”每一个消息都要建立一个同步流程。而有了vring底层收发包的逻辑被归一化了消息可以流水线式地收发。3.3 RPMSG的端点endpoint与名字服务RPMSG里还有一个很点睛的设计叫endpoint也就是端点。一个远程处理器可以对应多个逻辑通信点就像一台服务器可以开很多服务端口。Linux侧可以创建多个rpmsg设备节点分别绑定到不同的远端端点。这样一来在一个物理链路上可以跑多种业务互不干扰。端点的建立早期依赖一种名为name service的机制。简而言之当远端处理器比如M4核有一个新的服务要开放时它会先通过RPMSG向宿主机发一条特殊消息内容类似“我叫rpmsg-ctrl我空闲来连接我吧”宿主机收到后创建对应的端点和设备节点然后双方建立正式的连接。整个过程看起来像是两边在共享内存上聊了个天先打个招呼再开正题。这个机制在实际项目里非常好用。比如我在M4侧实现了两个模块一个是电机参数反馈一个是温度采集通过name service分别注册成两个通道Linux侧启动后就自动看到两个对应的设备节点不需要人为指定哪个通道对应哪个功能。4. 实操共享内存划分与RPMSG环境搭建全流程4.1 物理内存划分的实践经验先说一下平台背景某款基于Cortex-A53四核加Cortex-M4双核的SoCA53侧跑LinuxM4侧跑基于FreeRTOS的裸机应用。DDR大小是1GB我们给Linux留了大部分M4侧专门在DDR尾部划出64MB作为M4的代码、数据以及核间共享内存池。在Linux侧设备树里需要将这段区域标记为reserved-memory。常见的写法如下这里给出一个简单的参考片段reserved-memory { #address-cells 2; #size-cells 2; ranges; m4_reserved: m440000000 { compatible shared-dma-pool; reg 0x0 0x40000000 0x0 0x04000000; no-map; }; rpmsg_reserved: rpmsg44000000 { compatible shared-dma-pool; reg 0x0 0x44000000 0x0 0x01000000; no-map; }; };这里的核心是no-map属性它告诉内核这段物理地址不做默认映射也不要交给Linux常规的页分配器。当驱动需要访问时通过ioremap或dma_alloc_coherent等方式动态映射。实际项目中我遇到过有人漏掉no-map结果Linux启动后把这段共享内存当普通内存分配了出去M4和Linux就各写各的数据完全对不上。排查了大半天非常折腾。在M4侧需要把同一段物理地址映射到M4的地址空间中。如果这颗SoC的M4支持本地内存映射重映射那直接用相同物理地址即可如果不支持需要查SoC的地址映射表做一次偏移换算。我在另一颗平台上就遇到过A53侧物理地址和M4侧访问地址相差0x80000000的情况规则很简单但忘了换算的话通信100%失败。4.2 共享内存深度设计怎么放东西才不会撞车两边都能访问共享内存了接下来就要决定共享内存里面的布局。我见过一些项目直接在共享内存头部定义一个超大的结构体双方同时读写结果改动一个字段就导致兼容性问题。更稳妥的做法是在共享内存头部放一个metadata区域用于版本号、能力标志、状态机之后的区域按通道划分成不同的数据块数据块内部再细分为控制块和负载区。我在实际项目里的布局大致是偏移0x0000共享内存魔数magic与版本号偏移0x0004M4侧状态字running、ready、fault等偏移0x0010通道0的vring指针和buffer指针偏移0x0020通道0的环形缓冲写指针、读指针偏移0x0030通道0的负载缓冲区长度为16KB偏移0x4000通道1的类似结构这样划分的好处是每个通道有独立的收发队列业务之间不会互相阻塞同时metadata区域集中存放关键状态定位问题时一个内存dump就能看到全局状态。buffer区域是否要做cacheable是一个非常关键的取舍。如果整段共享内存都设置为non-cacheable读写性能会比较差特别是在大量搬运数据时吞吐量会明显下滑。但如果不做non-cacheable又可能出现缓存一致性问题。我的折中方案是控制结构vring、指针区放在一段non-cacheable区域确保同步语义正确真正的负载数据区放在另一段普通区域在发送/接收时做必要的cache maintenance。如果平台支持DMA且具备硬件一致性能力优先利用这一点。这里引出一个实践经验尽量别在共享内存里使用复杂的链表或动态结构。两个核各自运行各自的系统没有统一的动态内存管理器所谓的“指针在共享内存里指向另一个地址”很容易因为两侧地址转换差异而崩溃。固定偏移、固定大小的数组和环形队列是最安全的。4.3 基于OpenAMP的RPMSG环境搭建步骤OpenAMP是一套统一的AMP框架实现它内含rpmsg、remoteproc、virtio和libmetal等组件。Linux侧通常直接使用内核主线里已经合入的remoteproc/rpmsg框架配合一个remoteproc平台驱动M4侧则使用libmetal加open-amp的库。搭建步骤大致如下在内核配置里使能remoteproc、rpmsg、virtio相关配置项。比如CONFIG_REMOTEPROC、CONFIG_RPMSG_VIRTIO不同内核版本名字有细微差异需要查一下当前内核的menuconfig。编写remoteproc平台驱动告诉内核M4固件存放位置通常在文件系统或固定分区、M4使用的内存区域、启动方式和中断号。准备M4侧固件使用open-amp和libmetal编译出一个带有rpmsg功能的RTOS应用。Linux侧加载remoteproc驱动启动M4。启动后如果一切正常Linux侧会在/dev下出现rpmsg0、rpmsg_ctrl0这类设备节点或者在/sys/class/rpmsg下看到端点信息。接着写一个最简单的测试用例从Linux侧向M4发送“hello m4”字符串M4收到后打印并回一条“hello linux”。这个用例虽然简单却可以验证整条链路共享内存地址是否正确中断是否能正常触发vring读写是否匹配缓存一致性维护是否有效我在初次搭通时是先让M4侧把收到的数据写到一个固定的共享内存状态字里Linux侧轮询这个状态字是否变化以此先排除中断问题再进一步启用完整的中断通知。这个方法非常适合前期定位问题。5. 提高通信效率的细节性能瓶颈与优化手段5.1 怎样估算RPMSG通信的带宽和延迟很多工程师在方案选型时会问RPMSG单次通信最大能传多大块延迟大概多少这里给出一个通用的估算思路。单次传输的字节数上限通常由buffer大小决定。OpenAMP默认的buffer大小可以配置常见的有512字节、1024字节。如果一次要传的数据超过一个buffer的大小就需要在上层做拆包和重组或者直接把buffer尺寸调大。但buffer过大也有代价因为共享内存是有限的队列深度会变小也会占用更多内存带宽。延迟大致等于几部分相加发送方写数据到共享内存的时间 vring更新和内存屏障时间 触发IPI中断的时间 接收方从产生中断到真正处理中断的中断延迟 读取数据的时间。在Linux侧如果中断被其他高优先级任务阻塞或者CPU处于低功耗状态延迟会比较波动。实测中A53 1.2GHz主频下轮询模式的共享内存单向延迟可以做到2-5微秒走中断通知大约10-20微秒而如果用传统串口怎么也得几百微秒。差距非常明显。5.2 优化通信吞吐量的四个方向在我自己调优的过程中下面几个方向效果最显著批量发送不要一条业务消息就触发一次IPI中断而要把多条短消息攒到一个buffer里发一次充分拉高中断的利用率。比如M4上报传感器数据通常不是单点上报而是收集几十个样本后一次性打包。一次中断处理几十条消息CPU占用率能降一半以上。避免频繁内存屏障ARM平台上对共享内存的访问如果加了较重的dmb指令会拖慢速度。如果平台支持可以只在每次消息发送完成时加一次性barrier而不是对buffer内每个字段都加。利用DMA或硬件加速搬运如果SoC里有的DMAC可以访问共享内存而不经过CPU类似“内存到内存”的拷贝可以交给DMA做CPU只在头和尾处理控制信息。合理调整vring深度vring深度决定了队列中最多能pending多少个buffer。深度太小双方容易卡住等待尤其在一方处理速度较慢时深度太大会浪费共享内存空间而且清空队列的时延变大。通常16或32是比较折中的选择。5.3 缓存一致性处理的实操准则缓存一致性是共享内存方案里最坑的一点。不同ARM平台在这方面行为差异很大有的平台对部分内存区域默认为“outer shareable且inner/outer可缓存”有的则必须在页表里设置成non-cacheable。如果Linux侧把共享内存被映射成普通cacheable内存M4侧也是普通映射两边又没有做cache clean/invalidate几乎必然会出现以下诡异现象消息发过去了接收方读不到要等一会儿或再读一次才看到。接收方能看到发送方的一部分数据但某些字段是旧的尤其是相邻两次发送的数据只有末尾几个字节变化时比较容易出现“半个新包、半个旧包”。性能不稳定有时快有时慢因为数据是否命中缓存完全取决于运气。我的处理准则是控制区vring描述符、状态字所在的页映射为non-cacheable或device memory保证同步语义。数据区可以保持cacheable但在发送方写完数据后执行cache clean回写在接收方读取前执行cache invalidate使失效。如果平台支持DMA且缓冲区被标记为dma-coherent则不需要手动维护。OpenAMP里通常通过libmetal来申请共享内存它可以选择申请non-cacheable内存或普通的共享内存根据平台不同“缓存安全”的实现会封装到底层代码中。实操时我建议先把整段共享内存都设为non-cacheable来验证通信链路是否通通了之后再做性能优化逐步选择性cacheable。一上来就两头都优化出了问题反而难查。6. 实战过程中最常见的几个坑与排查方法6.1 共享内存被Linux常规内存管理器占用这是一种非常常见的“低级错误”但出现的频率高得惊人。表现是Linux侧和M4侧偶尔能通偶尔不能通而且能通的时候往往发生在Linux刚启动、内存占用不高时系统跑一段时间后通信就彻底失效。这类问题多半是设备树reserved-memory配置不对导致Linux进程或内核线程把共享内存页面分配出去使用。排查方式很直接在Linux侧查看/proc/iomem确认共享内存那段地址的标记不是被某个普通驱动占用也可以故意在共享内存的前几个字节写一个特殊值过几分钟再读如果值被改写说明有别的代码在写这段内存。另一个办法是使用page owner或kmemleak这类工具来追踪谁分配了这些页面不过略显重了。最有效的预防还是设备树里务必加上no-map。6.2 地址映射偏移错误A53侧物理地址和M4侧访问地址不一致的平台非常多。有的SoC里M4有一个“别名”区域访问DDR的地址需要加上一个固定的偏移比如0x40000000的DDR在M4眼里是0x80000000。如果Linux侧在共享内存头部写入magicM4侧读到的永远是错误数据往往是全零或垃圾值。建议刚开始调试时先在共享内存固定位置写入0xDEADBEEF之类的魔数然后在M4侧读取并打印出来对比是否符合预期。如果不符合优先检查地址转换关系而不是急着查中断和vring。6.3 中断触发了但对端没反应RPMSG传输的方向如果是A53发送到M4A53写完vring后通过IPI通知M4。有时候Linux侧显示发送成功但M4侧完全没有收到消息。拿到这类问题我一般按顺序排查中断号有没有配置对。有些SoC的IPI中断号在不同核心视角下并不一致需要查阅参考手册。中断是否在M4侧被屏蔽或抢占。M4侧跑FreeRTOS时如果屏蔽了全局中断或者把IPI中断的优先级设置太低就可能被其他中断一直抢占。vring的available index有没有写错。虚拟队列的索引管理比较微妙发送方和接收方对索引的理解必须一致差一个字节就完全无法工作。浮点数转换问题也在真实项目中遇到过A53侧传出的浮点数在M4侧按整数解析导致数据完全对不上。这倒不是RPMSG的问题而是共享内存结构体里类型定义不一致。经验是不要在两边的头文件里各自维护同一份结构体定义而应从一个公共头文件自动生成从源头上杜绝错位。6.4 消息乱序与丢包的处理RPMSG基于共享内存环形队列在底层不保证多个vring描述符的处理顺序一定严格排队。尤其当多个发送端同时向同一个通道写数据时可能出现消息A比消息B先写入vring、但B被对方先取走的状况。虽然RPMSG规范里尽量保证FIFO但跨核调度和缓存行为可能让顺序看起来混乱。如果业务对顺序有强要求建议在消息头里加上序号字段。接收方收到消息后先检查序号是否连续如果不连续直接丢弃或请求重发。这个方法看着low但在跨异构核通信场景中却是最简单可靠的保序手段。与其依赖上层协议不如在消息设计之初就把序号带上。另外当buffer深度耗尽、vring写不进去时RPMSG通常表现为发送端返回EAGAIN或阻塞等待。实际项目中如果长时间不处理可能出现发送方一直阻塞、接收方又没收到新数据的死锁。建议发送侧增加超时机制超过一定时间没写入就主动报错或丢包避免一个通道卡死影响其他所有通道。7. 从“能通”到“可靠”三个额外的工程建议7.1 日志与状态上报机制核间通信一旦出问题共守内存里的数据往往是全乱的不知所措。工程师屏幕上打出的日志可能来自A53和M4各自串口如果时间对齐不好定位问题非常痛苦。项目里强烈建议在共享内存的metadata区维护一个循环日志缓冲区两边各写各的日志都带上单调递增的时间戳可以用各自内核的节拍计数器。A53侧需要时直接dump这段共享内存就能把两边各自的日志按时间戳排序快速还原故障现场。7.2 固件加载与版本对齐在AMP方案中A53侧的Linux内核和M4侧的固件通常是分别编译的。如果两边对共享内存结构体、RPMSG的buffer大小、vring深度使用了不一致的配置或头文件很容易产生“能启动但一通信就异常”的问题。建议把两边对外暴露的接口头文件放到同一个代码仓库使用同一个版本号并且设备树里的内存规划、remoteproc加载路径也要一并纳入版本管理。我在项目里吃过亏之后直接写了个脚本编译A53内核时自动生成M4侧头文件的hash值M4固件加载前校验这个hash不一致就不允许连接。7.3 安全边界与看门狗AMP架构下两个核是平等运行、共享资源的。如果一个核跑飞了另一个核不应该被拖下水。共享内存上的通信最好有超时看门狗机制。例如A53每500ms给M4发一次心跳M4如果在超过1秒内没有更新心跳状态字就说明对方异常可以自动重启或报错。不要把这种机制想成小事核间通信卡死导致的系统异常比单核系统里的软件异常更难排查。最后再说一个个人经验。第一次调RPMSG时我一直试图在Linux侧用类似socket的阻塞接口来读消息结果消息稍慢一点就觉得“卡了”其实只是对端还没发出来。后来我把收发都改成基于事件回调的模式配合共享内存状态字做监控整个开发体验就顺畅了许多。多核环境下不要用单核的线性思维去理解两边的节奏共享内存和RPMSG提供的只是一个“管道”真正的流程编排还是要依靠中断、状态机、超时机制共同协作。