18 min read

virto虚拟化

1-背景

在完全虚拟化中,CPU会发生VM entryVM exit事件,从而满足不同权限级别的事件。在虚拟机执行IO事件时,会频繁发生entryexit事件而影响虚拟机性能。

virtio这种软件模拟的半虚拟化技术,就可以避免上述这种问题。virtio的核心是利用virtqueue的方式传输数据,而不是标准io中操作特定寄存器的方式。

2-virtio协议

virtio的本质是一个前后端结构,其提供一种虚拟化场景的子机(guest)与母机(host)之间的通讯接口。在子机内是作为前端驱动存在,在母机内则是作为后端设备。并且其可以应用在各种虚拟化技术中,我们主要关注的是KVM中的应用。

a88866825457436ea45bb38f5c731a26.png

2.1-前端驱动

前端驱动是子机中virt用来模拟设备对应的驱动,不同的设备要对应不同的virtio驱动,例如上图中块设备、网络设备、scsi设备都有特殊的驱动模块。这些驱动已经被继承在内核代码中,例如drivers\block\virtio_blk.c

前端设备的主要作用是:

  • 接受子机内的用户态请求
  • 按照virtio协议封装请求
  • 写virtio子机端口,并发送给后端母机中的设备

2.2-后端设备

后端设备主要是用来接受前端设备发送过来的请求并按照规格式解析请求。后端设备是在虚拟化层的,在qemu中则是模拟的pci设备。此外,除了qemu中模拟的pci设备,还有给dpdk使用的vhost设备。

2.3-传输层 split virtqueue

前后端的数据是通过virtqueue来传输的,一个后端设备可以有多个queue组成。virtqueue是通过vring来实现功能,vring是子机和vmm(qemu/kvm)的中共享一段环形缓冲区。

32f26e4669d34a48b21b96049c70b7ab.png

virtqueue主要有三部分组成

  • 描述符表(Descriptor Table)
  • 可用描述符区(Available Ring)
  • 已用描述符区(Used Ring)

前端驱动从子机中获取数据并准备好,将其放置在可用描述符区的vring中,写一个io端口。

QEMU从vring中获取数据,完成请求后,将具体数据放置在已用描述符区的vring中,子机内的前端驱动则从其获取数据。

2.3.1-描述符

描述符表由描述符组成,其又分为inout两种,in表示读数据,out表示写数据。

/**
 * struct vring_desc - Virtio ring descriptors,
 * 16 bytes long. These can chain together via @next.
 *
 * @addr: buffer address (guest-physical)
 * @len: buffer length
 * @flags: descriptor flags
 * @next: index of the next descriptor in the chain,
 *        if the VRING_DESC_F_NEXT flag is set. We chain unused
 *        descriptors via this, too.
 */
struct vring_desc {
	__virtio64 addr;
	__virtio32 len;
	__virtio16 flags;
	__virtio16 next;
};

描述符是由如下字段组成

  • add:存储数据的内存块的起始地址
  • len:out描述符中表示读取数据量;in描述符中表示可以写的最大数据量。
  • flag:表示描述符的属性。
  • next:本次io操作的描述符是否还有下一个连续的描述符。

一次IO操作会由多个描述符组成,他们整体被叫做描述符链,描述符链会包含一个header,header描述符会记录:

  • 命令类型——读/写
  • 操作的起始扇区

header后的是数据描述符,每一个描述符都代表着一个连续内存区域的读写。

最后的状态描述符,其记录了设备的IO执行状态。

image-20240622135048082.png

2.3.2-可用描述符区域

struct vring_avail {
	__virtio16 flags;
	__virtio16 idx;
	__virtio16 ring[];
};
  • flags:这个成员变量表示可用环的标志。它用于指示可用环的状态,例如是否需要通知设备。
  • idx:它是一个递增的计数器,用于表示下一个可用的缓冲区描述符在 ring 数组中的位置。
  • ring:这个成员变量表示可用环的数组。它是一个动态数组,用于存储可用的缓冲区描述符的索引。

前端驱动作为生产者,会将准备好I/O request转换成描述符链,并将他们描述符链中的首描述符id放置在可用描述区里的ring数组中。

每次cpu从guest切换至host中时,后端设备消费者就会开始消费ring中的元素,并通过last_avail_idx索引上次消费的位置。

image-20240622142004374.png

2.3.3-已用描述符区域

struct vring_used {
	__virtio16 flags;
	__virtio16 idx;
	vring_used_elem_t ring[];
};

/* u32 is used here for ids for padding reasons. */
struct vring_used_elem {
	/* Index of start of used descriptor chain. */
	__virtio32 id;
	/* Total length of the descriptor chain which was used (written to) */
	__virtio32 len;
};
  • id:表示已使用的缓冲区描述符的索引。它是该缓冲区描述符在描述符表中的位置。
  • len:表示已使用的缓冲区描述符链的总长度。它表示设备在该描述符链中写入的数据的总长度。

类似于可用描述符,已用描述符是用来给前端驱动返回数据的区域,后端设备完成IO请求后,则将描述符添加到vring_used的ring数组里,并且还会记录返回数据的长度。

image-20240622143457435.png

2.4-packed queue

2.4.1-packed queue数据结构

在split queue中时将availd、free、used三种uring(环)区分开的,而packed queue中则是集合在一起,其描述符的数据结构如下所示

struct vring_packed_desc {
	/* Buffer Address. */
	__le64 addr;
	/* Buffer Length. */
	__le32 len;
	/* Buffer ID. */
	__le16 id;
	/* The flags depending on descriptor type. */
	__le16 flags;
};
  1. addr:这是一个 64 位字段,表示缓冲区的物理地址。设备(如 virtio-blk 或 virtio-net)将使用这个地址直接访问内存中的数据,以执行 I/O 操作。

  2. len:这是一个 32 位字段,表示缓冲区的长度(以字节为单位)。这个字段告诉设备缓冲区的大小,以便设备知道可以读取或写入的数据量。

  3. id:这是一个 16 位字段,表示缓冲区的 ID。这个 ID 通常与描述符在描述符表中的索引相对应。当设备完成 I/O 操作后,它会在响应中返回这个 ID,以便客户机(虚拟机)可以识别哪个请求已完成。

  4. flags:这是一个 16 位字段,表示描述符的类型和状态。flags 字段可以包含以下标志:

    • VRING_DESC_F_NEXT:表示描述符链中还有下一个描述符。当一个请求的数据缓冲区跨越多个非连续内存区域时,这个标志用于链接多个描述符。

    • VRING_DESC_F_WRITE:表示缓冲区用于写操作。如果这个标志被设置,设备将把数据写入缓冲区;如果没有设置,设备将从缓冲区读取数据。

    • VRING_DESC_F_INDIRECT:表示描述符使用间接寻址。这意味着 addrlen 字段实际上指向另一个描述符表,而不是数据缓冲区。间接寻址用于减小描述符表的大小,以减少同步开销。

    • 此外这个flag还会包含描述符是否可用的标记为

                flags = cpu_to_le16(vq->packed.avail_used_flags |
                        (++c == total_sg ? 0 : VRING_DESC_F_NEXT) |
                        (n < out_sgs ? 0 : VRING_DESC_F_WRITE));
                if (i == head)
                    head_flags = flags;
                else
                    desc[i].flags = flags;
      

struct vring_virtqueue->struct vring_virtqueue_packedu16 avail_used_flags;则是用来标记这个ring是avail还是used的标记。

2.4.2-修改packed desc状态

对于packed queue来说,只有一种ring,不用像split queue

  • 前端驱动处理完后添加到used
  • 在后端设备处理后添加到available

在内核中,修改packed_desc状态的api如下

static inline int virtqueue_add_packed(struct virtqueue *_vq,
				       struct scatterlist *sgs[],
				       unsigned int total_sg,
				       unsigned int out_sgs,
				       unsigned int in_sgs,
				       void *data,
				       void *ctx,
				       gfp_t gfp)
{
  ......
  // 获取下一个可用描述符的索引和当前的状态标志
  head = vq->packed.next_avail_idx;
  avail_used_flags = vq->packed.avail_used_flags;

  // 获取描述符数组的引用
  desc = vq->packed.vring.desc;
  i = head;
  descs_used = total_sg;

  // 遍历 scatterlist(sgs)数组
  for (n = 0; n < out_sgs + in_sgs; n++) {
      for (sg = sgs[n]; sg; sg = sg_next(sg)) {
          // ...

          // 设置描述符的 flags 字段
          // 这里使用了 avail_used_flags 变量来设置描述符的状态(可用或已用)
          flags = cpu_to_le16(vq->packed.avail_used_flags |
                  (++c == total_sg ? 0 : VRING_DESC_F_NEXT) |
                  (n < out_sgs ? 0 : VRING_DESC_F_WRITE));
          if (i == head)
              head_flags = flags;
          else
              desc[i].flags = flags;
					
        	// 更新描述符的其他关键字段
          desc[i].addr = cpu_to_le64(addr);
					desc[i].len = cpu_to_le32(sg->length);
					desc[i].id = cpu_to_le16(id);
          //......

          // 如果已经遍历完整个环,就切换 avail_used_flags 变量的值
          // 这会改变环中描述符的状态(可用或已用)
          if ((unlikely(++i >= vq->packed.vring.num))) {
              i = 0;
              vq->packed.avail_used_flags ^=
                  1 << VRING_PACKED_DESC_F_AVAIL |
                  1 << VRING_PACKED_DESC_F_USED;
          }
      }
  }

  // 如果环中的可用描述符已经遍历完,就切换 avail_wrap_counter 变量的值
  // 这会改变环中描述符的状态(可用或已用)
  if (i <= head)
      vq->packed.avail_wrap_counter ^= 1;

  // 更新下一个可用描述符的索引
  vq->packed.next_avail_idx = i;
  ......
}

3-virtio代码

以常见的pci设备+virtblk举例

3.1-注册

virtio的驱动是在前端中完成,也就是guest子机中。

linux内核中驱动是分为三个层级:

  • bus
  • device
  • driver

在这个例子中,virtio_blk对应的就是driver,virtio-pci对应的是device,二者关联是virtio_bus。

# tree /sys/bus/virtio/
/sys/bus/virtio/
├── devices
│   ├── virtio0 -> ../../../devices/pci0000:00/0000:00:05.0/virtio0
│   ├── virtio1 -> ../../../devices/pci0000:00/0000:00:06.0/virtio1
│   ├── virtio2 -> ../../../devices/pci0000:00/0000:00:07.0/virtio2
│   └── virtio3 -> ../../../devices/pci0000:00/0000:00:08.0/virtio3
├── drivers
│   ├── virtio_balloon
│   │   ├── bind
│   │   ├── module -> ../../../../module/virtio_balloon
│   │   ├── uevent
│   │   ├── unbind
│   │   └── virtio3 -> ../../../../devices/pci0000:00/0000:00:08.0/virtio3
│   ├── virtio_blk
│   │   ├── bind
│   │   ├── module -> ../../../../module/virtio_blk
│   │   ├── uevent
│   │   ├── unbind
│   │   └── virtio1 -> ../../../../devices/pci0000:00/0000:00:06.0/virtio1
│   ├── virtio_console
│   │   ├── bind
│   │   ├── module -> ../../../../module/virtio_console
│   │   ├── uevent
│   │   ├── unbind
│   │   └── virtio2 -> ../../../../devices/pci0000:00/0000:00:07.0/virtio2
│   ├── virtio_net
│   │   ├── bind
│   │   ├── module -> ../../../../module/virtio_net
│   │   ├── uevent
│   │   ├── unbind
│   │   └── virtio0 -> ../../../../devices/pci0000:00/0000:00:05.0/virtio0
│   └── virtio_rproc_serial
│       ├── bind
│       ├── module -> ../../../../module/virtio_console
│       ├── uevent
│       └── unbind
├── drivers_autoprobe
├── drivers_probe
└── uevent
  1. /sys/bus/virtio/devices:这个目录包含了所有注册到 virtio 子系统的设备。在这个例子中,有四个设备(virtio0virtio1virtio2virtio3),它们分别对应了不同的 PCI 设备。这些设备的具体信息可以在相应的设备目录(例如 /sys/devices/pci0000:00/0000:00:05.0/virtio0)中找到。
  2. /sys/bus/virtio/drivers:这个目录包含了所有注册到 virtio 子系统的驱动程序。在这个例子中,有五个驱动程序(virtio_balloonvirtio_blkvirtio_consolevirtio_netvirtio_rproc_serial)。每个驱动程序目录下都有一些文件,如 bindunbinduevent,分别用于绑定设备、解绑设备和发送设备事件。此外,驱动程序目录下还有一个名为 module 的符号链接,指向与该驱动程序关联的内核模块目录。还有一个或多个设备的符号链接,表示当前与该驱动程序绑定的设备。
  3. /sys/bus/virtio/drivers_autoprobe/sys/bus/virtio/drivers_probe/sys/bus/virtio/uevent:这些文件用于控制驱动程序的自动探测和设备事件。在这个例子中,它们没有提供额外的信息。

3.1.1-注册总线

首先,virtio会根据定义好的virtio_bus结构体,利用通用的bus_register注册驱动

static struct bus_type virtio_bus = {
	.name  = "virtio",
	.match = virtio_dev_match,
	.dev_groups = virtio_dev_groups,
	.uevent = virtio_uevent,
	.probe = virtio_dev_probe,
	.remove = virtio_dev_remove,
};

static int virtio_init(void)
{
	if (bus_register(&virtio_bus) != 0)
		panic("virtio bus registration failed");
	virtio_debug_init();
	return 0;
}

注册成功后,就可以通过/sys/bus中查看到一个virtio类型的总线。

# ls /sys/bus/virtio/
devices  drivers  drivers_autoprobe  drivers_probe  uevent

https://m.elecfans.com/article/1306596.html

3.1.2-物理总线初始化

想要初始化一个具体的驱动设备前,要初始化好driver的具体总线,virtio-blk对应的是pci设备。

当插入一个虚拟pci设备时,此时会注册为vendor 0x1af4device id 0x1003,此时会通过pci的总线会根据设备号等信息调用virtio_pci_probe进一步初始化成上述的具体pci-device

/* Qumranet donated their vendor ID for devices 0x1000 thru 0x10FF. */
static const struct pci_device_id virtio_pci_id_table[] = {
	{ PCI_DEVICE(PCI_VENDOR_ID_REDHAT_QUMRANET, PCI_ANY_ID) },
	{ 0 }
};

static int virtio_pci_probe(struct pci_dev *pci_dev,
			    const struct pci_device_id *id)
{
	......
	if (force_legacy) {
		rc = virtio_pci_legacy_probe(vp_dev);
		/* Also try modern mode if we can't map BAR0 (no IO space). */
		if (rc == -ENODEV || rc == -ENOMEM)
			rc = virtio_pci_modern_probe(vp_dev);
		if (rc)
			goto err_probe;
	} else {
		rc = virtio_pci_modern_probe(vp_dev);
		if (rc == -ENODEV)
			rc = virtio_pci_legacy_probe(vp_dev);
		if (rc)
			goto err_probe;
	}
	......
	rc = register_virtio_device(&vp_dev->vdev);
	......
}

3.1.3-驱动初始化

当我们插入virtio_blk ko时,驱动会先定义一个virtio_bus的driver结构体

static struct virtio_driver virtio_blk = {
	.feature_table			= features,
	.feature_table_size		= ARRAY_SIZE(features),
	.feature_table_legacy		= features_legacy,
	.feature_table_size_legacy	= ARRAY_SIZE(features_legacy),
	.driver.name			= KBUILD_MODNAME,
	.id_table			= id_table,
	.probe				= virtblk_probe,
	.remove				= virtblk_remove,
	.config_changed			= virtblk_config_changed,
#ifdef CONFIG_PM_SLEEP
	.freeze				= virtblk_freeze,
	.restore			= virtblk_restore,
#endif
};

virtio_blk.c中通过virtio_blk_init->register_virtio_driver->__register_virtio_driver->driver_register,在这里会将自己注册到virtio的bus总线中,并且注册为一个驱动 driver类型。

#define VIRTIO_ID_BLOCK			2 /* virtio block */

static const struct virtio_device_id id_table[] = {
	{ VIRTIO_ID_BLOCK, VIRTIO_DEV_ANY_ID },
	{ 0 },
};

当出现匹配的设备id时,就会自动执行virtblk_probe的逻辑,进行驱动的初始化。

3.2-创建virtqueue

初始化中一个很重要的流程就是初始化当前设备的virtqueue

virtblk_probe->init_vq->virtio_find_vqs->vdev->config->find_vqs->vp_modern_find_vqs(virtio_pci)

static int init_vq(struct virtio_blk *vblk)
{
	......
	err = virtio_cread_feature(vdev, VIRTIO_BLK_F_MQ,
				   struct virtio_blk_config, num_queues,
				   &num_vqs);
	......
	num_vqs = min_t(unsigned int,
			min_not_zero(num_request_queues, nr_cpu_ids),
			num_vqs); //取一个最小值

	num_poll_vqs = min_t(unsigned int, poll_queues, num_vqs - 1);
	......
	vblk->vqs = kmalloc_array(num_vqs, sizeof(*vblk->vqs), GFP_KERNEL); // 为vqs分配实际的内存
	......
	/* Discover virtqueues and write information to configuration.  */
	err = virtio_find_vqs(vdev, num_vqs, vqs, callbacks, names, &desc);
	......
}

在virtio-pci中virtio_find_vqs会最终调用到vp_modern_find_vqs->vp_find_vqs

这里会优先中msi-x中断的vqs,然后找不到msi-x的话才会查找传统的int-x的中断。

/* the config->find_vqs() implementation */
int vp_find_vqs(struct virtio_device *vdev, unsigned int nvqs,
		struct virtqueue *vqs[], vq_callback_t *callbacks[],
		const char * const names[], const bool *ctx,
		struct irq_affinity *desc)
{
	int err;

	/* Try MSI-X with one vector per queue. */
	err = vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, true, ctx, desc);
	if (!err)
		return 0;
	/* Fallback: MSI-X with one vector for config, one shared for queues. */
	err = vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, false, ctx, desc);
	if (!err)
		return 0;
	/* Is there an interrupt? If not give up. */
	if (!(to_vp_device(vdev)->pci_dev->irq))
		return err;
	/* Finally fall back to regular interrupts. */
	return vp_find_vqs_intx(vdev, nvqs, vqs, callbacks, names, ctx);
}

​ 这三个api最终都会调用到vp_setup_vq来实际建立virtqueue,而pci设备的setup_vq函数被定义在了drivers\virtio\virtio_pci_modern.c

static struct virtqueue *setup_vq(struct virtio_pci_device *vp_dev,
				  struct virtio_pci_vq_info *info,
				  unsigned int index,
				  void (*callback)(struct virtqueue *vq),
				  const char *name,
				  bool ctx,
				  u16 msix_vec)
{
  ......
	if (__virtio_test_bit(&vp_dev->vdev, VIRTIO_F_NOTIFICATION_DATA))
		notify = vp_notify_with_data;
	else
		notify = vp_notify; // 注册通知函数
	......
	/* Check if queue is either not available or already active. */
	num = vp_modern_get_queue_size(mdev, index); // 找到queue中vring的数量
	if (!num || vp_modern_get_queue_enable(mdev, index))
		return ERR_PTR(-ENOENT);

	info->msix_vector = msix_vec;

	/* 为virtqueue创建num数量的vring */
	vq = vring_create_virtqueue(index, num,
				    SMP_CACHE_BYTES, &vp_dev->vdev,
				    true, true, ctx,
				    notify, callback, name);
	......
	err = vp_active_vq(vq, msix_vec); // 使能当前的virtqueue
	......
	return vq;
	......
}

3.3-I\O流程

https://blog.csdn.net/flyingnosky/article/details/132724114

https://blog.csdn.net/wing_7/article/details/138562337

virtio-blk会在初始化流程中设置标准的blk-mq操作ops,从而在virtio-blk设备中执行标准的io操作后,执行具体virtio-blk逻辑,将rq填入virtqueue中,通过虚拟化发给后端设备(qemu)。

static const struct blk_mq_ops virtio_mq_ops = {
	.queue_rq	= virtio_queue_rq,
	.queue_rqs	= virtio_queue_rqs,
	.commit_rqs	= virtio_commit_rqs,
	.complete	= virtblk_request_done,
	.map_queues	= virtblk_map_queues,
	.poll		= virtblk_poll,
};

例如,当前端设备完成一些列io操作,在通用块层的blk-mq完成rq蓄流后,最终执行rq的队列下发时的函数queue_rqs->virtio_queue_rqs

static void virtio_queue_rqs(struct request **rqlist)
{
	......
	rq_list_for_each_safe(rqlist, req, next) { // 遍历硬件队列,挨个将rq填入virtqueue
		struct virtio_blk_vq *vq = get_virtio_blk_vq(req->mq_hctx); // 获取这个virtblk设备所创建的virtqueue
		bool kick;

		if (!virtblk_prep_rq_batch(req)) { // 准备做好把request list转换成virtqueue的准备
			rq_list_move(rqlist, &requeue_list, req, prev);
			req = prev;
			if (!req)
				continue;
		}

		if (!next || req->mq_hctx != next->mq_hctx) {
			req->rq_next = NULL;
			kick = virtblk_add_req_batch(vq, rqlist); // 把rqlist转换成virtqueue
			if (kick)
				virtqueue_notify(vq->vq);// 通知后端设备(host qemu)

			*rqlist = next;
			prev = NULL;
		} else
			prev = req;
	}

	*rqlist = requeue_list;
}

3.3.1-前端填入

static bool virtblk_add_req_batch(struct virtio_blk_vq *vq,
					struct request **rqlist)
{
	......
	while (!rq_list_empty(*rqlist)) {// 遍历rqlist里的每个request
		struct request *req = rq_list_pop(rqlist); // 弹出当前rqlist的第一个request
		struct virtblk_req *vbr = blk_mq_rq_to_pdu(req); // 获取req对应的pdu(协议数据单元)

		err = virtblk_add_req(vq->vq, vbr); //把这个vb reques填入virtequeue里
		......
	}

	kick = virtqueue_kick_prepare(vq->vq);
	spin_unlock_irqrestore(&vq->lock, flags);

	return kick;
}

而这里会通过virtblk_add_req来实际将req添加到virtqueue中

函数首先会初始化好需要填入的scatterlist(散射列表)

static int virtblk_add_req(struct virtqueue *vq, struct virtblk_req *vbr)
{
	struct scatterlist out_hdr, in_hdr, *sgs[3];
	unsigned int num_out = 0, num_in = 0;

	sg_init_one(&out_hdr, &vbr->out_hdr, sizeof(vbr->out_hdr));
	sgs[num_out++] = &out_hdr; // 将 out_hdr 添加到 sgs 数组,并增加 num_out 计数。

	if (vbr->sg_table.nents) {
		if (vbr->out_hdr.type & cpu_to_virtio32(vq->vdev, VIRTIO_BLK_T_OUT)) // 如果是写事件
			sgs[num_out++] = vbr->sg_table.sgl; // 则将数据缓冲区的scatterlist添加到 sgs 数组
		else
			sgs[num_out + num_in++] = vbr->sg_table.sgl;// 读事件要挈带num_in
	}

	sg_init_one(&in_hdr, &vbr->in_hdr.status, vbr->in_hdr_len);
	sgs[num_out + num_in++] = &in_hdr; // 将 in_hdr 添加到 sgs 数组,并增加 num_in 计数。

	return virtqueue_add_sgs(vq, sgs, num_out, num_in, vbr, GFP_ATOMIC);
}

然后通过virtqueue_add_sgs->virtqueue_add->virtqueue_add_packed/virtqueue_add_split来填充上述准备sgs。

3.3.2-notify后端

这里的virtqueue_notify会调用在创建virtqueue时通过setup_vq绑定的通知函数vq->notify

通过iowrite16向后端设备的pci配置空间的通知寄存器写值。


/* the notify function used when creating a virt queue */
bool vp_notify(struct virtqueue *vq)
{
	/* we write the queue's selector into the notification register to
	 * signal the other end */
	iowrite16(vq->index, (void __iomem *)vq->priv);
	return true;
}

其中,vq->priv = (void __force *)vp_dev->ldev.ioaddr + VIRTIO_PCI_QUEUE_NOTIFY;(drivers/virtio/virtio_pci_legacy.c)

在写完成这个值后,则会触发kvm_exit事件,然后qemu就会进一步执行virtio的后端设备操作。

3.3.3-后端设备处理

QEMU进程的kvm_main_loop_cpu循环等待KVM_EXIT的产生。当有退出产生时,调用KVM_RUN函数来确定退出的原因,对于IO操作的退出,KVM_EXIT为KVM_EXIT_IO。

当进入到host后,qemu会通过注册好的函数对前端pci设备发送过来的请求进行处理

qemu代码:hw/virtio/virtio-pci.c

static const MemoryRegionOps notify_ops = {
    .read = virtio_pci_notify_read, // 通知寄存器是读操作时
    .write = virtio_pci_notify_write, // 通知寄存器是写操作时
    .impl = {
        .min_access_size = 1,
        .max_access_size = 4,
    },
    .endianness = DEVICE_LITTLE_ENDIAN,
};

进一步调用virtio_pci_notify_write->virtio_queue_notify->vq->handle_output,在virtblk中被定义成virtio_blk_handle_output(virtio_add_queue(vdev, conf->queue_size, virtio_blk_handle_output);)。

hw/block/virtio-blk.cvirtio_blk_handle_request

static int virtio_blk_handle_request(VirtIOBlockReq *req, MultiReqBuffer *mrb)
{
  	uint32_t type;
    ......
    type = virtio_ldl_p(vdev, &req->out.type); // 获取req的类型
    /* VIRTIO_BLK_T_OUT defines the command direction. VIRTIO_BLK_T_BARRIER
     * is an optional flag. Although a guest should not send this flag if
     * not negotiated we ignored it in the past. So keep ignoring it. */
    switch (type & ~(VIRTIO_BLK_T_OUT | VIRTIO_BLK_T_BARRIER)) {
    case VIRTIO_BLK_T_IN: // 初始化命令
    {
       ......
    }
    case VIRTIO_BLK_T_FLUSH: // flush请求命令
        virtio_blk_handle_flush(req, mrb); // 把virtioblk req刷新
        break;
    ......
    default:
        virtio_blk_req_complete(req, VIRTIO_BLK_S_UNSUPP);
        virtio_blk_free_request(req);
    }
    return 0;
}

进一步调用virtio_blk_handle_flush->virtio_blk_submit_multireq->submit_requests->blk_aio_pwritev

最终qemu会通过文件系统通用的接口实现在宿主机内的读写。

3.3.4-blk_done

当宿主机的标准io读写完成后,则会通过之前绑定好的virtio_blk_rw_complete来完成进一步通知, 并通过virtio_notify写回通知寄存器,向虚拟机写入中断。

当虚拟机接收中断后,回通过vring_interrupt来处理中断逻辑,并最终调用virtblk_done来完成设备驱动调用,最终返回给虚拟机内的通用块层,最终完成本次子机内的IO操作。

https://www.cnblogs.com/edver/p/16255243.html