RPMsg协议原理深度解析

各位好,欢迎来到RPMsg协议栈调试课程的第二讲。今天我们要啃的这块硬骨头,是RPMsg协议的核心原理。说实话,我早年刚接触这个协议时,也被它绕得够呛——virtio、remoteproc、rpmsg,这三者到底什么关系?数据是怎么从A核跑到B核的?

别急,我们一层层剥开它。你只要搞懂了今天的内容,后面调试性能问题、定位故障,就有了底气。

一、RPMsg协议栈全景图

先看整体。RPMsg不是凭空冒出来的协议,它建立在两个底层框架之上:virtio 提供传输通道,remoteproc 管理远程处理器的生命周期。RPMsg本身,说白了就是在这两层之间加了一个消息总线。

核心关系链:

remoteproc(启动/加载固件)→ virtio(共享内存+通知机制)→ RPMsg(消息封装+端点路由)

我画了一张图,把这三层的关系和关键数据结构标了出来。你看一眼,心里就有谱了。

RPMsg 协议栈三层架构 remoteproc 框架 固件加载 · 生命周期管理 · 资源表解析 关键结构:rproc / rproc_vdev / rproc_mem_entry virtio 传输层 共享内存管理 · vring 环形队列 · kick 通知机制 关键结构:virtqueue / vring / vring_desc / vring_avail / vring_used 传输方式:共享内存(无拷贝) + 硬件中断(Doorbell) RPMsg 消息总线 消息封装 · 端点路由 · 通道管理 · 服务发现 关键结构:rpmsg_hdr / rpmsg_endpoint / rpmsg_device / rpmsg_channel 数据流向:Master 核 → virtio TX vring → 共享内存 → Remote 核 RX vring → RPMsg 端点

这张图我建议你保存下来。每次调试时,先定位问题出在哪一层,再深入细节,效率会高很多。

二、核心数据结构:rpmsg_hdr

所有RPMsg消息,在共享内存里传输时,都带着一个头部——struct rpmsg_hdr。这个结构体只有16字节,但每个字段都至关重要。

struct rpmsg_hdr {
    __u32 src;      // 源端点地址
    __u32 dst;      // 目标端点地址
    __u32 len;      // 有效载荷长度(不包含头部)
    __u16 flags;    // 标志位(保留/校验等)
    __u16 reserved; // 保留对齐
    // 紧接着是 payload 数据
} __packed;

我的经验:调试时第一个要检查的就是 len 字段。我曾经遇到一个坑:发送端填的 len 是 1024,但实际 payload 只有 200 字节,接收端直接读越界了。这种问题用 printk 打出来一眼就能发现。

这里有个关键点:端点地址(src/dst)不是物理地址,也不是CPU核心号,而是RPMsg总线上的逻辑地址。每个端点(endpoint)在创建时会被分配一个唯一的32位地址。发送方填src为自己的地址,dst填接收方的地址。

字段 大小 说明 调试关注点
src 4字节 源端点地址 检查是否与注册的端点匹配
dst 4字节 目标端点地址 接收端路由的依据
len 4字节 payload长度 最容易出错的字段,注意字节序
flags 2字节 控制标志 目前多用bit0表示是否需要ACK
reserved 2字节 对齐填充 必须为0,否则兼容性会出问题

注意字节序! 如果Master核是x86(小端),Remote核是ARM(也是小端),一般没问题。但如果你遇到PowerPC或某些DSP(大端),rpmsg_hdr 的所有多字节字段都需要做字节序转换。我见过一个团队因为这个排查了整整两天。

三、工作流程:从virtio到rpmsg总线

好,现在我们来看一条消息从发送到接收的完整路径。我把它拆成6个步骤,你跟着走一遍就明白了。

  1. 端点发送:应用层调用 rpmsg_send(),数据进入rpmsg核心层。核心层封装 rpmsg_hdr,填入src/dst/len。
  2. 入队vring:封装好的消息被放入virtio的TX vring。这里用的是 virtqueue_add_outbuf(),把buffer描述符链到vring_desc表里。
  3. kick通知:调用 virtqueue_kick(),写Remote核的Doorbell寄存器(通常是写一个共享内存地址或触发硬件中断)。
  4. Remote核接收:Remote核收到中断,在中断处理函数中调用 virtqueue_get_buf() 从RX vring取出buffer。
  5. 解析头部:Remote核的rpmsg驱动解析 rpmsg_hdr,根据dst字段找到对应的端点回调函数。
  6. 回调分发:调用端点的回调函数,把payload传给上层应用。

