第四章实验:I/O虚拟化
实验一:使用完全设备模拟方式创建一个虚拟PCI设备,并实现和虚拟机的数据传输
设计思路:
- 在宿主机上实现qemu虚拟PCI设备
- 新建虚拟PCI设备“demo”
- 在虚拟设备初始化中提供BAR0的MMIO绑定
- 定义对BAR0读写的监听,提供数据的存取服务
- 在虚拟机中实现PCI设备驱动
- 新建内核态驱动模块,打开“demo”设备
- 监听用户态测试程序读写操作
- 发起对BAR0的数据写入、读取操作,
- 在虚拟机中实现用户态测试程序
- 打开上述设备
- 读写设备来调用驱动,验证虚拟PCI设备可用性
实验过程:
- 在Qemu源码中创建hw/misc/demo.c
static void pci_demo_realize(PCIDevice *pdev, Error **errp)
{
DemoState *demo = DEMO(pdev);
memory_region_init_io(&demo->mmio, OBJECT(demo), &demo_mmio_ops, demo,
"demo-mmio", 1 * MiB);
pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &demo->mmio);
}
设备在初始化的过程中需以MMIO的方式注册BAR0及其共享内存区域。
static uint64_t demo_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
DemoState *demo = opaque;
uint64_t val = ~0ULL;
if (addr != 0x00 || size != 4) {
return val;
}
switch (addr) {
case 0x00:
val = demo->buff;
break;
}
return val;
}
static void demo_mmio_write(void *opaque, hwaddr addr, uint64_t val,
unsigned size)
{
DemoState *demo = opaque;
if (addr != 0x00 || size != 4) {
return;
}
switch (addr) {
case 0x00:
demo->buff = val;
break;
}
}
static const MemoryRegionOps demo_mmio_ops = {
.read = demo_mmio_read,
.write = demo_mmio_write,
.endianness = DEVICE_NATIVE_ENDIAN,
};
分别定义BAR0上的读写操作:针对BAR0上0x00地址的4字节读写,实现简单的数据存取功能。
- 修改misc目录下的Makefile.objs
common-obj-y += demo.o
增加demo.o的编译目标,之后重新编译、安装Qemu。
- 在虚拟机内核中创建驱动源码demo.c
static int demo_probe(struct pci_dev *pdev,
const struct pci_device_id *ent)
{
struct device *dev = &pdev->dev;
int err = -1;
printk("demo_probe() begin\n");
data = kzalloc(sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->pdev = pdev;
err = pci_enable_device(pdev);
if (err) {
dev_err(dev, "Cannot enable PCI device\n");
goto err_kfree;
}
err = pci_request_regions(pdev, DRV_MODULE_NAME);
if (err) {
dev_err(dev, "Cannot obtain PCI resources\n");
goto err_disable_pdev;
}
data->bar0 = pci_iomap(pdev, 0, 128);
if (!data->bar0) {
dev_err(dev, "failed to read BAR0\n");
goto err_disable_bar;
}
pci_set_drvdata(pdev, data);
demo_major = register_chrdev(0, DRV_MODULE_NAME, &demo_fops);
if (demo_major < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
goto err_iounmap;
}
printk("demo_probe() end\n");
return 0;
//...
}
在内核模块的probe函数中依次:初始化驱动数据结构体、注册设备、请求PCI资源、映射BAR0内存、绑定数据结构体、注册字符设备。
static ssize_t demo_read(struct file *fp, char __user *ubuf,
size_t len, loff_t *off)
{
//...
val = readl(data->bar0);
ret = copy_to_user(ubuf, &val, 4);
//...
}
static ssize_t demo_write(struct file *fp, const char __user *ubuf,
size_t len, loff_t *off)
{
//...
ret = copy_from_user(&val, ubuf, 4);
//...
writel(val, data->bar0);
//...
}
static const struct file_operations demo_fops =
{
.owner = THIS_MODULE,
.open = demo_open,
.release = demo_release,
.read = demo_read,
.write = demo_write,
};
其中,demo_fops指定了驱动作为字符设备,在被读写时的回调函数,用于从用户态进行功能调用。
- 创建用户态测试程序test.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE "/dev/demo"
int main(void)
{
int fd = -1;
unsigned int val;
fd = open(FILE, O_RDWR);
if (fd < 0)
{
printf("open %s error.\n", FILE);
return -1;
}
printf("open %s success..\n", FILE);
val = 0x1333;
write(fd, &val, 4);
printf("0. write 1333\n");
read(fd, &val, 4);
printf("1. read = %x\n", val);
val = 0x2333;
write(fd, &val, 4);
printf("2. write 2333\n");
read(fd, &val, 4);
printf("3. read = %x\n", val);
val = 0x3333;
write(fd, &val, 4);
printf("4. write 3333\n");
read(fd, &val, 4);
printf("5. read = %x\n", val);
close(fd);
return 0;
}
通过open、write、read对驱动模块功能进行调用。
- 设计Makefile进行虚拟机中测试用驱动模块和应用程序编译
CONFIG_MODULE_SIG=n
obj-m += demo.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
gcc test.c -o test
clean:
rm -f *.o *.mod.c *.ko test
all-clean:
rm -f *.o *.mod.c *.ko Module.symvers modules.order test
运行make
命令即可编译驱动模块和用户态测试程序。
- 功能测试运行
# 启动虚拟机
qemu-system-x86_64 -smp 4 -m 4096 --enable-kvm --device demo guest.img
# --device demo参数将demo设备挂载到虚拟机上
# 连接vnc
vncviewer :0
# 进入exp1目录,编译实验代码
cd exp1/guest-driver
make
# 安装驱动模块
sudo insmod demo.ko
# 查看注册的设备编号
cat /proc/device | grep demo
# 输出:
# 248 demo
# 给编号248的驱动创建设备文件
sudo mknod /dev/demo c 248 0
# 运行用户态测试程序
sudo ./test
# 检查内核输出
dmesg
用户态测试程序运行结果、dmesg输出如下:

实验二:使用virtio方式创建一个虚拟外设,并实现和虚拟机的数据传输
设计思路:
- 在宿主机qemu中实现虚拟设备
- 新建virtio虚拟设备“virtio-demo”
- 定义virtio虚拟设备类型QOM,继承自TYPE_DEVICE->TYPE_VIRTIO_DEVICE->TYPE_VIRTIO_DEMO
- 具像化过程中,virtio设备添加queue
- 在宿主机qemu中实现代理设备
- 新建代理设备“virtio-demo-pci”
- 定义代理设备类型QOM,继承自TYPE_DEVICE->TYPE_PCI_DEVICE->TYPE_VIRTIO_PCI->TYPE_VIRTIO_DEMO_PCI
- 具像化过程中,代理设备设置virtio总线上的virtio设备
- 在虚拟机中实现virtio设备驱动
- 新建virtio虚拟设备驱动“virtio-demo”
- 设置对virtio的读写操作
- 监听用户态测试程序的读写操作,执行相关功能
- 在虚拟机中实现用户态测试程序
- 新建用户态测试程序“test”
- 打开上述virtio设备
- 读写设备来调用驱动,验证虚拟PCI设备可用性
实验过程:
- 在Qemu源码中增加hw/virtio/virtio-demo-pci.c
static void virtio_demo_pci_realize(VirtIOPCIProxy *vpci_dev, Error **errp)
{
VirtIODemoPCI *dev = VIRTIO_DEMO_PCI(vpci_dev);
DeviceState *vdev = DEVICE(&dev->vdev);
qdev_set_parent_bus(vdev, BUS(&vpci_dev->bus));
object_property_set_bool(OBJECT(vdev), true, "realized", errp);
}
在realize函数中将virtio-demo-pci设备挂载到PCI总线。
static void virtio_demo_pci_instance_init(Object *obj)
{
VirtIODemoPCI *dev = VIRTIO_DEMO_PCI(obj);
virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev),
TYPE_VIRTIO_DEMO);
}
在instance_init函数中调用virtio_instance_init_common初始化virtio-demo设备,并将其挂载到virtio总线。
- 在Qemu源码中增加hw/virtio/virtio-demo.c,以及对相关头文件进行修改
static void virtio_demo_device_realize(DeviceState *dev, Error **errp)
{
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
VirtIODemo *s = VIRTIO_DEMO(dev);
virtio_init(vdev, "virtio-demo", VIRTIO_ID_DEMO,
sizeof(struct virtio_demo_config));
s->vq = virtio_add_queue(vdev, 128, virtio_demo_handle);
s->buff = 0x1333;
}
在realize函数中调用virtio_init进行virtio设备初始化,调用virtio_add_queue添加一条vring消息通道,使用virtio_demo_handle函数处理队列上的回调。
对s->buff的赋值用于后续通信测试。
static void virtio_demo_handle(VirtIODevice *vdev, VirtQueue *vq)
{
VirtIODemo *s = VIRTIO_DEMO(vdev);
VirtQueueElement *elem;
uint64_t data;
uint32_t *opt_p = (uint32_t *)&data;
uint32_t *buff_p = (uint32_t *)((void *)&data + 4);
int len = 0;
for (;;) {
elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
if (!elem) {
printf("exit virtio_demo_handle()\n");
return;
}
if (iov_to_buf(elem->out_sg, elem->out_num, 0, &data, 8) == 8) {
printf("recv opt: %x\n", *opt_p);
printf("recv buff: %x\n", *buff_p);
switch (*opt_p) {
case 0:
printf("opt: read from guest\n");
*buff_p = s->buff;
break;
case 1:
printf("opt: write from guest\n");
s->buff = *buff_p;
break;
default:
printf("opt: error!\n");
}
len = iov_from_buf(elem->in_sg, elem->in_num, 0, &data, 8);
printf("iov_from_buf ret = %d\n", len);
printf("buff: %x\n", s->buff);
}
virtqueue_push(vq, elem, 8);
virtio_notify(vdev, vq);
g_free(elem);
}
}
virtio_demo_handle函数用于处理队列上的回调。
for (;;) {
elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
if (!elem) {
return;
}
iov_to_buf(elem->out_sg, elem->out_num, 0, &data, 8);
iov_from_buf(elem->in_sg, elem->in_num, 0, &data, 8);
virtqueue_push(vq, elem, 8);
virtio_notify(vdev, vq);
g_free(elem);
}
上述代码为virtqueue队列回调函数的常用代码框架:
- virtqueue_pop函数从队列中获取队列元素;
- iov_to_buf函数从队列元素中获取数据到缓冲区;
- iov_from_buf函数将数据从缓冲区中写入队列元素;
- virtqueue_push函数将队列元素推入队列中;
- virtio_notify函数发送中断通知对端有新的消息;
对于本实验的业务逻辑:
- 消息队列元素中存放4字节操作码和4字节的操作数;
- 操作码为0代表客户机向外设发起的读操作,将s->buff内容写人操作数缓冲区;
- 操作码为1代表客户机向外设发起的写操作,将操作数缓冲区内容写入s->buff中。
- 修改virtio目录下的Makefile.objs
obj-y += virtio-demo.o
obj-y += virtio-demo-pci.o
增加virtio-demo.o和virtio-demo-pci.o编译目标,之后重新编译、安装Qemu。
- 在虚拟机内核中创建驱动源码virtio_demo.c
static int virtdemo_probe(struct virtio_device *vdev)
{
struct virtio_demo *vd;
int err = ~0;
printk("virtdemo_probe() begin\n");
sema_init(&demo_io_sem, 0);
mutex_init(&demo_io_mutex);
demo_major = register_chrdev(0, DRV_MODULE_NAME, &demo_fops);
if (demo_major < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
goto out;
}
vdev->priv = vd = kmalloc(sizeof(*vd), GFP_KERNEL);
if (!vd) {
err = -ENOMEM;
goto out_uunregister_chrdev;
}
global_data = vd;
vd->vdev = vdev;
vd->vq = virtio_find_single_vq(vd->vdev, demo_handle_request, "demo-vq");
if (!vd->vq) {
goto out_free_vd;
}
virtio_device_ready(vdev);
printk("virtdemo_probe() end\n");
return 0;
//...
}
在virtio-demo的驱动模块的probe函数中,分别执行以下操作:
- 初始化读写互斥锁和信号量
- 注册驱动模块的字符设备
- 初始化驱动模块数据结构
- 调用virtio_find_single_vq函数匹配virtqueue消息队列
- 启用virtio设备
static void demo_handle_request(struct virtqueue *vq)
{
uint32_t *data_p = NULL;
uint32_t *opt_p = NULL;
uint32_t *buff_p = NULL;
int len;
printk("demo_handle_request() begin\n");
data_p = virtqueue_get_buf(vq, &len);
if (!data_p)
return;
opt_p = data_p + 2;
buff_p = opt_p + 1;
printk("check ret opt = %x\n", *opt_p);
printk("check ret buff = %x\n", *buff_p);
val = *buff_p;
up(&demo_io_sem);
printk("demo_io_sem up\n");
printk("demo_handle_request() end\n");
}
消息队列在驱动模块侧的回调函数设置为demo_handle_request。其中,调用virtqueue_get_buf获取读写缓冲区,缓冲区的数据结构与iov_to_buf函数、iov_from_buf函数、virtqueue_add_sgs函数中所使用的缓冲区相对应。
static void send_kick(struct virtio_demo *vd, uint32_t v1, uint32_t v2)
{
struct scatterlist sg0, sg1, *sgs[2];
printk("send_kick() begin\n");
vd->opt = v1;
vd->buff = v2;
vd->opt2 = 0;
vd->buff2 = 0;
sg_init_one(&sg0, &vd->opt, 8);
sg_init_one(&sg1, &vd->opt2, 8);
sgs[0] = &sg0;
sgs[1] = &sg1;
virtqueue_add_sgs(vd->vq, sgs, 1, 1, &vd->opt, GFP_ATOMIC);
virtqueue_kick(vd->vq);
down_interruptible(&demo_io_sem);
printk("demo_io_sem down\n");
printk("send_kick() end\n");
}
send_kick是对驱动上消息发送功能的封装:
- 两次调用sg_init_one分别初始化作为输入、输出使用的scatterlist数据结构
- virtqueue_add_sgs函数可用于将多个scatterlist结构(可分别用于输入、输出)同时压入消息队列中
- 调用virtqueue_kick函数发送中断通知对端
send_kick与demo_handle_request设置有同步信号量:
-
信号量demo_io_sem在初始化时被置为0
- 在send_kick运行结束时,调用down_interruptible获取信号量demo_io_sem
- 由于此时信号量为0,send_kick函数产生阻塞,进而demo_read/demo_write函数产生阻塞,无法返回
- 在demo_handle_request被调用,并完成主体工作后,调用up释放信号量demo_io_sem
- send_kick函数获取到信号量,继续运行,随后demo_read/demo_write向用户态返回结果
- 创建用户态测试程序test.c
用户态测试程序的实现与实验一类似,此处不作赘述。
- 设计Makefile进行虚拟机中测试用驱动模块和应用程序编译
Makefile的设计与实验一类似,此处不作赘述。
运行make
命令即可编译驱动模块。
- 功能测试运行
# 启动虚拟机
qemu-system-x86_64 -smp 4 -m 4096 --enable-kvm --device virtio-demo-pci guest.img
# --device virtio-demo-pci参数将virtio-demo-pci设备挂载到虚拟机的PCI总线上,virtio-demo设备随之被挂载到virtio总线
# 连接vnc
vncviewer :0
# 进入exp2目录,编译实验代码
cd exp2/guest-driver
make
# 安装驱动模块
sudo insmod virtio_demo.ko
# 查看注册的设备编号
cat /proc/device | grep virtio_demo
# 输出:
# 248 virtio_demo
# 给编号248的驱动创建设备文件
sudo mknod /dev/virtio_demo c 248 0
# 运行用户态测试程序
sudo ./test
# 检查内核输出
dmesg
实验运行结果如下:

