第四章实验:I/O虚拟化

实验一:使用完全设备模拟方式创建一个虚拟PCI设备,并实现和虚拟机的数据传输

设计思路:

  1. 在宿主机上实现qemu虚拟PCI设备
    • 新建虚拟PCI设备“demo”
    • 在虚拟设备初始化中提供BAR0的MMIO绑定
    • 定义对BAR0读写的监听,提供数据的存取服务
  2. 在虚拟机中实现PCI设备驱动
    • 新建内核态驱动模块,打开“demo”设备
    • 监听用户态测试程序读写操作
    • 发起对BAR0的数据写入、读取操作,
  3. 在虚拟机中实现用户态测试程序
    • 打开上述设备
    • 读写设备来调用驱动,验证虚拟PCI设备可用性

实验过程:

  1. 在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字节读写,实现简单的数据存取功能。

  1. 修改misc目录下的Makefile.objs
common-obj-y += demo.o

增加demo.o的编译目标,之后重新编译、安装Qemu。

  1. 在虚拟机内核中创建驱动源码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指定了驱动作为字符设备,在被读写时的回调函数,用于从用户态进行功能调用。

  1. 创建用户态测试程序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对驱动模块功能进行调用。

  1. 设计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命令即可编译驱动模块和用户态测试程序。

  1. 功能测试运行
# 启动虚拟机
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输出如下:

exp1

实验二:使用virtio方式创建一个虚拟外设,并实现和虚拟机的数据传输

设计思路:

  1. 在宿主机qemu中实现虚拟设备
    • 新建virtio虚拟设备“virtio-demo”
    • 定义virtio虚拟设备类型QOM,继承自TYPE_DEVICE->TYPE_VIRTIO_DEVICE->TYPE_VIRTIO_DEMO
    • 具像化过程中,virtio设备添加queue
  2. 在宿主机qemu中实现代理设备
    • 新建代理设备“virtio-demo-pci”
    • 定义代理设备类型QOM,继承自TYPE_DEVICE->TYPE_PCI_DEVICE->TYPE_VIRTIO_PCI->TYPE_VIRTIO_DEMO_PCI
    • 具像化过程中,代理设备设置virtio总线上的virtio设备
  3. 在虚拟机中实现virtio设备驱动
    • 新建virtio虚拟设备驱动“virtio-demo”
    • 设置对virtio的读写操作
    • 监听用户态测试程序的读写操作,执行相关功能
  4. 在虚拟机中实现用户态测试程序
    • 新建用户态测试程序“test”
    • 打开上述virtio设备
    • 读写设备来调用驱动,验证虚拟PCI设备可用性

实验过程:

  1. 在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总线。

  1. 在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中。
  1. 修改virtio目录下的Makefile.objs
obj-y += virtio-demo.o
obj-y += virtio-demo-pci.o

增加virtio-demo.o和virtio-demo-pci.o编译目标,之后重新编译、安装Qemu。

  1. 在虚拟机内核中创建驱动源码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向用户态返回结果
  1. 创建用户态测试程序test.c

用户态测试程序的实现与实验一类似,此处不作赘述。

  1. 设计Makefile进行虚拟机中测试用驱动模块和应用程序编译

Makefile的设计与实验一类似,此处不作赘述。

运行make命令即可编译驱动模块。

  1. 功能测试运行
# 启动虚拟机
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

实验运行结果如下:

exp2-1

部分dmesg输出如下:

exp2-2

实验三:使用vhost方式创建一个虚拟外设,并实现和虚拟机的数据传输

设计思路:

  1. 在Qemu中实现 vhost_demo虚拟外设
  2. 在宿主机内核中实现vhost_demo虚拟外设的后端内核态部分
  3. 在虚拟机中沿用实验二中的内核态驱动模块virtio-demo和用户态测试程序test

实验过程:

  1. 基于实验二中的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监听。

  1. 创建宿主机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开启监听。

  1. 设计Makefile进行宿主机中vhost模块编译

Makefile的设计与实验一类似,此处不作赘述。

  1. 创建虚拟机中测试程序

虚拟机测试程序完全复用实验二中的virtio前端模块和用户态程序,此处不作赘述。

  1. 设计Makefile进行虚拟机中测试用驱动模块和应用程序编译

Makefile的设计完全复用实验二中的对应Makefile,此处不作赘述。

  1. 功能测试运行
# 进入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

实验运行结果如下: