编写 Virtio 驱动程序

简介

本文档为需要修改新的 virtio 驱动程序或理解现有驱动程序基本原理的驱动程序程序员提供基本指南。有关 virtio 的一般概述,请参阅 Linux 上的 Virtio

驱动程序样板

作为一个最低要求,virtio 驱动程序需要在 virtio 总线中注册,并根据其规范为设备配置 virtqueue,驱动程序端的 virtqueue 配置必须与设备中的 virtqueue 定义匹配。一个基本的驱动程序框架可能如下所示

#include <linux/virtio.h>
#include <linux/virtio_ids.h>
#include <linux/virtio_config.h>
#include <linux/module.h>

/* device private data (one per device) */
struct virtio_dummy_dev {
        struct virtqueue *vq;
};

static void virtio_dummy_recv_cb(struct virtqueue *vq)
{
        struct virtio_dummy_dev *dev = vq->vdev->priv;
        char *buf;
        unsigned int len;

        while ((buf = virtqueue_get_buf(dev->vq, &len)) != NULL) {
                /* process the received data */
        }
}

static int virtio_dummy_probe(struct virtio_device *vdev)
{
        struct virtio_dummy_dev *dev = NULL;

        /* initialize device data */
        dev = kzalloc(sizeof(struct virtio_dummy_dev), GFP_KERNEL);
        if (!dev)
                return -ENOMEM;

        /* the device has a single virtqueue */
        dev->vq = virtio_find_single_vq(vdev, virtio_dummy_recv_cb, "input");
        if (IS_ERR(dev->vq)) {
                kfree(dev);
                return PTR_ERR(dev->vq);

        }
        vdev->priv = dev;

        /* from this point on, the device can notify and get callbacks */
        virtio_device_ready(vdev);

        return 0;
}

static void virtio_dummy_remove(struct virtio_device *vdev)
{
        struct virtio_dummy_dev *dev = vdev->priv;

        /*
         * disable vq interrupts: equivalent to
         * vdev->config->reset(vdev)
         */
        virtio_reset_device(vdev);

        /* detach unused buffers */
        while ((buf = virtqueue_detach_unused_buf(dev->vq)) != NULL) {
                kfree(buf);
        }

        /* remove virtqueues */
        vdev->config->del_vqs(vdev);

        kfree(dev);
}

static const struct virtio_device_id id_table[] = {
        { VIRTIO_ID_DUMMY, VIRTIO_DEV_ANY_ID },
        { 0 },
};

static struct virtio_driver virtio_dummy_driver = {
        .driver.name =  KBUILD_MODNAME,
        .id_table =     id_table,
        .probe =        virtio_dummy_probe,
        .remove =       virtio_dummy_remove,
};

module_virtio_driver(virtio_dummy_driver);
MODULE_DEVICE_TABLE(virtio, id_table);
MODULE_DESCRIPTION("Dummy virtio driver");
MODULE_LICENSE("GPL");

这里的设备 ID VIRTIO_ID_DUMMY 是一个占位符,virtio 驱动程序应该仅为规范中定义的设备添加,参见 include/uapi/linux/virtio_ids.h。设备 ID 至少需要在 virtio 规范中保留,然后才能添加到该文件中。

如果您的驱动程序在其 initexit 方法中不需要做任何特别的事情,您可以使用 module_virtio_driver() 助手来减少样板代码的数量。

在这种情况下,probe 方法执行最小的驱动程序设置(为设备数据分配内存)并初始化 virtqueue。virtio_device_ready() 用于启用 virtqueue 并通知设备驱动程序已准备好管理设备(“DRIVER_OK”)。无论如何,virtqueue 会在 probe 返回后由核心自动启用。

void virtio_device_ready(struct virtio_device *dev)

在 probe 函数中启用 vq 的使用

参数

struct virtio_device *dev

virtio 设备

描述

驱动程序必须调用此函数才能在 probe 函数中使用 vq。

注意

vq 在 probe 返回后自动启用。

无论如何,在向 virtqueue 添加缓冲区之前,需要先启用它们。

发送和接收数据

当设备在完成对描述符或描述符链的处理后(无论是读取还是写入)通知驱动程序时,将触发上述代码中的 virtio_dummy_recv_cb() 回调。但是,这只是 virtio 设备-驱动程序通信过程的后半部分,因为通信始终由驱动程序启动,而与数据传输的方向无关。

要配置从驱动程序到设备的缓冲区传输,首先必须使用 virtqueue_add_inbuf()virtqueue_add_outbuf()virtqueue_add_sgs() 中的任何一个,将缓冲区(打包为 scatterlists)添加到适当的 virtqueue 中,具体取决于您是否需要添加一个输入 scatterlist(供设备填充)、一个输出 scatterlist(供设备使用)或多个 scatterlists。然后,一旦 virtqueue 设置好,调用 virtqueue_kick() 会发送一个通知,该通知将由实现该设备的虚拟机监控程序提供服务