部分dmesg输出如下:

实验三:使用vhost方式创建一个虚拟外设,并实现和虚拟机的数据传输
设计思路:
- 在Qemu中实现 vhost_demo虚拟外设
- 在宿主机内核中实现vhost_demo虚拟外设的后端内核态部分
- 在虚拟机中沿用实验二中的内核态驱动模块virtio-demo和用户态测试程序test
实验过程:
- 基于实验二中的qemu侧virtio后端virtio_demo.c进行修改
修改virtio_demo_device_realize,即virtio_demo后端设备初始化函数
static void vhost_demo_handle(VirtIODevice *vdev, VirtQueue *vq)
{
}
static void virtio_demo_device_realize(DeviceState *dev, Error **errp)
{
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
VirtIODemo *d = VIRTIO_DEMO(dev);
VhostDemoOptions options;
int vhostfd;
printf("virtio_demo_device_realize() begin\n");
virtio_init(vdev, "virtio-demo", VIRTIO_ID_DEMO,
sizeof(struct virtio_demo_config));
d->vq = virtio_add_queue(vdev, 128, vhost_demo_handle);
d->dc = g_malloc0(sizeof(DemoClientState));
options.backend_type = VHOST_BACKEND_TYPE_KERNEL;
options.dc = d->dc;
vhostfd = open("/dev/vhost-demo", O_RDWR);
if (vhostfd < 0) {
warn_report("open vhost-demo char device failed: %s",
strerror(errno));
exit(1);
}
qemu_set_nonblock(vhostfd);
options.opaque = (void *)(uintptr_t)vhostfd;
if (!vhost_demo_init(&options)) {
warn_report(VHOST_DEMO_INIT_FAILED);
exit(1);
}
printf("virtio_demo_device_realize() end normal\n");
}
其中,DemoClientState为vhost设备后端在virtio设备中的状态结构体,因此需要在virtio设备初始化过程中进行结构体的初始化。随后调用open函数打开vhost-demo模块对应的后端设备文件。包括状态结构体、后端设备文件描述符在内的参数被放入参数结构体中,传入vhost_demo_init函数进行进一步的vhost相关初始化。
static void virtio_demo_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);
dc->vmsd = &vmstate_virtio_demo;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
vdc->realize = virtio_demo_device_realize;
vdc->unrealize = virtio_demo_device_unrealize;
vdc->get_features = virtio_demo_get_features;
vdc->set_status = virtio_demo_set_status;
vdc->guest_notifier_mask = virtio_demo_guest_notifier_mask;
vdc->guest_notifier_pending = virtio_demo_guest_notifier_pending;
}
virtio_demo_class_init函数中,增加了多项回调函数。与vhost最为相关的是set_status回调函数。在虚拟机启动过程中,一旦vhost前端驱动准备完毕,即产生一个set_status,此时将调用virtio_demo_set_status函数,对vhost后端做进一步的初始化和激活。
static void virtio_demo_vhost_status(VirtIODemo *d, uint8_t status)
{
VirtIODevice *vdev = VIRTIO_DEVICE(d);
DemoClientState *dc = d->dc;
printf("virtio_demo_vhost_status() begin\n");
if (!get_vhost_demo(dc)) {
printf("virtio_demo_vhost_status() return 1\n");
return;
}
if ((virtio_demo_started(d, status)) == !!d->vhost_started) {
printf("virtio_demo_vhost_status() return 2\n");
return;
}
if (!d->vhost_started) {
int r;
d->vhost_started = 1;
r = vhost_demo_start(vdev, dc);
printf("virtio_demo_vhost_status() after vhost_demo_start\n");
if (r < 0) {
error_report("unable to start vhost crypto: %d: "
"falling back on userspace virtio", -r);
d->vhost_started = 0;
}
} else {
vhost_demo_stop(vdev, dc);
d->vhost_started = 0;
}
printf("virtio_demo_vhost_status() end normal\n");
}
static void virtio_demo_set_status(VirtIODevice *vdev, uint8_t status)
{
VirtIODemo *dev = VIRTIO_DEMO(vdev);
virtio_demo_vhost_status(dev, status);
}
其中,此处判断在vhost后端还未启动的情况下,调用vhost_demo_start函数进行vhost后端启动,vhost_demo_start函数依次调用set_guest_notifiers、vhost_dev_enable_notifiers、vhost_dev_start、vhost_set_vring_enable开启对vring监听。
- 创建宿主机vhost后端驱动内核模块demo.c
static int vhost_demo_open(struct inode *inode, struct file *f)
{
struct vhost_demo *d;
printk("vhost_demo: vhost_demo_open() begin\n");
d = kvmalloc(sizeof *d, GFP_KERNEL | __GFP_RETRY_MAYFAIL);
if (!d)
return -ENOMEM;
d->vqs[0] = &d->vq;
d->vq.handle_kick = handle_demo_kick;
vhost_dev_init(&d->dev, d->vqs, 1,
UIO_MAXIOV,
VHOST_DEMO_PKT_WEIGHT, VHOST_DEMO_WEIGHT, true,
NULL);
printk("vhost_demo: vhost_demo_open() vhost_dev_init finish\n");
f->private_data = d;
printk("vhost_demo: vhost_demo_open() end\n");
return 0;
}
其中,vhost_demo为后端主要结构体,其中维护了一个virtqueue结构体的列表,这里的demo中我们只使用了一个virtqueue队列,同时对其handle_kick函数进行赋值,确定接受到队列上的消息时回调的函数。调用vhost_dev_init函数执行vhost设备初始化。
static void handle_demo_kick(struct vhost_work *work)
{
struct vhost_virtqueue *vq = container_of(work, struct vhost_virtqueue,
poll.work);
struct vhost_demo *demo = container_of(vq->dev, struct vhost_demo, dev);
int head;
unsigned in, out;
size_t out_len, in_len, total_len = 0;
struct iov_iter iov_iter;
printk("vhost_demo: handle_demo_kick() begin\n");
mutex_lock(&vq->mutex);
vhost_disable_notify(&demo->dev, vq);
printk("vhost_demo: handle_demo_kick() mutex_lock and vhost_disable_notify\n");
for (;;) {
head = vhost_get_vq_desc(vq, vq->iov,
ARRAY_SIZE(vq->iov),
&out, &in,
NULL, NULL);
if (unlikely(head < 0)) {
printk("vhost_demo: handle_demo_kick() for(;;) break1\n");
break;
}
/* Nothing new? Wait for eventfd to tell us they refilled. */
if (head == vq->num) {
if (unlikely(vhost_enable_notify(&demo->dev, vq))) {
vhost_disable_notify(&demo->dev, vq);
continue;
}
printk("vhost_demo: handle_demo_kick() for(;;) break2\n");
break;
}
printk("vhost_demo: handle_demo_kick(), out=%u in=%u\n", out, in);
out_len = iov_length(vq->iov, out);
in_len = iov_length(&vq->iov[out], in);
printk("vhost_demo: handle_demo_kick(), out_len=%lu in_len=%lu\n",
out_len, in_len);
iov_iter_init(&iov_iter, WRITE, vq->iov, out, out_len);
copy_from_iter(&demo->v1, 8, &iov_iter);
demo->v3 = demo->v1;
if (1 == demo->v1) {
demo->v4 = demo->v2;
}
iov_iter_init(&iov_iter, READ, &vq->iov[out], in, in_len);
copy_to_iter(&demo->v3, 8, &iov_iter);
printk("vhost_demo: handle_demo_kick(), handle finish, v1-v4 = %u %u %u %u\n",
demo->v1, demo->v2, demo->v3, demo->v4);
vhost_add_used_and_signal(&demo->dev, vq, head, 0);
total_len += (out_len + in_len);
if (unlikely(vhost_exceeds_weight(vq, 0, total_len))) {
break;
}
}
mutex_unlock(&vq->mutex);
vhost_poll_queue(&vq->poll);
printk("vhost_demo: handle_demo_kick() end\n");
}
handle_demo_kick是vhost demo接收到队列中消息(即kick)时的回调函数。mutex_lock确保临界代码区间保持单例。vhost_disable_notify临时关闭virtqueue队列消息通知。随后进入无限循环,获取virtqueue队列内容。vhost_get_vq_desc获得队列中元素的描述符,iov_iter_init对队列中元素进行遍历,copy_from_iter、copy_to_iter进行队列中信息的获取和补充。最后通过vhost_add_used_and_signal函数标记队列中元素被使用,并向前端发送信号。循环会在vhost_get_vq_desc无法获取到新的描述符时退出,vhost_enable_notify重新打开队列的消息通知,并调用vhost_poll_queue开启监听。
- 设计Makefile进行宿主机中vhost模块编译
Makefile的设计与实验一类似,此处不作赘述。
- 创建虚拟机中测试程序
虚拟机测试程序完全复用实验二中的virtio前端模块和用户态程序,此处不作赘述。
- 设计Makefile进行虚拟机中测试用驱动模块和应用程序编译
Makefile的设计完全复用实验二中的对应Makefile,此处不作赘述。
- 功能测试运行
# 进入exp3目录,编译vhost后端驱动,并安装
cd exp3/host-driver
make
sudo insmod demo.ko
# 以下操作与实验二相同
# 启动虚拟机
qemu-system-x86_64 -smp 4 -m 4096 --enable-kvm --device virtio-demo-pci guest.img
# --device virtio-demo-pci参数将virtio-demo-pci设备挂载到虚拟机的PCI总线上,virtio-demo设备随之被挂载到virtio总线
# 连接vnc
vncviewer :0
# 进入exp3目录,编译实验代码
cd exp3/guest-driver
make
# 安装驱动模块
sudo insmod virtio_demo.ko
# 查看注册的设备编号
cat /proc/device | grep virtio_demo
# 输出:
# 248 virtio_demo
# 给编号248的驱动创建设备文件
sudo mknod /dev/virtio_demo c 248 0
# 运行用户态测试程序
sudo ./test
# 检查内核输出
dmesg
实验运行结果如下:
