编写 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”)。无论如何,在 probe 返回后,核心会自动启用 virtqueue。

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

散列表(必须格式正确且已终止!)

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

散列表(必须格式正确且已终止!)

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[]

已终止的散列表数组。

unsigned int out_sgs

可由另一端读取的散列表数

unsigned int in_sgs

可写入(在可读列表之后)的散列表数

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

请同时查看该规范的更高版本。