你看,整个过程没有数据拷贝——Master核把数据写到共享内存,Remote核直接读同一块内存。这就是RPMsg高性能的关键。

性能关键路径:

整个链路中,最耗时的操作是第3步的kick和第4步的中断响应。如果你在做性能调优,重点关注这两个地方的延迟。

四、RPMsg与virtio、remoteproc的关系

这三者的关系,我习惯用一个比喻来理解:

  • remoteproc 是「物业」——负责把Remote核这个「房间」打扫干净(加载固件、分配内存),然后告诉你房间准备好了。
  • virtio 是「管道」——在Master和Remote之间铺设了一条共享内存的管道,两端各有一个vring作为收发缓冲区。
  • RPMsg 是「邮递员」——它不管管道怎么铺设,只管把信封装好(rpmsg_hdr),通过管道投递到正确的收件人(端点)。

从代码层面看,它们的初始化顺序是固定的:

// 伪代码:初始化流程
1. rproc_boot()                    // remoteproc启动Remote核
2.   → rproc_parse_resource_table() // 解析资源表,找到virtio设备描述
3.     → rproc_virtio_probe()       // 注册virtio设备
4.       → rpmsg_probe()            // RPMsg总线探测,创建ns_service端点
5.         → rpmsg_register_device() // 注册rpmsg总线,等待通道创建

这里有个细节:RPMsg总线是在virtio设备探测成功后自动注册的。所以如果你发现 /sys/bus/rpmsg 下面没有设备,先检查virtio层有没有正常工作。

层次 职责 调试切入点
remoteproc 固件加载、内存分配、生命周期 检查 /sys/kernel/debug/remoteproc/ 下的状态文件
virtio 共享内存管理、vring操作、kick通知 检查vring地址是否正确,可用 trace_virtio_queue()
rpmsg 消息封装、端点路由、通道管理 检查端点地址是否冲突,用 rpmsg_chrdev 测试收发

避坑指南:我曾经遇到一个问题:Remote核的固件里virtio设备描述符的vring地址,和Master核remoteproc解析出来的地址不一致。原因是资源表中的地址是Remote核视角的物理地址,而Master核需要做一次地址转换。如果你发现kick之后Remote核没反应,先查这个地址对不对。

五、端点与通道:RPMsg的路由机制

RPMsg的消息路由,靠的是端点(endpoint)和通道(channel)这两个概念。

通道代表一个逻辑连接,比如「Master核的音频服务 ↔ Remote核的音频处理」。每个通道有唯一的名称(如"rpmsg-audio")。

端点是通道两端的通信端口。每个端点有一个32位地址。Master核的音频端点地址是0x1,Remote核的音频端点地址是0x2。

当Master核发送消息时,rpmsg_hdr.src=0x1, dst=0x2。Remote核收到后,根据dst=0x2找到对应的端点,调用它的回调函数。

端点的创建有两种方式:

  • 静态创建:在资源表中预定义,固件加载时自动注册。
  • 动态创建:通过NS(Name Service)消息协商。Master核发送一个NS公告,Remote核收到后创建对应的端点和通道。

动态通道创建的NS消息格式:

NS消息本身也是一个RPMsg消息,它的 rpmsg_hdr 中 dst=0(特殊地址,表示NS服务端点),payload里包含通道名称和标志位。

嗯,到这里,RPMsg协议的核心原理你应该已经清楚了。从rpmsg_hdr这个16字节的头部,到virtio的vring传输,再到remoteproc的底层支撑,整个链路其实并不复杂。复杂的是调试时怎么定位问题——这个我们后面几章会专门讲。

记住一句话:RPMsg的本质,就是在共享内存上做的一次零拷贝消息投递。 搞懂了这个,你就抓住了它的灵魂。