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)。两个核之间可以建立多条通道,每条通道跑不同的服务。

通道建立的流程大致如下:

  1. 主核调用 rpmsg_create_ept 创建一个端点(endpoint),绑定到某个通道。
  2. RPMsg 核心层构造一个 RPMSG_NS_CREATE 消息,通过 virtqueue 发送给从核。
  3. 从核收到消息后,解析通道名称和地址,在本地创建对应的端点。
  4. 双方握手完成,通道建立成功。

我画了一张流程图,帮你理清这个关系:

RPMsg 通道建立流程图 RPMsg 通道建立流程图 主核 (Master) 从核 (Remote) 1. 调用 rpmsg_create_ept() 创建端点,绑定通道 NS_CREATE 消息 2. 解析 NS_CREATE 提取通道名称和地址 3. 创建本地端点 绑定到对应通道 NS_ACK 确认 4. 收到 NS_ACK 通道建立完成 通道建立成功,可以收发数据

注意:通道建立是异步的。主核发送 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、建好通道、走通数据流。把这四个模块搞透了,剩下的就是调试和优化了。