4、RPMsg驱动适配:RPMsg virtio层适配、buffer管理、通道建立、数据传输流程
好,咱们进入 RPMsg 驱动适配的核心环节。说白了,RPMsg 就是一个基于 virtio 的高层消息传递协议。你想想看,底层 Mailbox 已经把数据从一个核搬到另一个核了,但上层应用怎么知道这包数据是给谁的?怎么管理多个通信通道?这就是 RPMsg 要解决的问题。
我个人习惯把 RPMsg 适配分成四个模块来看:virtio 层适配、buffer 管理、通道建立、数据传输流程。咱们一个一个拆开讲。
4.1 virtio 层适配:让 RPMsg 跑在 virtio 上
RPMsg 在 Linux 内核里是作为 virtio 驱动实现的。为什么?因为 virtio 提供了一套标准的、跨平台的传输抽象层。你不需要关心底层是 Mailbox 还是共享内存,virtio 帮你封装好了。
适配的第一步,是实现 struct virtio_driver 结构体。我贴一段核心代码,你看一下骨架:
static struct virtio_driver rpmsg_virtio_drv = {
.driver.name = KBUILD_MODNAME,
.driver.owner = THIS_MODULE,
.id_table = rpmsg_virtio_id_table,
.probe = rpmsg_virtio_probe,
.remove = rpmsg_virtio_remove,
.config_changed = rpmsg_virtio_config_changed,
};
这里最关键的是 rpmsg_virtio_probe 函数。它负责初始化 virtqueue、分配共享内存、注册 RPMsg 总线。我在项目中遇到过一个问题:probe 里如果没等到底层 Mailbox 就绪就注册 virtqueue,系统直接 panic。嗯,后来我加了一个 wait_for_completion 等待底层握手完成,才稳定下来。
核心要点:virtio 层适配的本质,是把底层传输能力(Mailbox/共享内存)包装成 virtqueue 的接口。上层 RPMsg 只认 virtqueue,不认别的。
4.2 buffer 管理:共享内存怎么分?
RPMsg 的 buffer 管理,说白了就是管理一块预先分配好的共享内存。这块内存通常由主核(Master)在系统启动时分配,然后通过 Mailbox 把物理地址告诉从核(Remote)。
我习惯把 buffer 管理分成三个层次:
- Buffer Pool:整块共享内存被切分成固定大小的 buffer 槽位。每个槽位可以放一条消息。
- Buffer Descriptor:每个 buffer 有一个描述符,记录它的状态(空闲/使用中)、长度、归属通道。
- Buffer Allocator:负责从 pool 里分配和回收 buffer。
来看一个典型的 buffer 描述符结构:
struct rpmsg_buffer_desc {
dma_addr_t dma_addr; /* 物理地址 */
void *cpu_addr; /* 虚拟地址 */
unsigned int len; /* buffer 长度 */
unsigned int used; /* 0: 空闲, 1: 使用中 */
struct list_head node; /* 链表节点 */
};
避坑指南:我曾经在 buffer 回收时没做 DMA 同步,导致从核读到的数据是乱码。记住:在释放 buffer 之前,一定要调用 dma_sync_single_for_device 或 dma_unmap_single,确保 cache 一致性。
4.3 通道建立:RPMsg 的“电话线”
RPMsg 的通道(channel)就像一条电话线。每个通道有一个唯一的 源地址(src) 和 目的地址(dst)。两个核之间可以建立多条通道,每条通道跑不同的服务。
通道建立的流程大致如下:
- 主核调用
rpmsg_create_ept创建一个端点(endpoint),绑定到某个通道。 - RPMsg 核心层构造一个
RPMSG_NS_CREATE消息,通过 virtqueue 发送给从核。 - 从核收到消息后,解析通道名称和地址,在本地创建对应的端点。
- 双方握手完成,通道建立成功。
我画了一张流程图,帮你理清这个关系:
注意:通道建立是异步的。主核发送 NS_CREATE 后不能阻塞等待,否则可能死锁。我建议用 completion 或回调机制来处理确认消息。
4.4 数据传输流程:消息怎么走?
通道建立好了,接下来就是数据传输。RPMsg 的数据传输流程,我总结为“三步走”:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1. 发送 | 从 buffer pool 申请一个空闲 buffer | 调用 rpmsg_alloc_buffer() |
| 2. 填充 | 将消息数据拷贝到 buffer,设置头部 | 头部包含 src、dst、len、flags |
| 3. 提交 | 将 buffer 放入 virtqueue,通知对方 | 调用 virtqueue_add_outbuf() + virtqueue_kick() |
接收端则是反过来:
- 从 virtqueue 取出 buffer
- 解析头部,找到对应的端点
- 调用端点的回调函数,把数据送上去
- 释放 buffer 回 pool
来看一段发送端的核心代码:
int rpmsg_send(struct rpmsg_endpoint *ept, void *data, int len)
{
struct rpmsg_virtio_device *rvdev = ept->rvdev;
struct rpmsg_buffer_desc *desc;
struct rpmsg_hdr *hdr;
int ret;
/* 1. 申请 buffer */
desc = rpmsg_alloc_buffer(rvdev, len + sizeof(*hdr));
if (!desc)
return -ENOMEM;
/* 2. 填充头部和数据 */
hdr = desc->cpu_addr;
hdr->src = ept->addr;
hdr->dst = ept->dest;
hdr->len = len;
hdr->flags = 0;
memcpy(hdr->data, data, len);
/* 3. 提交到 virtqueue */
ret = virtqueue_add_outbuf(rvdev->svq, &desc->sg, 1, desc, GFP_KERNEL);
if (ret) {
rpmsg_free_buffer(rvdev, desc);
return ret;
}
virtqueue_kick(rvdev->svq);
return 0;
}
个人经验:我在调试时发现,virtqueue_kick 之后数据不一定立刻到达对方。因为底层 Mailbox 可能有延迟。我习惯在 kick 之后加一个 mb() 内存屏障,确保写操作完成。另外,接收端的中断处理函数里,记得用 virtqueue_get_buf 循环取出所有 buffer,别漏了。
4.5 避坑总结:我踩过的几个坑
最后,我把这些年适配 RPMsg 时踩过的坑列出来,你遇到了可以少走弯路:
- Buffer 大小不匹配:主核和从核的 buffer 大小必须一致。我曾经因为两边配置不同,导致数据截断。建议在握手阶段交换 buffer 大小信息。
- Cache 一致性问题:共享内存区域必须使用
dma_alloc_coherent分配,或者手动做 cache 刷新。否则数据错乱是家常便饭。 - Virtqueue 索引溢出:如果 buffer 分配和回收速度不匹配,virtqueue 的索引可能溢出。我建议在
rpmsg_alloc_buffer里加一个WARN_ON检查。 - 中断风暴:接收端如果处理太慢,发送端频繁 kick,可能引发中断风暴。解决方案是接收端用 NAPI 或 tasklet 批量处理。
嗯,RPMsg 的适配说复杂也复杂,说简单也简单。核心就是理解 virtio 层的抽象、管好 buffer、建好通道、走通数据流。把这四个模块搞透了,剩下的就是调试和优化了。