編寫 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

也請檢查規範的更高版本。