3、RPMsg协议原理:消息格式、Virtio层、Buffer管理与Endpoint
RPMsg,全称Remote Processor Messaging。说白了,它就是一套定义好的通信规矩,让主处理器(比如ARM Cortex-A)和协处理器(比如Cortex-M、DSP)之间能互相发消息。你想想看,两个完全不同的核心,跑着不同的系统,怎么聊天?RPMsg就是那个翻译官。
我个人习惯把RPMsg看作是“寄信服务”。你写好信(消息),放进信封(Buffer),贴上邮票(Virtio层处理),然后邮递员(Mailbox驱动)帮你送过去。收件人那边再拆信、读信。这一整套流程,就是RPMsg要干的事。
3.1 RPMsg消息格式:信封长什么样?
RPMsg的消息格式其实挺简洁的。它不像有些协议那样头重脚轻,核心就一个结构体。我刚开始看的时候,觉得它有点像网络协议里的TCP头,但简单得多。
struct rpmsg_hdr {
u32 src; // 源端点地址
u32 dst; // 目标端点地址
u32 len; // 有效载荷长度
u16 flags; // 标志位(保留或用于控制)
u16 reserved; // 保留字段
// 紧接着就是有效载荷数据
};
这个头只有16个字节。嗯,这里要注意,src和dst不是物理地址,而是端点地址。每个端点就像一个邮箱,有自己的编号。发消息时,你得知道对方的邮箱号。
关键点:RPMsg消息的最小单元是“帧”。一帧数据包含一个rpmsg_hdr和紧随其后的有效载荷。有效载荷长度由len字段指定,最大不能超过底层Buffer的大小。
我在项目中遇到过一个问题:两个核之间传图片数据,结果图片花屏。查了半天,发现是len字段被写错了,导致接收方只读了部分数据。从那以后,我每次都会在发送前校验一下len是否和实际数据长度一致。
3.2 Virtio层介绍:虚拟化IO的魔法
Virtio,全称Virtual I/O。它最初是为虚拟化场景设计的,让虚拟机里的操作系统能高效访问硬件。但RPMsg把它借过来,用在异构多核通信上。为什么?因为Virtio提供了一套现成的、高效的Buffer管理机制。
你想想看,如果没有Virtio,两个核之间传数据,你得自己设计环形缓冲区、自己处理同步、自己管理内存。累不累?Virtio把这些脏活累活都干了。
Virtio的核心概念就两个:Virtqueue和Buffer Descriptor。
- Virtqueue:一个环形队列,里面放的是Buffer描述符。发送方把描述符扔进队列,接收方从队列里取出来处理。
- Buffer Descriptor:描述一个Buffer的信息,包括物理地址、长度、是否只读等。
Virtio层的工作流程,我画了个图,你看一眼就明白了。
这张图里,发送端把Buffer描述符放进发送Virtqueue,接收端从接收Virtqueue里取描述符。数据本身存在共享内存里。描述符就像一张“取货单”,告诉你数据在哪、有多长。
我的经验:Virtqueue的大小(即队列深度)很关键。太浅了,容易丢消息;太深了,浪费内存。我一般根据最大并发消息数来定,再留20%的余量。
3.3 Buffer管理策略:谁分配?谁释放?
Buffer管理是RPMsg里最容易出问题的地方。我曾经在一个项目里,因为Buffer分配和释放没配对,导致内存泄漏,系统跑了三天就挂了。排查起来特别痛苦。
RPMsg的Buffer管理策略,说白了就两种:
| 策略 | 描述 | 适用场景 |
|---|---|---|
| 静态预分配 | 系统启动时,一次性分配好所有Buffer。每个Buffer大小固定,数量固定。 | 消息大小固定、数量可预测的场景。比如传感器数据采集。 |
| 动态分配 | 运行时按需分配Buffer。用完后立即释放。 | 消息大小变化大、数量不确定的场景。比如文件传输。 |
我个人更倾向于静态预分配。为什么?因为嵌入式系统最怕不确定性。动态分配容易产生碎片,而且分配失败时处理起来很麻烦。你想想看,如果分配Buffer失败,你是重试还是丢弃消息?重试可能导致死锁,丢弃可能导致数据丢失。
Virtio层对Buffer的管理,遵循一个原则:谁分配,谁释放。发送端分配Buffer,填充数据,然后交给Virtqueue。接收端处理完数据后,把Buffer还给发送端。注意,是“还”给发送端,不是自己释放。这样能避免很多竞态条件。
避坑指南:我曾经在代码里忘了把Buffer还给发送端,结果发送端的Buffer池很快就空了,系统卡死。排查了整整两天才发现。所以,一定要确保每个Buffer都有明确的“归还”路径。
3.4 Endpoint概念:通信的“门牌号”
Endpoint,直译就是“端点”。你可以把它理解成通信双方的“门牌号”。每个Endpoint都有一个唯一的地址(就是前面说的src和dst)。
为什么需要Endpoint?因为一个处理器上可能跑着多个服务。比如,Cortex-A上同时运行着音频服务、网络服务、控制服务。这些服务都要和Cortex-M通信。如果没有Endpoint,消息来了,你都不知道该交给谁处理。
Endpoint的创建和销毁,由RPMsg框架管理。每个Endpoint都有一个回调函数。当消息到达时,框架会根据消息头里的dst字段,找到对应的Endpoint,然后调用它的回调函数。
// 创建一个Endpoint
struct rpmsg_endpoint *ept;
ept = rpmsg_create_ept(rpdev, "my_service", 0x10, 0x20, my_callback, NULL);
// 回调函数示例
static int my_callback(struct rpmsg_endpoint *ept, void *data, size_t len, void *priv)
{
// 处理接收到的数据
printk("Received %zu bytes from endpoint 0x%x\n", len, ept->src);
return 0;
}
这段代码里,rpmsg_create_ept创建了一个Endpoint,源地址是0x10,目标地址是0x20。当收到目标地址为0x20的消息时,my_callback就会被调用。
重要概念:Endpoint地址是逻辑地址,不是物理地址。它只在RPMsg协议层有意义。底层Mailbox和共享内存,根本不关心Endpoint地址是什么。
嗯,这里要注意,Endpoint的地址分配要避免冲突。我一般会做一个地址分配表,每个服务固定一个地址范围。比如,音频服务用0x10-0x1F,网络服务用0x20-0x2F。这样管理起来清晰多了。
好了,RPMsg协议原理就讲到这里。消息格式、Virtio层、Buffer管理、Endpoint,这四个概念是RPMsg的基石。理解了它们,你就能看懂RPMsg的代码了。