VDUSE - “使用者空間的 vDPA 裝置”

vDPA (virtio 資料路徑加速) 裝置是一種使用符合 virtio 規範的資料路徑,並具有供應商特定控制路徑的裝置。 vDPA 裝置可以物理位於硬體上,也可以由軟體模擬。 VDUSE 是一個框架,使得可以在使用者空間中實現軟體模擬的 vDPA 裝置。 為了使裝置模擬更加安全,模擬的 vDPA 裝置的控制路徑在核心中處理,而只有資料路徑在使用者空間中實現。

請注意,VDUSE 框架現在僅支援 virtio 塊裝置,這可以降低安全風險,因為實現資料路徑的使用者空間程序由非特權使用者執行。 在相應的裝置驅動程式的安全問題在將來得到澄清或修復後,可以新增對其他裝置型別的支援。

建立/銷燬 VDUSE 裝置

VDUSE 裝置的建立方式如下

  1. 在 /dev/vduse/control 上使用 ioctl(VDUSE_CREATE_DEV) 建立一個新的 VDUSE 例項。

  2. 在 /dev/vduse/$NAME 上使用 ioctl(VDUSE_VQ_SETUP) 設定每個 virtqueue。

  3. 開始處理來自 /dev/vduse/$NAME 的 VDUSE 訊息。 當 VDUSE 例項附加到 vDPA 匯流排時,第一批訊息將到達。

  4. 傳送 VDPA_CMD_DEV_NEW netlink 訊息以將 VDUSE 例項附加到 vDPA 匯流排。

VDUSE 裝置的銷燬方式如下

  1. 傳送 VDPA_CMD_DEV_DEL netlink 訊息以將 VDUSE 例項從 vDPA 匯流排分離。

  2. 關閉引用 /dev/vduse/$NAME 的檔案描述符。

  3. 在 /dev/vduse/control 上使用 ioctl(VDUSE_DESTROY_DEV) 銷燬 VDUSE 例項。

netlink 訊息可以透過 iproute2 中的 vdpa 工具傳送,也可以使用以下示例程式碼

static int netlink_add_vduse(const char *name, enum vdpa_command cmd)
{
        struct nl_sock *nlsock;
        struct nl_msg *msg;
        int famid;

        nlsock = nl_socket_alloc();
        if (!nlsock)
                return -ENOMEM;

        if (genl_connect(nlsock))
                goto free_sock;

        famid = genl_ctrl_resolve(nlsock, VDPA_GENL_NAME);
        if (famid < 0)
                goto close_sock;

        msg = nlmsg_alloc();
        if (!msg)
                goto close_sock;

        if (!genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, famid, 0, 0, cmd, 0))
                goto nla_put_failure;

        NLA_PUT_STRING(msg, VDPA_ATTR_DEV_NAME, name);
        if (cmd == VDPA_CMD_DEV_NEW)
                NLA_PUT_STRING(msg, VDPA_ATTR_MGMTDEV_DEV_NAME, "vduse");

        if (nl_send_sync(nlsock, msg))
                goto close_sock;

        nl_close(nlsock);
        nl_socket_free(nlsock);

        return 0;
nla_put_failure:
        nlmsg_free(msg);
close_sock:
        nl_close(nlsock);
free_sock:
        nl_socket_free(nlsock);
        return -1;
}

VDUSE 如何工作

如上所述,VDUSE 裝置透過在 /dev/vduse/control 上呼叫 ioctl(VDUSE_CREATE_DEV) 來建立。 透過此 ioctl,使用者空間可以指定一些基本配置,例如裝置名稱(唯一標識 VDUSE 裝置)、virtio 特性、virtio 配置空間、virtqueue 的數量等等,用於此模擬裝置。 然後,將字元裝置介面 (/dev/vduse/$NAME) 匯出到使用者空間以進行裝置模擬。 使用者空間可以使用 /dev/vduse/$NAME 上的 VDUSE_VQ_SETUP ioctl 來新增每個 virtqueue 的配置,例如 virtqueue 的最大大小。

初始化後,可以透過 VDPA_CMD_DEV_NEW netlink 訊息將 VDUSE 裝置附加到 vDPA 匯流排。 使用者空間需要在 /dev/vduse/$NAME 上進行 read()/write() 操作,以便從 VDUSE 核心模組接收/回覆一些控制訊息,如下所示

