目录
一、概述
二、代码部分
1、Virtio 前端
(1) User Space
(2) Kernel Space
2、Virtio 后端
三、运行
QEMU Version:qemu-7.2.0
Linux Version:linux-5.4.239
一、概述
本篇文章的主要内容是使用Virtio前后端数据传输的机制实现一个计算阶乘的小程序,主要功能是在Virtio driver中传递一个整数到Virtio device,在Virtio device中计算这个整数的阶乘,计算完成后再将计算结果传递给Virtio driver,下面是代码部分。
二、代码部分
代码主要分为两个部分,分别是Virtio前端(Guest Os)和Virtio后端(QEMU),而Virtio前端又分User Space和Kernel Space。
1、Virtio 前端
(1) User Space
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>int main(int argc, char *argv[])
{int fd, retvalue;uint32_t factorial[1];if(argc != 2) {printf("ERROR: please enter two parameters!\n");return -1;}factorial[0] = atoi(argv[1]); /* string to number */fd = open("/dev/virtio_misc", O_RDWR);if(fd < 0) {printf("ERROR: virtio_misc open failed!\n");return -1;}retvalue = write(fd, factorial, sizeof(factorial));if(retvalue < 0) {printf("ERROR: write failed!\r\n");close(fd);return -1;}close(fd);return 0;
}
(2) Kernel Space
linux-5.4.239/drivers/virtio/Makefile
......
obj-y += virtio_test.o
......
linux-5.4.239/include/uapi/linux/virtio_ids.h
#ifndef _LINUX_VIRTIO_IDS_H
#define _LINUX_VIRTIO_IDS_H
/** Virtio IDs**/......#define VIRTIO_ID_TEST 45 /* virtio test */#endif /* _LINUX_VIRTIO_IDS_H */
linux-5.4.239/include/uapi/linux/virtio_test.h
#ifndef _LINUX_VIRTIO_TEST_H_
#define _LINUX_VIRTIO_TEST_H_#include <linux/types.h>
#include <linux/virtio_types.h>
#include <linux/virtio_ids.h>
#include <linux/virtio_config.h>#define VIRTIO_TEST_F_CAN_PRINT 0struct virtio_test_config {__u32 num_pages;__u32 actual;
};struct virtio_test_stat {__virtio16 tag;__virtio64 val;
} __attribute__((packed));#endif
linux-5.4.239/drivers/virtio/virtio_test.c
#include <linux/virtio.h>
#include <linux/virtio_test.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/miscdevice.h>#define MISC_NAME "virtio_misc"
#define MISC_MINOR 144struct test_request {__virtio32 arg1;char arg2[32];
};struct test_response {__virtio32 ret;
};struct virtio_test {struct test_request req;struct test_response res;struct virtio_device *vdev;struct virtqueue *factorial_vq;
};static struct virtio_test *vt_dev;static void print_response_data(struct virtio_test *vt)
{printk("virtio response ret is %d\n",vt->res.ret);
}/* Called from virtio device, in IRQ context */
static void test_request_done(struct virtqueue *vq)
{uint32_t len;struct virtio_test *vt;printk(" %s called, line: %d \n", __func__, __LINE__);do {virtqueue_disable_cb(vq);while ((vt = virtqueue_get_buf(vq, &len)) != NULL) {// request packet will be completed by response packetprint_response_data(vt);}if (unlikely(virtqueue_is_broken(vq)))break;} while (!virtqueue_enable_cb(vq));
}static void build_test_request(struct virtio_test *vt, uint32_t num)
{vt->req.arg1 = num;strncpy(vt->req.arg2, "hello back end!", sizeof(vt->req.arg2));
}static void virtio_test_submit_request(uint32_t num)
{struct virtqueue *vq;struct virtio_test *vt;struct scatterlist out_sg, in_sg, *sgs[2];int num_out = 0, num_in = 0;vt = vt_dev;vq = vt->factorial_vq;build_test_request(vt, num);sg_init_one(&out_sg, &vt->req, sizeof(vt->req));sgs[num_out++] = &out_sg;sg_init_one(&in_sg, &vt->res, sizeof(vt->res));sgs[num_out + num_in++] = &in_sg;/* We should always be able to add one buffer to an empty queue. */virtqueue_add_sgs(vq, sgs, num_out, num_in, vt, GFP_ATOMIC);virtqueue_kick(vq);
}static int init_vqs(struct virtio_test *vt)
{int err, nvqs;struct virtqueue *vqs[1];vq_callback_t *callbacks[] = { test_request_done };const char * const names[] = { "virtio_test"};nvqs = virtio_has_feature(vt->vdev, VIRTIO_TEST_F_CAN_PRINT) ? 1 : 0;err = virtio_find_vqs(vt->vdev, nvqs, vqs, callbacks, names, NULL);if (err)return err;vt->factorial_vq = vqs[0];return 0;
}static void remove_common(struct virtio_test *vt)
{vt->vdev->config->reset(vt->vdev);vt->vdev->config->del_vqs(vt->vdev);
}static int virtio_misc_open(struct inode *inode, struct file *filp)
{return 0;
}static int virtio_misc_release(struct inode *inode, struct file *filp)
{return 0;
}static ssize_t virtio_misc_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos)
{int ret;uint32_t factorial[1];ret = copy_from_user(factorial, buf, count);if(ret < 0)return -EINVAL;virtio_test_submit_request(factorial[0]);return 0;
}struct file_operations virtio_misc_fops = {.owner = THIS_MODULE,.open = virtio_misc_open,.release = virtio_misc_release,.write = virtio_misc_write,
};static struct miscdevice virtio_miscdev = {.minor = MISC_MINOR,.name = MISC_NAME,.fops = &virtio_misc_fops,
};static int virttest_probe(struct virtio_device *vdev)
{int err;struct virtio_test *vt;if (!vdev->config->get) {return -EINVAL;}vdev->priv = vt = kmalloc(sizeof(*vt), GFP_KERNEL);if (!vt) {err = -ENOMEM;goto out;}vt->vdev = vdev;err = init_vqs(vt);if (err)goto out_free_vt;virtio_device_ready(vdev);vt_dev = vt;/* misc driver registered */err = misc_register(&virtio_miscdev);if(err < 0) {printk( "misc register is failed\n");goto out_free_misc;}printk( "misc register has succeeded\n");return 0;out_free_misc:misc_deregister(&virtio_miscdev);
out_free_vt:kfree(vt);
out:return err;
}static void virttest_remove(struct virtio_device *vdev)
{struct virtio_test *vt = vdev->priv;remove_common(vt);kfree(vt);vt_dev = NULL;misc_deregister(&virtio_miscdev);
}static struct virtio_device_id id_table[] = {{ VIRTIO_ID_TEST, VIRTIO_DEV_ANY_ID },{ 0 },
};static unsigned int features[] = {VIRTIO_TEST_F_CAN_PRINT,
};static struct virtio_driver virtio_test_driver = {.feature_table = features,.feature_table_size = ARRAY_SIZE(features),.driver.name = KBUILD_MODNAME,.driver.owner = THIS_MODULE,.id_table = id_table,.probe = virttest_probe,.remove = virttest_remove,
};module_virtio_driver(virtio_test_driver);
MODULE_DEVICE_TABLE(virtio, id_table);
MODULE_DESCRIPTION("Virtio test driver");
MODULE_LICENSE("GPL");
下面对 virtio_test.c 文件中的virtio_test_submit_request函数进行解释,函数如下:
static void virtio_test_submit_request(uint32_t num)
{struct virtqueue *vq;struct virtio_test *vt;struct scatterlist out_sg, in_sg, *sgs[2];int num_out = 0, num_in = 0;vt = vt_dev;vq = vt->factorial_vq;build_test_request(vt, num);sg_init_one(&out_sg, &vt->req, sizeof(vt->req));sgs[num_out++] = &out_sg;sg_init_one(&in_sg, &vt->resp, sizeof(vt->resp));sgs[num_out + num_in++] = &in_sg;/* We should always be able to add one buffer to an empty queue. */virtqueue_add_sgs(vq, sgs, num_out, num_in, vt, GFP_ATOMIC);virtqueue_kick(vq);
}
virtio_test_submit_request函数主要用来构建前端请求包并将数据数据添加到Vring中,然后通知QEMU后端,参数num是用户传递的一个参数,进入到函数里面, build_test_request 函数用来构建请求包。
sg_init_one(&out_sg, &vb->req, sizeof(vb->req));
sgs[num_out++] = &out_sg;
sg_init_one(&in_sg, &vb->res, sizeof(vb->res));
sgs[num_out + num_in++] = &in_sg;
Virtio前后端数据传输是通过Linux内核中的scatter-gather(SG)列表来进行管理的。scatter-gather列表是一种数据结构,用于将多个不连续的内存块组合成一个逻辑上的连续块,以便进行数据传输。
sg_init_one
函数初始化两个SG条目out_sg
和in_sg
,分别指向vb->req
和vb->res
,并设置其大小为sizeof(vb->req)
和sizeof(vb->res)
。vb->req
的内容即为一个请求数据包,用于写入到后端设备,而vb->res
则是用来存放从设备接收到的数据。
sgs[num_out++] = &out_sg
是将out_sg
的地址添加到sgs
数组中。num_out
是一个索引,表示添加到列表中的输出SG条目的数量。通过num_out++
,确保下一个输出SG条目将被添加到数组的下一个位置。
sgs[num_out + num_in++] = &in_sg
则是将in_sg的地址
添加到sgs
数组中,添加的位置是基于已添加的num_out
和num_in
之和,这里num_in 初始化为 0,所以 in_sg
被添加到了out_sg
的后面,在这里sgs
数组的前半部分也就是sgs[0]用于存储输出SG条目,而后半部分sgs[1]用于存储输入SG条目。通过num_in++
,确保下一个输入SG条目被添加到sgs的适当位置。
virtqueue_add_sgs(vq, sgs, num_out, num_in, vt, GFP_ATOMIC);
vq
: 指向一个virtqueue
结构体的指针,这个结构体就是host和guest之间通信的一个虚拟队列。
sgs
: 指向一个scatterlist
结构体数组的指针,表示scatterlist
元素指向内存中的一个物理地址非连续区域,也就是上面填充的sgs[2]数组。
num_out
: 指定了sgs
数组中用于输出的scatterlist
的数量。
num_in
: 指定了sgs
数组中用于输入的scatterlist
的数量。
vt
: struct virtio_test 类型的一个结构体。
GFP_ATOMIC
: 表示这个操作应该在原子上下文中进行,不能睡眠(即不能等待I/O操作或内存分配)。
virtqueue_add_sgs 函数主要将一组散列列表添加到虚拟队列vq
中,而在virtqueue_add_sgs函数中又会调用virtqueue_add函数,用来将新的数据更新到 vring_virtqueue->vring的具体实现。
最后在调用 virtqueue_kick 函数通知QEMU 后端有数据更新了。
2、Virtio 后端
qemu-7.2.0/hw/virtio/meson.build
......virtio_ss.add(when: 'CONFIG_VIRTIO_TEST', if_true: files('virtio-test.c'))......
qemu-7.2.0/hw/virtio/Kconfig
config VIRTIO_TESTbooldefault ydepends on VIRTIO
qemu-7.2.0/include/standard-headers/linux/virtio_ids.h
#ifndef _LINUX_VIRTIO_IDS_H
#define _LINUX_VIRTIO_IDS_H
/** Virtio IDs**/
......#define VIRTIO_ID_TEST 45 /* virtio test */......
#endif /* _LINUX_VIRTIO_IDS_H */
qemu-7.2.0/include/standard-headers/linux/virtio_test.h
#ifndef _LINUX_VIRTIO_TEST_H
#define _LINUX_VIRTIO_TEST_H#include "standard-headers/linux/types.h"
#include "standard-headers/linux/virtio_types.h"
#include "standard-headers/linux/virtio_ids.h"
#include "standard-headers/linux/virtio_config.h"#define VIRTIO_TEST_F_CAN_PRINT 0struct virtio_test_config {uint32_t num_pages;uint32_t actual;uint32_t event;
};struct virtio_test_stat {__virtio16 tag;__virtio64 val;
} QEMU_PACKED;#endif
qemu-7.2.0/include/hw/virtio/virtio-test.h
#ifndef QEMU_VIRTIO_TEST_H
#define QEMU_VIRTIO_TEST_H#include "standard-headers/linux/virtio_test.h"
#include "hw/virtio/virtio.h"#define TYPE_VIRTIO_TEST "virtio-test-device"
#define VIRTIO_TEST(obj) \OBJECT_CHECK(VirtIOTest, (obj), TYPE_VIRTIO_TEST)typedef struct VirtIOTest {VirtIODevice parent_obj;VirtQueue *ivq;uint32_t host_features;QEMUTimer *stats_timer;uint32_t actual;uint32_t event;uint32_t num_pages;size_t stats_vq_offset;VirtQueueElement *stats_vq_elem;
} VirtIOTest;#endif
qemu-7.2.0/hw/virtio/virtio.c
const char *virtio_device_names[] = {......[VIRTIO_ID_TEST] = "virtio-test"
};
qemu-7.2.0/hw/virtio/virtio-test.c
#include "qemu/osdep.h"
#include "qemu/log.h"
#include "qemu/iov.h"
#include "hw/virtio/virtio.h"
#include "sysemu/kvm.h"
#include "sysemu/hax.h"
#include "exec/address-spaces.h"
#include "qapi/error.h"
#include "qapi/qapi-events-misc.h"
#include "qapi/visitor.h"
#include "qemu/error-report.h"
#include "hw/virtio/virtio-bus.h"
#include "hw/virtio/virtio-access.h"
#include "migration/migration.h"#include "hw/virtio/virtio-test.h"static uint32_t Queue_Size = 128;struct test_request {uint32_t arg1;char arg2[32];
};struct test_response {uint32_t ret;
};static uint32_t factorial(uint32_t n) { uint32_t result = 1;for (uint32_t i = 1; i <= n; i++) { result *= i; } return result;
}static void print_req_and_build_resp_pack(struct test_request *req, struct test_response *res)
{ qemu_log("QEMU: >>> get arg1 [ %d ] form the front end <<<\n", req->arg1);qemu_log("QEMU: >>> get arg2 [ %s ] form the front end <<<\n", req->arg2);res->ret = factorial(req->arg1);
}static void virtio_test_handle_output(VirtIODevice *vdev, VirtQueue *vq)
{struct test_request req;struct test_response res;VirtQueueElement *elem;size_t offset = 0;for (;;) {elem = virtqueue_pop(vq, sizeof(VirtQueueElement));if (!elem)return;if (!iov_to_buf(elem->out_sg, elem->out_num, offset, &req, sizeof(req))) {qemu_log("QEMU ERROR: iov_to_buf function failed.\n");virtqueue_detach_element(vq, elem, 0);continue;}print_req_and_build_resp_pack(&req, &res);iov_from_buf(elem->in_sg, elem->in_num, offset, &res, sizeof(res));virtqueue_push(vq, elem, sizeof(res));virtio_notify(vdev, vq);g_free(elem);}
}static void virtio_test_get_config(VirtIODevice *vdev, uint8_t *config_data)
{VirtIOTest *dev = VIRTIO_TEST(vdev);struct virtio_test_config config;config.actual = cpu_to_le32(dev->actual);config.event = cpu_to_le32(dev->event);memcpy(config_data, &config, sizeof(struct virtio_test_config));
}static void virtio_test_set_config(VirtIODevice *vdev,const uint8_t *config_data)
{VirtIOTest *dev = VIRTIO_TEST(vdev);struct virtio_test_config config;memcpy(&config, config_data, sizeof(struct virtio_test_config));dev->actual = le32_to_cpu(config.actual);dev->event = le32_to_cpu(config.event);
}static uint64_t virtio_test_get_features(VirtIODevice *vdev, uint64_t f,Error **errp)
{VirtIOTest *dev = VIRTIO_TEST(vdev);f |= dev->host_features;virtio_add_feature(&f, VIRTIO_TEST_F_CAN_PRINT);return f;
}static void virtio_test_device_realize(DeviceState *dev, Error **errp)
{VirtIODevice *vdev = VIRTIO_DEVICE(dev);VirtIOTest *s = VIRTIO_TEST(dev);virtio_init(vdev, VIRTIO_ID_TEST, sizeof(struct virtio_test_config));s->ivq = virtio_add_queue(vdev, Queue_Size, virtio_test_handle_output);
}static void virtio_test_device_unrealize(DeviceState *dev)
{VirtIODevice *vdev = VIRTIO_DEVICE(dev);virtio_cleanup(vdev);
}static int virtio_test_post_load_device(void *opaque, int version_id)
{return 0;
}static const VMStateDescription vmstate_virtio_test_device = {.name = "virtio-test-device",.version_id = 1,.minimum_version_id = 1,.post_load = virtio_test_post_load_device,.fields = (VMStateField[]) {VMSTATE_UINT32(actual, VirtIOTest),VMSTATE_END_OF_LIST()},
};static const VMStateDescription vmstate_virtio_test = {.name = "virtio-test",.minimum_version_id = 1,.version_id = 1,.fields = (VMStateField[]) {VMSTATE_VIRTIO_DEVICE,VMSTATE_END_OF_LIST()},
};static Property virtio_test_properties[] = {DEFINE_PROP_END_OF_LIST(),
};static void virtio_test_class_init(ObjectClass *klass, void *data)
{DeviceClass *dc = DEVICE_CLASS(klass);VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);dc->props_ = virtio_test_properties;dc->vmsd = &vmstate_virtio_test;set_bit(DEVICE_CATEGORY_MISC, dc->categories);vdc->realize = virtio_test_device_realize;vdc->unrealize = virtio_test_device_unrealize;vdc->get_config = virtio_test_get_config;vdc->set_config = virtio_test_set_config;vdc->get_features = virtio_test_get_features;vdc->vmsd = &vmstate_virtio_test_device;
}static const TypeInfo virtio_test_info = {.name = TYPE_VIRTIO_TEST,.parent = TYPE_VIRTIO_DEVICE,.instance_size = sizeof(VirtIOTest),.class_init = virtio_test_class_init,
};static void virtio_register_types(void)
{type_register_static(&virtio_test_info);
}type_init(virtio_register_types)
下面对virtio-test.c文件中的virtio_test_handle_output函数进行分析,如下:
static void virtio_test_handle_output(VirtIODevice *vdev, VirtQueue *vq)
{struct test_request req;struct test_response res;VirtQueueElement *elem;size_t offset = 0;for (;;) {elem = virtqueue_pop(vq, sizeof(VirtQueueElement));if (!elem)return;if (!iov_to_buf(elem->out_sg, elem->out_num, offset, &req, sizeof(req))) {qemu_log("QEMU ERROR: iov_to_buf function failed.\n");virtqueue_detach_element(vq, elem, 0);continue;}print_req_and_build_resp_pack(&req, &res);iov_from_buf(elem->in_sg, elem->in_num, offset, &res, sizeof(res));virtqueue_push(vq, elem, sizeof(res));virtio_notify(vdev, vq);g_free(elem);}
}
VirtQueueElement 结构体:
typedef struct VirtQueueElement
{unsigned int index;unsigned int len;unsigned int ndescs;unsigned int out_num;unsigned int in_num;hwaddr *in_addr;hwaddr *out_addr;struct iovec *in_sg;struct iovec *out_sg;
} VirtQueueElement;struct iovec {void *iov_base;size_t iov_len;
};
VirtQueueElement 结构体如上所示,in_addr和 out_addr保存的是guest的物理地址,而in_sg和out_sg中的地址是host的虚拟地址,物理地址和虚拟地址之间需要进行映射。
index:
记录该buffer的首个物理内存块对应的描述符在描述符表中的下标,因为一个buffer数据可能由多个物理内存保存。
out_num/in_num:
表示输出和输入块的数量。一个buffer可能包含可读区和可写区,因为一个buffer由多个物理块组成,有的物理块是可读而有的物理块是可写,out_num表示可读块的数量,而in_num表示可写块的数量。
in_addr/out_addr:
记录的是可读块和可写块的物理地址(客户机的物理地址)。因为in_addr/out_addr是客户机的物理地址,如果host要访问这些地址,则需要将Guest物理地址映射成Host的虚拟地址。
in_sg/out_sg:
根据上面的分析,in_sg和out_sg就是保存的对应Guest物理块在Host的虚拟地址和长度。
elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
virtqueue_pop函数主要功能为:
1、以 vq->last_avail_idx为索引从VRingAvail的ring数组中获取一个buffer head索引,并赋值到elem.index,然后获取各个guest物理buffer的相关信息。
2、将可写的物理buffer地址(客户机物理地址)记录到in_addr数组中,而可读的记录到out_addr数组中,并记录in_num和out_num,直到最后一个desc记录完毕。
3、获取完成后再将in_addr和out_addr映射成虚拟地址,并记录到in_sg和out_sg数组中,这样才可以在host中访问到。
调用virtqueue_pop函数之后,QEMU后端就已经获取了buffer的相关信息,继续分析
if (!iov_to_buf(elem->out_sg, elem->out_num, offset, &req, sizeof(req))) {......print_req_and_build_resp_pack(&req, &res);iov_from_buf(elem->in_sg, elem->in_num, offset, &res, sizeof(res));
iov_to_buf
函数用于将 iovec
结构体数组中的数据复制到用户提供的缓冲区中,函数参数解释如下:
elem->out_sg
:指向iovec
结构体数组的指针。
elem->out_num
:指定了elem->out_sg
数组中iovec
结构体的数量。
offset
:指定了从哪个位置开始复制数据。
&req
:存放Guest
前端request
的缓冲区指针,把从iovec
数组中读取的数据复制到这个缓冲区中。
sizeof(req)
:这个参数指定了目标缓冲区req
的大小,即函数最多可以复制多少字节到req
中。
函数会从 elem->out_sg
指向的 iovec
数组开始,跳过 offset
指定的字节数,然后将数据复制到 req
指向的缓冲区中,直到达到 req
的大小限制或所有 iovec
中的数据都被复制完毕为止。
经过前面的分析,输出项out_sg
指向的地址的内容就读取到了req
结构体中,然后读取req结构体中的内容即可读取前端的数据,在这里是调用print_req_and_build_resp_pack函数,获取req中的数据计算阶乘,并初始化好struct test_response
为返回前端数据做准备。
iov_from_buf(elem->in_sg, elem->in_num, offset, &resp, sizeof(resp));
elem->in_sg
:指向一个iovec
数组的指针,用于存储数据的分段信息。
elem->in_num
:表示elem->in_sg
数组中可以使用的iovec的数量。
offset
:从缓冲区开始复制的偏移量。
&resp
:将数据从res复制到iov向量列表中去。
sizeof(resp)
:res
的大小。
和iov_to_buf函数的操作相反,iov_from_buf
函数是将一段数据buf(res)的内容复制到由 iovec
数组描述的内存区域中去,也就是elem->in_sg
中。
到目前为止就完成了根据前端传递来的数据计算阶乘,并将response包放入了in_sg中,然后调用virtqueue_push函数取消之前物理内存映射到虚拟内存的操作,并更新vring_ used表,如下:
void virtqueue_push(VirtQueue *vq, const VirtQueueElement *elem,unsigned int len)
{RCU_READ_LOCK_GUARD();virtqueue_fill(vq, elem, len, 0);virtqueue_flush(vq, 1);
}
最后再调用 virtio_notify 函数告诉前端传递过来的数据已经处理完毕了,然后前端再做一些其它的处理。
三、运行
在运行 qemu 时需要加上 -device virtio-test-device
参数,例如:
......
-machine virt \
-machine gic_version=3 \
-smp 4 \
-m 1024 \
-display none -nographic \
-device virtio-test-device \
......
如果编译成功运行 User Space 程序即可,运行结果如下: