远程处理器消息传递 (rpmsg) 框架

注意

本文档描述了 rpmsg 总线以及如何编写 rpmsg 驱动程序。要了解如何为新平台添加 rpmsg 支持,请查看 远程处理器框架(同样位于 Documentation/ 目录下)。

简介

现代 SoC 通常在非对称多处理 (AMP) 配置中采用异构远程处理器设备,它们可能运行不同实例的操作系统,无论是 Linux 还是任何其他风格的实时操作系统 (RTOS)。

例如,OMAP4 具有双 Cortex-A9、双 Cortex-M3 和一个 C64x+ DSP。通常,双 Cortex-A9 在 SMP 配置下运行 Linux,而其他三个核心(两个 M3 核心和一个 DSP)各自在 AMP 配置下运行自己的 RTOS 实例。

通常,AMP 远程处理器采用专用的 DSP 编解码器和多媒体硬件加速器,因此经常用于从主应用处理器卸载 CPU 密集型的多媒体任务。

这些远程处理器还可以用于控制对延迟敏感的传感器、驱动各种硬件模块,或者在主 CPU 处于空闲状态时执行后台任务。

这些远程文件的用户可以是用户空间应用程序(例如与远程 OMX 组件通信的多媒体框架),也可以是内核驱动程序(控制仅远程处理器可访问的硬件,代表远程处理器保留由内核控制的资源等)。

Rpmsg 是一个基于 virtio 的消息传递总线,允许内核驱动程序与系统上可用的远程处理器进行通信。反过来,如有需要,驱动程序随后可以公开相应的用户空间接口。

在编写向用户空间公开 rpmsg 通信的驱动程序时,请记住,远程处理器可能对系统的物理内存和其他敏感硬件资源拥有直接访问权限(例如在 OMAP4 上,远程核心和硬件加速器可能直接访问物理内存、gpio 组、dma 控制器、i2c 总线、gptimer、邮箱设备、hwspinlock 等)。此外,这些远程处理器可能运行 RTOS,其中每个任务都可以访问暴露给该处理器的整个内存/设备。为了将恶意(或有漏洞的)用户空间代码利用远程漏洞从而接管系统的风险降到最低,通常希望将用户空间限制在特定的 rpmsg 通道(见下文定义)上,它可以在这些通道发送消息,并且如果可能的话,尽量减少它对消息内容的控制程度。

每个 rpmsg 设备都是与远程处理器的通信通道(因此 rpmsg 设备被称为通道)。通道通过文本名称标识,并具有本地(“源”)rpmsg 地址和远程(“目标”)rpmsg 地址。

当驱动程序开始在某个通道上监听时,其 rx 回调函数会与一个唯一的 rpmsg 本地地址(一个 32 位整数)绑定。这样,当入站消息到达时,rpmsg 核心会根据其目标地址将它们分发给相应的驱动程序(这是通过使用入站消息的有效负载调用驱动程序的 rx 处理程序来完成的)。

用户 API

int rpmsg_send(struct rpmsg_endpoint *ept, void *data, int len);

从给定的端点向远程处理器发送一条消息。调用者应指定端点、要发送的数据及其长度(以字节为单位)。该消息将通过指定端点的通道发送,即其源地址和目标地址字段将分别设置为端点的 src 地址及其父通道的 dst 地址。

如果没有可用的 TX 缓冲区,该函数将阻塞,直到有缓冲区可用(即直到远程处理器消费了一个 tx 缓冲区并将其放回 virtio 的已使用描述符环中),或者 15 秒超时到期。当发生后者时,将返回 -ERESTARTSYS。

该函数只能从进程上下文中调用(目前如此)。成功时返回 0,失败时返回相应的错误值。

int rpmsg_sendto(struct rpmsg_endpoint *ept, void *data, int len, u32 dst);

从给定的端点向远程处理器发送一条消息,发送到调用者提供的目标地址。

调用者应指定端点、要发送的数据、其长度(以字节为单位)以及一个明确的目标地址。

然后,该消息将发送至端点所属的通道对应的远程处理器,使用端点的 src 地址和用户提供的 dst 地址(因此通道原有的 dst 地址将被忽略)。

如果没有可用的 TX 缓冲区,该函数将阻塞,直到有缓冲区可用(即直到远程处理器消费了一个 tx 缓冲区并将其放回 virtio 的已使用描述符环中),或者 15 秒超时到期。当发生后者时,将返回 -ERESTARTSYS。

该函数只能从进程上下文中调用(目前如此)。成功时返回 0,失败时返回相应的错误值。

int rpmsg_trysend(struct rpmsg_endpoint *ept, void *data, int len);

从给定的端点向远程处理器发送一条消息。调用者应指定端点、要发送的数据及其长度(以字节为单位)。该消息将通过指定端点的通道发送,即其源地址和目标地址字段将分别设置为端点的 src 地址及其父通道的 dst 地址。

如果没有可用的 TX 缓冲区,该函数将立即返回 -ENOMEM,而不会等待缓冲区变得可用。

该函数只能从进程上下文中调用(目前如此)。成功时返回 0,失败时返回相应的错误值。

int rpmsg_trysendto(struct rpmsg_endpoint *ept, void *data, int len, u32 dst)

从给定的端点向远程处理器发送一条消息,发送至用户提供的目标地址。

用户应指定通道、要发送的数据、其长度(以字节为单位)以及一个明确的目标地址。

然后,该消息将发送至通道所属的远程处理器,使用通道的 src 地址和用户提供的 dst 地址(因此通道原有的 dst 地址将被忽略)。

如果没有可用的 TX 缓冲区,该函数将立即返回 -ENOMEM,而不会等待缓冲区变得可用。