struct scatterlist sg[1];
sg_init_one(sg, buffer, BUFLEN);
virtqueue_add_inbuf(dev->vq, sg, 1, buffer, GFP_ATOMIC);
virtqueue_kick(dev->vq);
int virtqueue_add_inbuf(struct virtqueue *vq, struct scatterlist *sg, unsigned int num, void *data, gfp_t gfp)

将输入缓冲区暴露给另一端

参数

struct virtqueue *vq

我们正在讨论的 struct virtqueue

struct scatterlist *sg

scatterlist(必须是格式良好且终止的!)

unsigned int num

sg 中可由另一方写入的条目数

void *data

用于标识缓冲区的令牌。

gfp_t gfp

如何执行内存分配(如果需要)。

描述

调用者必须确保我们不会同时使用其他 virtqueue 操作调用此函数(除非另有说明)。

返回零或负错误(即 ENOSPC、ENOMEM、EIO)。

int virtqueue_add_outbuf(struct virtqueue *vq, struct scatterlist *sg, unsigned int num, void *data, gfp_t gfp)

将输出缓冲区暴露给另一端

参数

struct virtqueue *vq

我们正在讨论的 struct virtqueue

struct scatterlist *sg

scatterlist(必须是格式良好且终止的!)

unsigned int num

sg 中可由另一方读取的条目数

void *data

用于标识缓冲区的令牌。

gfp_t gfp

如何执行内存分配(如果需要)。

描述

调用者必须确保我们不会同时使用其他 virtqueue 操作调用此函数(除非另有说明)。

返回零或负错误(即 ENOSPC、ENOMEM、EIO)。

int virtqueue_add_sgs(struct virtqueue *_vq, struct scatterlist *sgs[], unsigned int out_sgs, unsigned int in_sgs, void *data, gfp_t gfp)

将缓冲区暴露给另一端

参数

struct virtqueue *_vq

我们正在讨论的 struct virtqueue

struct scatterlist *sgs[]

已终止的 scatterlists 数组。

unsigned int out_sgs

可由另一方读取的 scatterlists 的数量

unsigned int in_sgs

可写入的 scatterlists 的数量(在可读的 scatterlists 之后)

void *data

用于标识缓冲区的令牌。

gfp_t gfp

如何执行内存分配(如果需要)。

描述

调用者必须确保我们不会同时使用其他 virtqueue 操作调用此函数(除非另有说明)。

返回零或负错误(即 ENOSPC、ENOMEM、EIO)。

然后,在设备读取或写入驱动程序准备的缓冲区并将其通知回来之后,驱动程序可以调用 virtqueue_get_buf() 来读取设备生成的数据(如果 virtqueue 是用输入缓冲区设置的),或者只是回收已被设备使用的缓冲区

void *virtqueue_get_buf_ctx(struct virtqueue *_vq, unsigned int *len, void **ctx)

获取下一个已使用的缓冲区

参数

struct virtqueue *_vq

我们正在讨论的 struct virtqueue

unsigned int *len

写入缓冲区中的长度

void **ctx

令牌的额外上下文

描述

如果设备将数据写入缓冲区,则 len 将设置为写入的数量。这意味着您无需事先清除缓冲区,以确保在短写入的情况下不会发生数据泄漏。

调用者必须确保我们不会同时使用其他 virtqueue 操作调用此函数(除非另有说明)。

如果没有已使用的缓冲区,则返回 NULL,否则返回传递给 virtqueue_add_*() 的“data”令牌。

可以使用 virtqueue_disable_cb()virtqueue_enable_cb() 系列函数分别禁用和重新启用 virtqueue 回调。有关更多详细信息,请参见 drivers/virtio/virtio_ring.c

void virtqueue_disable_cb(struct virtqueue *_vq)

禁用回调

参数

struct virtqueue *_vq

我们正在讨论的 struct virtqueue

描述

请注意,这不一定是同步的,因此不可靠,仅作为一种优化有用。

与其他操作不同,这不需要序列化。

bool virtqueue_enable_cb(struct virtqueue *_vq)

在 disable_cb 之后重新启动回调。

参数

struct virtqueue *_vq

我们正在讨论的 struct virtqueue

描述

这将重新启用回调;如果队列中存在挂起的缓冲区,则返回 “false”,以检测驱动程序检查更多工作和启用回调之间可能存在的竞争。

调用者必须确保我们不会同时使用其他 virtqueue 操作调用此函数(除非另有说明)。

但请注意,在某些情况下仍然可以触发一些虚假回调。可靠地禁用回调的方法是重置设备或 virtqueue (virtio_reset_device())。

参考文献

[1] Virtio Spec v1.2: https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html

也请检查规范的更高版本。