static int vduse_message_handler(int dev_fd)
{
        int len;
        struct vduse_dev_request req;
        struct vduse_dev_response resp;

        len = read(dev_fd, &req, sizeof(req));
        if (len != sizeof(req))
                return -1;

        resp.request_id = req.request_id;

        switch (req.type) {

        /* handle different types of messages */

        }

        len = write(dev_fd, &resp, sizeof(resp));
        if (len != sizeof(resp))
                return -1;

        return 0;
}

VDUSE 框架現在引入了三種類型的訊息

  • VDUSE_GET_VQ_STATE: 獲取 virtqueue 的狀態,使用者空間應返回拆分 virtqueue 的可用索引,或者返回裝置/驅動程式環繞計數器以及打包 virtqueue 的可用和已用索引。

  • VDUSE_SET_STATUS: 設定裝置狀態,使用者空間應遵循 virtio 規範:https://docs.oasis-open.org/virtio/virtio/v1.1/virtio-v1.1.html 來處理此訊息。 例如,如果裝置無法接受從 VDUSE_DEV_GET_FEATURES ioctl 獲取的協商 virtio 特性,則無法設定 FEATURES_OK 裝置狀態位。

  • VDUSE_UPDATE_IOTLB: 通知使用者空間更新指定 IOVA 範圍的記憶體對映,使用者空間應首先刪除舊的對映,然後透過 VDUSE_IOTLB_GET_FD ioctl 設定新的對映。

透過 VDUSE_SET_STATUS 訊息設定 DRIVER_OK 狀態位後,使用者空間可以開始資料平面處理,如下所示

  1. 使用 VDUSE_VQ_GET_INFO ioctl 獲取指定 virtqueue 的資訊,包括大小、描述符表的 IOVA、可用環和已用環、狀態和就緒狀態。

  2. 將上述 IOVA 傳遞給 VDUSE_IOTLB_GET_FD ioctl,以便可以將這些 IOVA 區域對映到使用者空間。 以下顯示了一些示例程式碼

static int perm_to_prot(uint8_t perm)
{
        int prot = 0;

        switch (perm) {
        case VDUSE_ACCESS_WO:
                prot |= PROT_WRITE;
                break;
        case VDUSE_ACCESS_RO:
                prot |= PROT_READ;
                break;
        case VDUSE_ACCESS_RW:
                prot |= PROT_READ | PROT_WRITE;
                break;
        }

        return prot;
}

static void *iova_to_va(int dev_fd, uint64_t iova, uint64_t *len)
{
        int fd;
        void *addr;
        size_t size;
        struct vduse_iotlb_entry entry;

        entry.start = iova;
        entry.last = iova;

        /*
         * Find the first IOVA region that overlaps with the specified
         * range [start, last] and return the corresponding file descriptor.
         */
        fd = ioctl(dev_fd, VDUSE_IOTLB_GET_FD, &entry);
        if (fd < 0)
                return NULL;

        size = entry.last - entry.start + 1;
        *len = entry.last - iova + 1;
        addr = mmap(0, size, perm_to_prot(entry.perm), MAP_SHARED,
                    fd, entry.offset);
        close(fd);
        if (addr == MAP_FAILED)
                return NULL;

        /*
         * Using some data structures such as linked list to store
         * the iotlb mapping. The munmap(2) should be called for the
         * cached mapping when the corresponding VDUSE_UPDATE_IOTLB
         * message is received or the device is reset.
         */

        return addr + iova - entry.start;
}
  1. 使用 VDUSE_VQ_SETUP_KICKFD ioctl 為指定的 virtqueue 設定 kick eventfd。 kick eventfd 由 VDUSE 核心模組用於通知使用者空間使用可用的環。 這是可選的,因為使用者空間可以選擇輪詢可用的環。

  2. 監聽 kick eventfd(可選)並使用可用的環。 在訪問之前,描述符表中的描述符所描述的緩衝區也應透過 VDUSE_IOTLB_GET_FD ioctl 對映到使用者空間。

  3. 在填充已用環後,使用 VDUSE_INJECT_VQ_IRQ ioctl 為特定 virtqueue 注入中斷。

有關 uAPI 的更多詳細資訊,請參閱 include/uapi/linux/vduse.h。