该函数只能从进程上下文中调用(目前如此)。成功时返回 0,失败时返回相应的错误值。

struct rpmsg_endpoint *rpmsg_create_ept(struct rpmsg_device *rpdev,
                                        rpmsg_rx_cb_t cb, void *priv,
                                        struct rpmsg_channel_info chinfo);

系统中的每个 rpmsg 地址都通过一个 rpmsg_endpoint 结构体绑定到一个 rx 回调函数(因此当入站消息到达时,它们由 rpmsg 总线使用适当的回调处理程序进行分发)。

此函数允许驱动程序创建这样一个端点,从而将回调函数以及可能的一些私有数据绑定到某个 rpmsg 地址(可以是预先知道的地址,也可以是动态分配给它们的地址)。

简单的 rpmsg 驱动程序不需要调用 rpmsg_create_ept,因为当它们被 rpmsg 总线探测时,已经为它们创建了一个端点(使用的是它们在注册到 rpmsg 总线时提供的 rx 回调)。

因此,对于简单的驱动程序,事情应该可以直接正常运作:它们已经拥有一个端点,其 rx 回调已绑定到它们的 rpmsg 地址,并且当相关的入站消息到达时(即其 dst 地址等于其 rpmsg 通道的 src 地址的消息),将调用驱动程序的处理程序来处理它。

话虽如此,更复杂的驱动程序可能确实需要分配额外的 rpmsg 地址,并将它们绑定到不同的 rx 回调。为了实现这一点,这些驱动程序需要调用此函数。驱动程序应提供其通道(以便新端点将绑定到其通道所属的相同远程处理器)、rx 回调函数、可选的私有数据(在调用 rx 回调时会将其传回)以及它们想要与回调绑定的地址。如果 addr 为 RPMSG_ADDR_ANY,则 rpmsg_create_ept 将动态分配给它们一个可用的 rpmsg 地址(驱动程序应该有充分的理由说明为什么不在这里总是使用 RPMSG_ADDR_ANY)。

成功时返回指向端点的指针,出错时返回 NULL。

void rpmsg_destroy_ept(struct rpmsg_endpoint *ept);

销毁现有的 rpmsg 端点。用户应提供一个指向先前用 rpmsg_create_ept() 创建的 rpmsg 端点的指针。

int register_rpmsg_driver(struct rpmsg_driver *rpdrv);

将 rpmsg 驱动程序注册到 rpmsg 总线。用户应提供一个指向 rpmsg_driver 结构体的指针,该结构体包含驱动程序的 ->probe() 和 ->remove() 函数、一个 rx 回调以及一个指定该驱动程序感兴趣对其进行探测的通道名称的 id_table。

void unregister_rpmsg_driver(struct rpmsg_driver *rpdrv);

从 rpmsg 总线注销 rpmsg 驱动程序。用户应提供一个指向先前注册的 rpmsg_driver 结构体的指针。成功时返回 0,失败时返回相应的错误值。

典型用法

以下是一个简单的 rpmsg 驱动程序,它在 probe() 时发送一条“hello!”消息,并且每当收到传入消息时,就会将内容转储到控制台。

#include <linux/dev_printk.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/rpmsg.h>
#include <linux/types.h>

static void rpmsg_sample_cb(struct rpmsg_channel *rpdev, void *data, int len,
                                              void *priv, u32 src)
{
      print_hex_dump(KERN_INFO, "incoming message:", DUMP_PREFIX_NONE,
                                              16, 1, data, len, true);
}

static int rpmsg_sample_probe(struct rpmsg_channel *rpdev)
{
      int err;

      dev_info(&rpdev->dev, "chnl: 0x%x -> 0x%x\n", rpdev->src, rpdev->dst);

      /* send a message on our channel */
      err = rpmsg_send(rpdev->ept, "hello!", 6);
      if (err) {
              dev_err(&rpdev->dev, "rpmsg_send failed: %d\n", err);
              return err;
      }

      return 0;
}

static void rpmsg_sample_remove(struct rpmsg_channel *rpdev)
{
      dev_info(&rpdev->dev, "rpmsg sample client driver is removed\n");
}

static struct rpmsg_device_id rpmsg_driver_sample_id_table[] = {
      { .name = "rpmsg-client-sample" },
      { },
};
MODULE_DEVICE_TABLE(rpmsg, rpmsg_driver_sample_id_table);

static struct rpmsg_driver rpmsg_sample_client = {
      .drv.name       = KBUILD_MODNAME,
      .id_table       = rpmsg_driver_sample_id_table,
      .probe          = rpmsg_sample_probe,
      .callback       = rpmsg_sample_cb,
      .remove         = rpmsg_sample_remove,
};
module_rpmsg_driver(rpmsg_sample_client);

注意

可以在 samples/rpmsg/ 中找到一个可以构建和加载的类似示例。

rpmsg 通道的分配

目前我们仅支持动态分配 rpmsg 通道。

这仅适用于设置了 VIRTIO_RPMSG_F_NS virtio 设备特性的远程处理器。此特征位意味着远程处理器支持动态名称服务通告消息。

启用此功能后,rpmsg 设备(即通道)的创建完全是动态的:远程处理器通过发送名称服务消息(其中包含远程服务的名称和 rpmsg 地址,参见 struct rpmsg_ns_msg)来通告远程 rpmsg 服务的存在。

然后,该消息由 rpmsg 总线处理,总线进而动态创建并注册一个 rpmsg 通道(代表远程服务)。如果/当注册了相关的 rpmsg 驱动程序时,总线会立即对其进行探测,然后该驱动程序就可以开始向远程服务发送消息。

计划还通过 virtio 配置空间添加 rpmsg 通道的静态创建,但目前尚未实现。