Virtio and Vhost Architecture - Part 1 · Better Tomorrow with Computer Science

Virtio and Vhost Architecture - Part 2 · Better Tomorrow with Computer Science

通过MMIO的方式实现VIRTIO-BLK设备 - 知乎

Virtio devices high-level design — Project ACRN™ v 1.6 documentation

VirtIO Spec: https://docs.oasis-open.org/virtio/virtio/

VirtIO 设备分为 modern 和 legacy 两种。

代码里的一些简称:

  • sg,比如 p_num_sg:maybe scatter/gather?

How to realize the virtio framework is specific to a hypervisor implementation.

  • VirtIO ring descriptors size in kernel is 16 bytes long,所以 256 个正好是一个页(4096)的大小?
  • Avail ring size in kernel is 2 bytes long, 但是好像这个 avail ring 至少有一个 64 bytes 的 part。

VirtIO 和 DMA 是有点像的。

VirtIO 相比于 full virtualization I/O 的优势是什么?

总结:

  • 相比于之前 PV 的实现,解决兼容性问题;
  • 相比于 full virtualization I/O,解决性能问题。

最开始 VirtIO 解决的最主要问题是兼容性问题。有点像 LSP 当时的发展思路:

  • LSP 是为了让语言和编辑器解耦,不需要每一个编辑器为每一个语言设计一套高亮/提示机制;
  • VirtIO 是为了让设备实现和 Hypervisor 解耦,不需要一个 Hypervisor 自己实现一个设备。在虚拟机和各种 Hypervisor 虚拟设备之间提供一个统一的通信框架和编程接口,减少跨平台所带来的兼容性问题,提升驱动程序开发效率。VirtIO 出现之前也是有 PV 方案的,只不过 hypervisor 各有各的 IO 设备模拟 PV 方案,并在 guest 中大量合入驱动代码,在代码目录下可以看到,导致一片混乱。有了 VirtIO,guest kernel 里的驱动就不用改了

VirtIO 解决了性能问题吗?

  • 只能说相比于 full virtualization,肯定效率是提升了的,即使 full virtualization 可以 DMA^,其也不是零拷贝,需要先从真实网卡 -> 虚拟 device 的内存,再从 device memcpy -> guest address space 的 DMA 空间里面(目前 QEMU code 的确就是这么实现的,那么是什么阻止了直接从真实网卡拷贝到 guest address space 的 DMA 空间里呢?QEMU 理论上来说是可以直接访问 guest address space 里的任何内存的。我觉得是因为从真实网卡得到的数据还是需要经过一定处理才能发给虚拟 device,另外还有一些架构上的原因)。
  • 但是 virtio 可以实现零拷贝,具体怎么实现的请看 virtio-blk and zero-copy^,大致上就是 device 把 driver 传过来的地址组了一个 iovec,然后把这个 iovec 扔给 host 文件系统直接做 IO 去了。
  • 以前 specific 的方式就是 PV 的,应该有一些也是用了 share memory 来加速的,这一点上 VirtIO 并没有提高性能,更多的是提升了兼容性。

VirtIO 相比于 full virtualization I/O 的优势在于:

  • 原来基于 trap-and-emulate 的方式集成了大量的 PIO/MMIO 操作,因为 MMIO 比如说 MOV 指令是基于 CPU 自己的寄存器的,寄存器空间有限,所以需要大量的 MOV 指令才能完成内存的搬运,之前对这种的处理方式就是每次 MOV 就 exit 出来,非常低效;即使是 DMA,每次设置寄存器也需要 exit 出来。
  • 对于使用 MMIO 来进行数据 IO 较多的场景,VirtIO 在真正的 I/O 路径上规避了大量可能导致 vm-exit 的 MMIO/PIO 操作,而是改为了 batch,让 host 来搬。
  • 对于使用 DMA 较多的场景,virtio 也规避了控制链路上的大量 vm-exit。

VirtIO 的缺点 / Shortcomings of VirtIO

Why virtio driver in guest set descriptor table address higher than available memory

VirtIO 3 layers

Both FE and BE drivers follow a layered architecture. Each side has three layers: transports (PCI, MMIO), core models (virtqueue, config space, etc.), and device types (block, net, scsi, rand, etc.).

Legacy and modern virtio

Legacy 和 modern 是 VirtIO 里的两个不同的模式。

virtio的legacy和modern模式 - 知乎

Legacy Interface is an interface specified by an earlier draft of this specification (before 1.0)

VIRTIO_F_VERSION_1 这个 feature,如果后端不支持这个 feature,前端就只能按照 lagacy 接口进行交互。

最主要的方面就是 PCIe 设备空间的 layout 方面的不同。

VirtIO and DMA

如果后端设备是软件模拟的,比如说是 QEMU 模拟出来的,那么 QEMU 里的 device 会触发 DMA 吗?

有了 VirtIO 之后,guest driver 就直接是 virtio driver 了,使用 virtio 的这一套逻辑进行 IO,就没有之前设置寄存器驱动 DMA 的方式了?所以就不需要 DMA 了。

但是在 device (hypervisor) 端,仍然需要进行 DMA,这样才能直接 DMA 到 guest driver 提供的 buffer 中。所以只能说从 guest driver 的接口上来说,VirtIO 替代了 DMA,但是其实在整个数据链路还是会用到 DMA。

Can a backend emulated totally in QEMU be called vhost?

问题补充:After all it can also be called emulated in host(QEMU 也是 host), so why not naming it vhost?

A: Yes, vhost protocol - A protocol that allows the virtio dataplane implementation to be offloaded to another element (user process or kernel module) in order to enhance performance.

The vhost protocol can be implemented in the kernel (vhost-net) or in the user space (vhost-user).

Introduction to virtio-networking and vhost-net

There is NO such a probability to emulate it in guest.

VirtIO backend

For VirtIO the frontend is the driver running on the guest. The backend is the everything that QEMU needs to do to handle the emulation of the VirtIO device. This can be done:

  • in QEMU itself.
  • in the host kernel (a.k.a. vhost).
  • in a separate process which is configured by QEMU (a.k.a. vhost-user).

VirtIO backend 和 qemu device backend 有什么区别?

backend 在 VirtIO 和 QEMU 的语境里看起来是不同的。

virtio backend is a type of qemu device backend implemented using virtio. That means there would be some device backends not implemented in the virtio way, such as nvme vs virtio-blk:

# using virtio-blk-pci to specify a disk
qemu-system-x86_64 -machine accel=kvm -vnc :0 -smp 4 -m 4096M \
    -net nic -net user,hostfwd=tcp::5023-:22 \
    -hda ol7.qcow2 -serial stdio \
    -device virtio-blk-pci,drive=drive0,id=virtblk0,num-queues=4 \
    -drive file=disk.qcow2,if=none,id=drive0

# using nvme to specify a disk
qemu-system-x86_64 -machine accel=kvm -vnc :0 -smp 4 -m 4096M \
    -net nic -net user,hostfwd=tcp::5023-:22 \
    -hda ol7.qcow2 -serial stdio \
    -device nvme,drive=nvme0,serial=deadbeaf1,num_queues=8 \
    -drive file=disk.qcow2,if=none,id=nvme0

NVMe Emulation — QEMU documentation

Device Emulation — QEMU documentation

According to www.reddit.com, NVME has worse performance than virtio.

QEMU virtio device read desc/avail/used shared page

QEMU 里 virtio device 读取 desc/avail/used 这些 vring 的 code:

首先是 descriptor table 的地址

根据下面代码片段我们可以看出来 cache->ptr 表示了这个 descriptor table 的地址:

virtqueue_split_pop
    // 把 last_avail_idx 转化成 head(desc table 里的第几项)
    virtqueue_get_head(vq, vq->last_avail_idx++, &head)
    // i 表示的就是 descriptor table 里的第几项
    i = head;
    vring_split_desc_read(vdev, &desc, desc_cache, i);
        // 每一项占用 sizeof(VRingDesc) 的大小
        address_space_read_cached(cache, i * sizeof(VRingDesc), desc, sizeof(VRingDesc));
            if (likely(cache->ptr))
                // addr 就是 i * sizeof(VRingDesc),所以 cache->ptr + addr
                // 指的应该就是第 i 个 desc entry 的地址。
                // 所以 cache->ptr 应该就是 descriptor table 的 HVA 地址。
                // memcpy(dst, src, size)...
                memcpy(buf, cache->ptr + addr, len);

那么 cache->ptr 是怎么被赋值的呢?请看 MemoryRegionCache^。

address_space_cache_init
    if (memory_access_is_direct(mr, is_write)) {
        /* We don't care about the memory attributes here as we're only
         * doing this if we found actual RAM, which behaves the same
         * regardless of attributes; so UNSPECIFIED is fine.
         */
        l = flatview_extend_translation(cache->fv, addr, len, mr, cache->xlat, l, is_write, MEMTXATTRS_UNSPECIFIED);
            
        cache->ptr = qemu_ram_ptr_length(mr->ram_block, cache->xlat, &l, true);
    }

其次是 avail vring 的地址


再次是 used vring 的地址:

Virtio frontend

Is Virtio frontend a driver implemented in the guest kernel, or the one specified in cmdline and created by QEMU?

我认为这是容易搞混的地方,VritIO 和 QEMU 都有 device frontend/backend 的概念,需要进行区分。

QEMU 里的 frontend 表示应该给 guest 呈现什么样的硬件,而 VirtIO 里的 frontend 指的就是 guest 里的 driver。

QEMU 里的 backend 表示的是应该怎么处理前端发过来的数据,这部分 guest 应该是无法感知到的。VritIO 里的 backend 好像也差不多是这个意思。

VirtIO Transport layer / VirtIO Transports / VirtIO 和 MMIO 关系 / virtio-pci, virtio-mmio

一个 virtio 设备可以以 pci 设备的方式呈现,也可以通过 MMIO 的方式来呈现。

我们要知道 Virtio 采用不同总线实现的优缺点。

  • 采用 PCI 总线实现的好处首先是通用,其次可以具备 PCI 设备的很多优势,如热插拔,最后如果我们使用物理设备实现 virtio 也是可以采 PCI 总线的,换句话说虚拟机内部感知不到这个 virtio 设备是一个虚拟设备还是一个物理设备。但采用 PCI 总线实现的缺点是复杂,具体可以参考 qemu 中关于 PCI 设备的模拟逻辑,无论是 PCI 主桥的模拟,还是 PCI 设备本身配置空间和中断分配的模拟都十分复杂。
  • 而采用 MMIO 总线由于直接是内存映射模拟设备,所以只能用于虚拟设备,无法用于物理设备,guest 知道设备是模拟出来的,另外也不具有 PCI 设备的热插拔能力,但其优点就是实现简单。所以 mmio 方式多用于轻量级虚拟场景,例如 aws 的 firecracker 就是通过 mmio 总线来实现 virtio 设备的。mmio 设备同样需要类似 PCI 设备配置空间一样实现一些寄存器来存放 virtio 设备的基本配置信息。virtio-mmio 设备的 MagicValue 寄存器值必须是 0x74726976,version 寄存器必须是 0x2。驱动只有确认以上信息正常才能进行后续 virtio-mmio 设备的协商配置。

Guest driver 在编写层面上,是感知不到 PCI 还是 MMIO 的。可以看:

  • drivers/net/caif/caif_virtio.c

Guest driver virtio 框架中应该有检测的逻辑,用来判断是 MMIO 还是 PCI。

Device 侧,比如说在 QEMU 中,是可以指定 mmio 还是 pci 的

virtio-pcivirtio-mmio 只是在设备发现上有区别吗?

首先我们要知道 virtio 只是一个半虚拟化标准,或者说是一个协议,协议本身实现的载体和总线并无绑定。virtio 协议实现过程中,CPU 与外设之间的通知机制以及外设访问内存方式由实际连接 CPU 与外设的总线协议决定。换句话说,virtio 协议可以基于多种不同的总线协议来实现。

举个例子,DMA 场景下,device 如何将数据传送到对于 VirtIO ring 里一个 entry 指向的内存地址?MMIO 场景下,driver 对于这种内存地址的读取,如何反映到设备的内存空间?

虚拟化场景中,主要采用 PCI 总线协议(virtio-pci)和 MMIO 总线协议(virtio-mmio):

  • 采用 PCI 总线协议的 virtio 设备叫 virtio-pci 设备,它可以支持 virtio 设备的热插拔特性 (基于 PCI 总线的设备热插拔机制),并可应用于真实物理外设
  • 采用 mmio 总线协议的 virito 设备叫 virito-mmio 设备,它完全是针对虚拟机设计的,是一种轻量的虚拟总线机制,支持快速设备发现,但是无法使用在真实物理外设中。原因是?

当前 virtio-mmio 被大量用于轻量级安全容器当中,Firecracker 就是通过 mmio 总线来实现 virtio 设备的。性能上来说,virtio-pci 是比 virtio-mmio 要更高级的。

什么是 VirtIO Transport?

想象一个普通的设备,driver 是如何发现设备的存在的?通过 PCIe 协议。其实 MMIO 是 PCIe 协议的一个组成部分。

VirtIO 只是能让 driver 把比如说 ring 里的每一个 entry 所指向的那片内存空间地址告诉 device,device 可以通过

VirtIO over MMIO: Virtual environments without PCI support (a common situation in embedded devices models) might use simple memory mapped device (“virtio-mmio”) instead of the PCI device. The memory mapped virtio device behaviour is based on the PCI device specification. Therefore most operations including device initialization, queues configuration and buffer transfers are nearly identical. Existing differences are described in the following sections.

VirtIO 驱动

可以参考 Spec 的 Chapter 4 VirtIO Transport Options。

  • VirtIO Over PCI Bus: -device virtio-foo-pci;
  • VirtIO Over MMIO: -device virtio-foo;
  • VirtIO Over Channel I/O

不同的传输层实现使用了不同的 ops。

static const struct virtio_config_ops virtio_mmio_config_ops = {
    //...
};

static const struct virtio_config_ops virtio_pci_config_ops = {
    //...
};

Virtqueue

Virtqueue is a part of the memory of the guest OS. It is also a channel between front-end and back-end. It is an interface Implemented as Vring.

Virtqueue 和 Vring 的关系是,Virtqueue 里面包含了三个 Vring,分别是 vring_desc, vring_avail 以及 vring_used。可以说 Virtqueue 就是 Vring 组织的。看起来所有的消息都是基于 virtqueue 的,比如 guest 发送 request 的时候就是把一个 virtqueue 的所有 Vring 设置好,然后通知,所以本质上是以 virtqueue 为单位来发送的。

一个 device 可以有多个 virtqueue

VirtIO

struct virtqueue Kernel

从 Guest kernel 角度来看的 virtqueue 结构体。

/**
 * struct virtqueue - a queue to register buffers for sending or receiving.
 * @list: the chain of virtqueues for this device(因为一个 device 可以有多个 virtqueue)
 * @callback: the function to call when buffers are consumed (can be NULL).
 * @name: the name of this virtqueue (mainly for debugging)
 * @vdev: the virtio device this queue was created for.(多对一的关系)
 * @priv: a pointer for the virtqueue implementation to use.(所以是 private 的)
 * @index: the zero-based ordinal number for this queue.(可以看作这个 queue 的 ID?)
 * @num_free: number of elements we expect to be able to fit.
 * @num_max: the maximum number of elements supported by the device.
 * @reset: vq is in reset state or not.
 *
 * A note on @num_free: with indirect buffers, each buffer needs one
 * element in the queue, otherwise a buffer will need one element per
 * sg element.
 */
struct virtqueue {
	struct list_head list;
	void (*callback)(struct virtqueue *vq);
	const char *name;
	struct virtio_device *vdev;
	unsigned int index;
	unsigned int num_free;
	unsigned int num_max;
	bool reset;
	void *priv;
};

struct vring_virtqueue Guest kernel

这个结构体看起来是对 virtqueue 进行了一层包装。和 virtqueue 的区别是:

  • virtio-blk 是一个 virtio 设备,它看到的队列是 virtqueue,里面没有 vring 的实现,只记录了 vring 中还有多少空闲的 buffer 可以使用;
  • vring_virtqueue 是一个 virtqueue,它将 VRing 的实现隐藏在 virtqueue 下面,当一个 virtio-blk 设备真正要发送数据时,只要传入 virtqueue 就能找到 VRing 并实现数据收发。
struct vring_virtqueue {
	struct virtqueue vq;
	/* Is this a packed ring? */
	bool packed_ring;
	/* Is DMA API used? */
	bool use_dma_api;
	/* Can we use weak barriers? */
	bool weak_barriers;
	/* Other side has made a mess, don't try any more. */
	bool broken;

	/* Host supports indirect buffers */
	bool indirect;

	/* Host publishes avail event idx */
	bool event;

	/* Do DMA mapping by driver */
	bool premapped;

	/* Do unmap or not for desc. Just when premapped is False and
	 * use_dma_api is true, this is true.
	 */
	bool do_unmap;

	/* Head of free buffer list. */
	unsigned int free_head;
	/* Number we've added since last sync. */
	unsigned int num_added;

	/* Last used index  we've seen.
	 * for split ring, it just contains last used index
	 * for packed ring:
	 * bits up to VRING_PACKED_EVENT_F_WRAP_CTR include the last used index.
	 * bits from VRING_PACKED_EVENT_F_WRAP_CTR include the used wrap counter.
	 */
	u16 last_used_idx;

	/* Hint for event idx: already triggered no need to disable. */
	bool event_triggered;

	union {
		/* Available for split ring */
		struct vring_virtqueue_split split;

		/* Available for packed ring */
		struct vring_virtqueue_packed packed;
	};

	/* How to notify other side. FIXME: commonalize hcalls! */
	bool (*notify)(struct virtqueue *vq);

	/* DMA, allocation, and size information */
	bool we_own_ring;

	/* Device used for doing DMA */
	struct device *dma_dev;
    //...
};

struct vring_virtqueue_split Kernel

struct vring_virtqueue_split {
	/* Actual memory layout for this queue. */
	struct vring vring;

	/* Last written value to avail->flags */
	u16 avail_flags_shadow;

	/*
	 * Last written value to avail->idx in
	 * guest byte order.
	 */
	u16 avail_idx_shadow;

	/* Per-descriptor state. */
	struct vring_desc_state_split *desc_state;
	struct vring_desc_extra *desc_extra;

    // 这个 vring 的地址 GPA,要告诉 host 进行 share 的。
    // 这个 vring 可以是 desc/avail/used
	dma_addr_t queue_dma_addr;
	size_t queue_size_in_bytes;

	/*
	 * The parameters for creating vrings are reserved for creating new
	 * vring.
	 */
	u32 vring_align;
	bool may_reduce_num;
};

vring_alloc_queue() Guest kernel driver

这是给 VRing 这个 shared memory 分配内存的地方。GPA。

dma_handle 是从 Guest 角度看到的物理地址,也就是 GPA。

static void *vring_alloc_queue(struct virtio_device *vdev, size_t size,
			       dma_addr_t *dma_handle, gfp_t flag,
			       struct device *dma_dev)
{
	if (vring_use_dma_api(vdev)) {
		return dma_alloc_coherent(dma_dev, size, dma_handle, flag);
	} else {
		void *queue = alloc_pages_exact(PAGE_ALIGN(size), flag);

		if (queue) {
			phys_addr_t phys_addr = virt_to_phys(queue);
			*dma_handle = (ma_addr_t)phys_addr;
            //...
		}
		return queue;
	}
}

struct vhost_virtqueue Kernel

virtqueue 不同,这个是 host 端的数据结构。

struct vhost_virtqueue {
	struct vhost_dev *dev;
	struct vhost_worker __rcu *worker;

	/* The actual ring of buffers. */
	struct mutex mutex;
	unsigned int num;

    // 这里显示出来的这三个 Vring 组织的数据结构。
	vring_desc_t __user *desc;
	vring_avail_t __user *avail;
	vring_used_t __user *used;
	const struct vhost_iotlb_map *meta_iotlb[VHOST_NUM_ADDRS];
	struct file *kick;
	struct vhost_vring_call call_ctx;
	struct eventfd_ctx *error_ctx;
	struct eventfd_ctx *log_ctx;

	struct vhost_poll poll;

	/* The routine to call when the Guest pings us, or timeout. */
	vhost_work_fn_t handle_kick;

	/* Last available index we saw.
	 * Values are limited to 0x7fff, and the high bit is used as
	 * a wrap counter when using VIRTIO_F_RING_PACKED. */
	u16 last_avail_idx;

	/* Caches available index value from user. */
	u16 avail_idx;

	/* Last index we used.
	 * Values are limited to 0x7fff, and the high bit is used as
	 * a wrap counter when using VIRTIO_F_RING_PACKED. */
	u16 last_used_idx;

	/* Used flags */
	u16 used_flags;

	/* Last used index value we have signalled on */
	u16 signalled_used;

	/* Last used index value we have signalled on */
	bool signalled_used_valid;

	/* Log writes to used structure. */
	bool log_used;
	u64 log_addr;

	struct iovec iov[UIO_MAXIOV];
	struct iovec iotlb_iov[64];
	struct iovec *indirect;
	struct vring_used_elem *heads;
	/* Protected by virtqueue mutex. */
	struct vhost_iotlb *umem;
	struct vhost_iotlb *iotlb;
	void *private_data;
	u64 acked_features;
	u64 acked_backend_features;
	/* Log write descriptors */
	void __user *log_base;
	struct vhost_log *log;
	struct iovec log_iov[64];

	/* Ring endianness. Defaults to legacy native endianness.
	 * Set to true when starting a modern virtio device. */
	bool is_le;
#ifdef CONFIG_VHOST_CROSS_ENDIAN_LEGACY
	/* Ring endianness requested by userspace for cross-endian support. */
	bool user_be;
#endif
	u32 busyloop_timeout;
};

struct VRing QEMU

typedef struct VRing
{
    // descriptor 的数量
    // 这个 num 就是 queue size
    // 应该是这三个 queue 每一个 queue 的 size
    unsigned int num;
    unsigned int num_default;
    unsigned int align;
    // 三个 Vring 的地址(PFN)
    hwaddr desc;
    hwaddr avail;
    hwaddr used;
    // 因为我们有三个 Vring,每一个其实都需要 cache?
    // 所以我们有了这个元素
    VRingMemoryRegionCaches *caches;
} VRing;

struct VRingMemoryRegionCaches QEMU

可以理解这里的这三个元素存了真正的共享内存。

typedef struct VRingMemoryRegionCaches {
    struct rcu_head rcu;
    MemoryRegionCache desc;
    MemoryRegionCache avail;
    MemoryRegionCache used;
} VRingMemoryRegionCaches;

virtqueue_add() QEMU

可以看到无论是 add 还是 pop 都是需要区分 packed 还是 split 结构的。

这个表示的是往 virtqueue 里面 add,而不是 add 一个新的 virtqueue(请参加 virtio_add_queue())。

static inline int virtqueue_add(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)
{
	struct vring_virtqueue *vq = to_vvq(_vq);

	return vq->packed_ring ? virtqueue_add_packed(_vq, sgs, total_sg,
					out_sgs, in_sgs, data, ctx, gfp) :
				 virtqueue_add_split(_vq, sgs, total_sg,
					out_sgs, in_sgs, data, ctx, gfp);
}

virtqueue_alloc_element() QEMU

// out_num: out descriptor 的数量
// in_num: in descriptor 的数量
static void *virtqueue_alloc_element(size_t sz, unsigned out_num, unsigned in_num)
{
    VirtQueueElement *elem;
    // sz 向上取整,成为 elem->in_addr[0] 的倍数。
    // 可以看到大家都是 size_t 类型的,值都在依次往后堆。
    // 也就是说把列表空间放到结构体后面。
    size_t in_addr_ofs = QEMU_ALIGN_UP(sz, __alignof__(elem->in_addr[0]));
    size_t out_addr_ofs = in_addr_ofs + in_num * sizeof(elem->in_addr[0]);
    size_t out_addr_end = out_addr_ofs + out_num * sizeof(elem->out_addr[0]);
    size_t in_sg_ofs = QEMU_ALIGN_UP(out_addr_end, __alignof__(elem->in_sg[0]));
    size_t out_sg_ofs = in_sg_ofs + in_num * sizeof(elem->in_sg[0]);
    size_t out_sg_end = out_sg_ofs + out_num * sizeof(elem->out_sg[0]);

    //...
    elem = g_malloc(out_sg_end);
    elem->out_num = out_num;
    elem->in_num = in_num;
    // 可以看到后面这 4 个指向的地址相比于 elem 的偏移取决于 sz
    elem->in_addr = (void *)elem + in_addr_ofs;
    elem->out_addr = (void *)elem + out_addr_ofs;
    elem->in_sg = (void *)elem + in_sg_ofs;
    elem->out_sg = (void *)elem + out_sg_ofs;
    return elem;
}

virtqueue_pop() QEMU

可以看到无论是 add 还是 pop 都是需要区分 packed 还是 split 结构的。sz 可以是很多种:

  • sizeof(VirtQueueElement)
  • sizeof(VirtIODeviceRequest)
  • sizeof(VirtIOBlockReq)

等等。附上这三个的内存布局:

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;

typedef struct VirtIODeviceRequest {
    VirtQueueElement elem;
    int fd;
    VirtIOPMEM *pmem;
    VirtIODevice *vdev;
    struct virtio_pmem_req req;
    struct virtio_pmem_resp resp;
} VirtIODeviceRequest;

typedef struct VirtIOBlockReq {
    VirtQueueElement elem;
    int64_t sector_num;
    VirtIOBlock *dev;
    VirtQueue *vq;
    IOVDiscardUndo inhdr_undo;
    IOVDiscardUndo outhdr_undo;
    struct virtio_blk_inhdr *in;
    struct virtio_blk_outhdr out;
    QEMUIOVector qiov;
    size_t in_len;
    struct VirtIOBlockReq *next;
    struct VirtIOBlockReq *mr_next;
    BlockAcctCookie acct;
} VirtIOBlockReq;

可见最基本的还是 VirtQueueElement。后面的内存布局就不一样了,但是因为 driver 保证了每一个 device specific 的布局的方式,所以传入 sz 即可。

void *virtqueue_pop(VirtQueue *vq, size_t sz)
{
    //...
    if (virtio_vdev_has_feature(vq->vdev, VIRTIO_F_RING_PACKED)) {
        return virtqueue_packed_pop(vq, sz);
    } else {
        return virtqueue_split_pop(vq, sz);
    }
}

virtqueue_split_pop() QEMU

我们要 return 的是 VirtQueueElement 类型,这本身就表示了一个请求。这个 elem 在此函数里被构造。

会 pop 一个 descriptor chain 出来。

static void *virtqueue_split_pop(VirtQueue *vq, size_t sz)
{
    unsigned int i, head, max;
    VRingMemoryRegionCaches *caches;
    MemoryRegionCache indirect_desc_cache = MEMORY_REGION_CACHE_INVALID;
    MemoryRegionCache *desc_cache;
    int64_t len;
    VirtIODevice *vdev = vq->vdev;
    VirtQueueElement *elem = NULL;
    unsigned out_num, in_num, elem_entries;
    hwaddr addr[VIRTQUEUE_MAX_SIZE];
    struct iovec iov[VIRTQUEUE_MAX_SIZE];
    VRingDesc desc;
    int rc;

    RCU_READ_LOCK_GUARD();
    // vq 是 empty 的,没有请求过来,那么直接结束
    if (virtio_queue_empty_rcu(vq))
        goto done;

    /* Needed after virtio_queue_empty(), see comment in virtqueue_num_heads(). */
    smp_rmb();

    /* When we start there are none of either input nor output. */
    out_num = in_num = elem_entries = 0;
    max = vq->vring.num;

    // checks...
    // 因为可能是一个 descriptor chain,所以我们先拿到这个
    // chain 的第一个 descriptor
    // 从 available ring 里拿,因为我们是 backend,是消费者
    virtqueue_get_head(vq, vq->last_avail_idx++, &head))

    // VIRTIO_RING_F_EVENT_IDX feature 相关的
    // ...
    i = head;

    caches = vring_get_region_caches(vq);
    // checks...

    // 拿出 cache 里的 descriptor table 这部分的 cache
    desc_cache = &caches->desc;
    vring_split_desc_read(vdev, &desc, desc_cache, i);
    // indirect...
    if (desc.flags & VRING_DESC_F_INDIRECT) {
        // checks...
        /* loop over the indirect descriptor table */
        len = address_space_cache_init(&indirect_desc_cache, vdev->dma_as, desc.addr, desc.len, false);
        desc_cache = &indirect_desc_cache;
        // checks...
        max = desc.len / sizeof(VRingDesc);
        i = 0;
        vring_split_desc_read(vdev, &desc, desc_cache, i);
    }

    /* Collect all the descriptors */
    do {
        // 每一个 iteration 处理一个 descriptor
        bool map_ok;

        // 一个 descriptor 可能是写的,也可能是读的。
        // 这表示这是从设备写给驱动的
        if (desc.flags & VRING_DESC_F_WRITE) {
            map_ok = virtqueue_map_desc(vdev, &in_num, addr + out_num,
                                        iov + out_num,
                                        VIRTQUEUE_MAX_SIZE - out_num, true,
                                        desc.addr, desc.len);
        } else {
            // 我们要保证顺序,显示 out 才能使 in
            // 先是 driver 写来的数据,才能是 device 写给 driver 的 buffer。
            if (in_num)
                virtio_error(vdev, "Incorrect order for descriptors");
            // 这个 descriptor 是从驱动写给设备的
            map_ok = virtqueue_map_desc(vdev, &out_num, addr, iov,
                                        VIRTQUEUE_MAX_SIZE, false,
                                        desc.addr, desc.len);
        }
        if (!map_ok) {
            goto err_undo_map;
        }

        // checks...
        // pop 直到这个 chain 被 pop 完
        rc = virtqueue_split_read_next_desc(vdev, &desc, desc_cache, max, &i);
    } while (rc == VIRTQUEUE_READ_DESC_MORE);

    // error handling...
    elem = virtqueue_alloc_element(sz, out_num, in_num);
    elem->index = head;
    elem->ndescs = 1;
    for (i = 0; i < out_num; i++) {
        elem->out_addr[i] = addr[i];
        elem->out_sg[i] = iov[i];
    }
    for (i = 0; i < in_num; i++) {
        elem->in_addr[i] = addr[out_num + i];
        elem->in_sg[i] = iov[out_num + i];
    }

    vq->inuse++;
    //...
    return elem;
    //...
}

Packed virtqueue / split virtqueue

The split virtqueue format separates the virtqueue into three areas where each area is writable by either the driver or the device, but not both. Packed 是对 split 的一种改进。

split 优点在于简洁。The avail-used buffer cycle needs to use memory in a very sparse way. This puts pressure on the CPU cache utilization, and in the case of hardware means several PCI transactions for each descriptor. Packed virtqueue amends it by merging the three rings in just one location in virtual environment guest memory.

it’s a natural step after the split version if we realize that the device can discard and overwrite the data it already has read from the driver, and the same happens the other way around.

Feature enumeration: After the agreement on RING_PACKED feature flag, the driver and the device starts with a shared blank canvas of descriptors with an agreed length (up to 2152^{15} entries) in a agreed guest’s memory location.

Packed virtqueues is an alternative compact virtqueue layout using read-write memory, that is memory that is both read and written by both host and guest.

核心机制如下:

  1. When the driver wants to send a buffer to the device, it writes at least one available descriptor (describing elements of the buffer) into the Descriptor Ring. The descriptor(s) are associated with a buffer by means of a Buffer ID stored within the descriptor.
  2. The driver then notifies the device. When the device has finished processing the buffer, it writes a used device descriptor including the Buffer ID into the Descriptor Ring (overwriting a driver descriptor previously made available), and sends a used event notification.

Driver 和 Device 对于 descriptor table 里 entry 的处理都是 in-order 的。虽然 Driver 往里面放 avail descriptor 是 in-order,同时 Device 从里面读从而开始处理的顺序也是 in-order 的,但是由于 device 这里处理完成的时间是不确定的,有可能晚开始处理的反而早结束,所以 used device descriptors are written in the order in which their processing is complete.(INORDER feature 不知道和这个有没有关系)。

想要更多了解,还是强烈推荐去看 Spec。

VirtIO Spec
2.8 Packed Virtqueues

How does the descriptors chained together in packed format?

不像 split 有三个 vring: descriptor table, available vring 和 used vring,packed 只有一个 vring: descriptor table。

因为三个 vring 合到一起了,那么怎样表示一个 entry 是 avail 还是 used 的呢?Both the driver and the device use the following two flags:

#define VIRTQ_DESC_F_AVAIL (1 << 7)
#define VIRTQ_DESC_F_USED (1 << 15)

注意这两个 bit 作用的机制并不是独立的,这两个 bit 的 combination 才能够表示一个 entry 是 avail 的还是 used 的:

  • 如果两个都置上了或者两个都没有置上:那么就是一个 used 的 descriptor。都置上了表示 driver 先置上了然后后面 device 跟上又置上了;
  • 如果一个置上了另一个没有置上:那么是 available 的 descriptor。可能是因为 avail 被 driver 置上了,但是 used 没有被 device 置上。是否存在 avail 没有置上,但是 used 被置上了这种情况?
// Driver code in guest kernel
static inline bool is_used_desc_packed(const struct vring_virtqueue *vq, u16 idx, bool used_wrap_counter)
{
    //...
	avail = !!(flags & (1 << VRING_PACKED_DESC_F_AVAIL));
	used = !!(flags & (1 << VRING_PACKED_DESC_F_USED));
	return avail == used && used == used_wrap_counter;
}

// Device code in QEMU
static inline bool is_desc_avail(uint16_t flags, bool wrap_counter)
{
    //...
    avail = !!(flags & (1 << VRING_PACKED_DESC_F_AVAIL));
    used = !!(flags & (1 << VRING_PACKED_DESC_F_USED));
    return (avail != used) && (avail == wrap_counter);
}

packed 下的 entry 不是用 next field 连接起来的这种松散的结构,而是下一个位置就是下一个 entry。Chained descriptors work likewise: no need for the next field in the head (or subsequent) descriptor in the chain to search subsequent ones, since the latter always occupies the next position. 这样我们就不用 used 和 avail 这两个 Vring 了。

descriptor entry 有一个 buffer id field 来表示其是哪一个 descriptor chain。

When using multiple descriptors, set the VIRTQ_DESC_F_NEXT bit in flags for all but the last available descriptor. 这样顺着找就知道哪一个是最后一个 descriptor 了。Buffer ID is included in the last descriptor(只有 last descriptor 会记录 buffer ID)。

没有 used ring,device 在处理完之后会直接写到 descriptor ring 的一个 entry,里面包含了 buffer id(used 的),有可能覆盖一个之前 make available 的 entry。

Driver 怎么放:The driver writes descriptors into the ring in order. After reaching the end of the ring, the next descriptor is placed at the head of the ring. Once the ring is full of driver descriptors, the driver stops sending new requests and waits for the device to start processing descriptors and to write out some used descriptors before making new driver descriptors available.

Device 怎么取:the device reads descriptors from the ring in order and detects that a driver descriptor has been made available. As processing of descriptors is completed, used descriptors are written by the device back into the ring.

对于 packed 的情况:After reading driver descriptors and starting their processing in order, the device might complete their processing out of order. Used device descriptors are written in the order in which their processing is complete.

VIRTQ_DESC_F_NEXT is reserved in used descriptors, and should be ignored by drivers. 也就是 Device 在填 used 的时候不会以 chain 的方式来填。

Driver and Device Ring Wrap Counters

Each of the driver and the device are expected to maintain, internally, a single-bit ring wrap counter initialized to 1. 名字不一样:

  • Driver Ring Wrap Counter:每次 driver 往里面放 avail descriptor 的时候会更新这个,简便我们叫 counter a;
  • Device Ring Wrap Counter:每次 after device marks the last descriptor used 的时候会更新这个,简便我们叫 counter b。

It is easy to see that the Driver Ring Wrap Counter in the driver matches the Device Ring Wrap Counter in the device when both are processing the same descriptor, or when all available descriptors have been used. 所以看出来两个 index 是一个互相追逐的过程,一个在前面放 available entry,一个在后面跟着捡 available entry 然后变成 used entry。

struct VRingPackedDesc QEMU

typedef struct VRingPackedDesc {
    uint64_t addr;
    uint32_t len;
    // 只有对 frontend (driver) 才有意义
    uint16_t id;
    // To mark a descriptor as available, the frontend (driver) makes the AVAIL(0x7) flag
    // To mark a descriptor as used, the backend (device) makes the USED(0x15) flag
    uint16_t flags;
} VRingPackedDesc;

Packed virtqueue: How to reduce overhead with virtio

struct VirtQueue QEMU

struct VirtQueue
{
    // 三个 vring 都在里面表示了
    // 详情请看这个结构体
    VRing vring;
    VirtQueueElement *used_elems;

    // 三个 Vring 数据结构
    /* Next head to pop */
    uint16_t last_avail_idx;
    bool last_avail_wrap_counter;

    /* Last avail_idx read from VQ. */
    uint16_t shadow_avail_idx;
    bool shadow_avail_wrap_counter;

    uint16_t used_idx;
    bool used_wrap_counter;

    /* Last used index value we have signalled on */
    uint16_t signalled_used;

    /* Last used index value we have signalled on */
    bool signalled_used_valid;

    // 如果是 1,表示允许 notification,如果是 0 则不允许
    bool notification;

    uint16_t queue_index;

    // 表示在用的 descriptor entry 的数量
    // 结合 vring-> 可以用来判断是不是满了
    unsigned int inuse;

    // 好像是和中断有关的。irqfd,device 用来向 guest driver 注入中断
    // 详参 virtio_queue_set_vector(), virtio_notify_vector() 
    // 
    uint16_t vector;
    // 用来处理 guest 里的 driver 请求来数据的函数。
    VirtIOHandleOutput handle_output;
    VirtIODevice *vdev;
    EventNotifier guest_notifier;
    EventNotifier host_notifier;
    bool host_notifier_enabled;
    QLIST_ENTRY(VirtQueue) node;
};

handle_output() QEMU

当有新的 ioeventfd poll 返回的时候,我们会 call 到这个函数。具体流程请看 ioeventfd 里 notify to QEMU^。

event_notifier_set_handler(&vq->host_notifier, virtio_queue_host_notifier_read);
    virtio_queue_notify_vq
        vq->handle_output()
            virtio_blk_handle_output

Vring

Vring is a memory mapped region between QEMU and guest OS.

代码在 include/uapi/linux/virtio_ring.h

Although we believe any optimal transport will share similar characteristics, the Linux virtio/virtqueue API is biassed towards our particular transport implementation, called virtio ring.

The virtio ring consists of three parts(一个 array 两个 ring):

  • The descriptor array where the guest chains together (address, length) pairs. 这个 chain 是由 Guest 建立起来的,但是 host 也会去用(不会更改链)。一个 descriptor array 有多条 descriptor chain。
  • The available ring where the guest indicates what descriptors chains are ready for use.
  • The used ring where the host indicates which descriptors chains it has used.

为什么要设计 available ring 和 used ring 这两个 ring 呢?我觉得一个 ring 好像就够了,像生产者和消费者模型那样设计两个 index 一个用来 driver 更新另一个让 device 来更新,两个 index 中间的部分就是还没有处理的 request,这样难道不是更加简洁吗?我觉得可能这是为了效率或者 race condition 考虑的?一个 Ring 只专心用来 track 还没有处理的,一个 Ring 只专心用来 track 处理过的。

struct vring {
	unsigned int num;
    // desc chain,这是一个 array
	vring_desc_t *desc;
	vring_avail_t *avail;
	vring_used_t *used;
};

The size of the ring is variable, but must be a power of two.

struct vring_desc Kernel / Descriptor Table

这个结构体本身对于 device 是只读的吗?

是的,virtio spec 里写了:A device MUST NOT write to any descriptor table entry。

每个描述符会指向一块共享内存,

  • 如果是驱动写给设备的数据,则称这个描述符为 out 类型的(bit VRING_DESC_F_WRITE == 0),
  • 如果是设备写给驱动的数据,则称这个描述符为 in 类型(bit VRING_DESC_F_WRITE == 1)。

描述符先是 out 类型,再是 in 类型的。

这个结构表示一个 descriptor array 的 entry。可以看到 out 其实是以驱动为中心来说的。

需要明白,一个 descriptor array 可能包含很多个 descriptor chains。这些 chain 是通过 next 连起来的。而不是直接把地址 +1 从而得到下一个 entry(packed 是这样的)。一条描述符链记录一次 I/O 事件

这种实现好像也隐含意味着,如果两个不同的 entry 的 next 具有相同的值,那是不是两个 chain 在这里算是 merge 了?

/**
 * struct vring_desc - Virtio ring descriptors,
 * 16 bytes long. These can chain together via @next.
 *
 * @next: index of the next descriptor in the chain,
 *        if the VRING_DESC_F_NEXT flag is set. We chain unused
 *        descriptors via this, tooW.
 */
struct vring_desc {
    // GPA
	__virtio64 addr;
	__virtio32 len;
    // 2 bits:
    //  - VRING_DESC_F_NEXT to indicate whether the next field is valid and,
    //  - VRING_DESC_F_WRITE controls whether the buffer is read-only or writeonly.
    //    By convention, readable buffers precede writable buffers.
    //  - VRING_DESC_F_INDIRECT 表示共享内存上将不是直接保存数据,而是保存一连串描述符。
	__virtio16 flags;
    // index (not the pointer) of the next descriptor in the chain
	__virtio16 next;
};

struct vring_avail Kernel / 可用描述符表

表示一个 available ring(注意不是一个 ring entry 哦)。

ring[] 里的每一个元素表示一个 chain 的 head。ring size 等于 descriptor array 的 size(因为一个 array 最多有 size 个 chain)。

struct vring_avail {
    // interrupt suppression flag
	__virtio16 flags;
    // Guest 端 driver 下一个填充的可用描述符
    // 结合 vhost_virtqueue->last_avail_idx 可以得到之间是等待 device 处理的可用描述符
	__virtio16 idx;
    // An array of indices into the descriptor table
	__virtio16 ring[];
};

struct vring_used Kernel / 已用描述符表

表示一个 used ring(注意不是一个 ring entry 哦)。

ring[] 里的每一个元素表示一个 chain 的 head。ring size 等于 descriptor array 的 size(因为一个 array 最多有 size 个 chain)。

struct vring_used {
	__virtio16 flags;
    // idx 由 host 端的设备维护,表示需要回收的 chain 的 idx
    // 结合 vringh->last_used_idx(Guest 端 driver 维护,表示已经回收的 idx)
    // 可以得到应该回收但是还没有回收的部分。
	__virtio16 idx;
	vring_used_elem_t ring[];
};

Allocate the memory for the descriptor tables

setup_vq
    // 拿到 queue size(for the tables)
	num = vp_modern_get_queue_size(mdev, index);
    vring_create_virtqueue
        vring_create_virtqueue_split
            vring_alloc_queue_split
                queue = vring_alloc_queue(vdev, vring_size(num, vring_align)...)
                    alloc_pages_exact(PAGE_ALIGN(size), flag);
                vring_init(queue)

next_desc() Kernel

vHost 里的。

static unsigned next_desc(struct vhost_virtqueue *vq, struct vring_desc *desc)
{
	unsigned int next;

    // 这个 desc 是 chain 的最后一个 desc
	if (!(desc->flags & cpu_to_vhost16(vq, VRING_DESC_F_NEXT)))
		return -1U;

    //...
	return desc->next;
}

vhost_get_desc() Kernel

vHost 里的。

作用就是拿到 idx 处的 desc。

把第 idx 个 desc 赋于传进来的 vring_desc 指针。

static inline int vhost_get_desc(struct vhost_virtqueue *vq, struct vring_desc *desc, int idx)
{
    // vq 是 vhost 里的 virtqueue,里面 desc 存了所有的 desc(难道)
	return vhost_copy_from_user(vq, desc, vq->desc + idx, sizeof(*desc));
}

struct virtio_dev_match

__device_attach
    // 当一个 virtio device attach 之后,找 virtio-bus 上的所有 driver 来 match
    bus_for_each_drv(dev->bus, NULL, &data, __device_attach_driver);
        __device_attach_driver
            driver_match_device
                // 这个函数是一个 bus 一个,说明一个 bus 对其上每一个 driver 用的 match 的方法是一致的
                ret = drv->bus->match(dev, drv)
                    virtio_dev_match
// 看一个 driver 能不能 match 这个 device
static int virtio_dev_match(struct device *_dv, struct device_driver *_dr)
{
	unsigned int i;
	struct virtio_device *dev = dev_to_virtio(_dv);
	const struct virtio_device_id *ids;

	ids = drv_to_virtio(_dr)->id_table;
	for (i = 0; ids[i].device; i++)
		if (virtio_id_match(dev, &ids[i]))
			return 1;
	return 0;
}

virtio_bus Kernel

virtio bus 的注册是所有 virtio 设备注册的基础,所有 virtio 设备的探测都是在 virtio bus 注册完成后进行的。从 virtio bus 注册到最后的设备探测,整个步骤如下:

  • 注册 virtio bus
  • 注册 virtio 设备驱动,比如 virtio net driver 或者 virtio blk driver
  • 注册 virtio 设备
  • 触发驱动核心 .match 操作 virtio_dev_match()
  • match 成功后驱动调用 probe 函数(virtnet_probe/virtblk_probe)探测 virtio 设备
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,
};

struct virtio_driver Guest kernel

/**
 * struct virtio_driver - operations for a virtio I/O driver
 * @driver: underlying device driver (populate name and owner).
 * @id_table: the ids serviced by this driver.
 * @feature_table: an array of feature numbers supported by this driver.
 * @feature_table_size: number of entries in the feature table array.
 * @feature_table_legacy: same as feature_table but when working in legacy mode.
 * @feature_table_size_legacy: number of entries in feature table legacy array.
 * @validate: the function to call to validate features and config space.
 *            Returns 0 or -errno.
 * @probe: the function to call when a device is found.  Returns 0 or -errno.
 * @scan: optional function to call after successful probe; intended
 *    for virtio-scsi to invoke a scan.
 * @remove: the function to call when a device is removed.
 * @config_changed: optional function to call when the device configuration
 *    changes; may be called in interrupt context.
 * @freeze: optional function to call during suspend/hibernation.
 * @restore: optional function to call on resume.
 */
struct virtio_driver {
	struct device_driver driver;
	const struct virtio_device_id *id_table;
	const unsigned int *feature_table;
	unsigned int feature_table_size;
	const unsigned int *feature_table_legacy;
	unsigned int feature_table_size_legacy;
	int (*validate)(struct virtio_device *dev);
	int (*probe)(struct virtio_device *dev);
	void (*scan)(struct virtio_device *dev);
	void (*remove)(struct virtio_device *dev);
	void (*config_changed)(struct virtio_device *dev);
	int (*freeze)(struct virtio_device *dev);
	int (*restore)(struct virtio_device *dev);
};

在初始化的时候(比如 virtio-blk 就是 kernel module init 的时候)调用 register_virtio_driver() 将这个 driver 进行注册。

register_virtio_driver() Kernel

int register_virtio_driver(struct virtio_driver *driver)
{
	/* Catch this early. */
	BUG_ON(driver->feature_table_size && !driver->feature_table);
	driver->driver.bus = &virtio_bus;
	return driver_register(&driver->driver);
}

virtqueue_kick() Guest kernel

这个函数为大多数 driver 提供了 kick 的实现,当然有一些 driver 并没有完全直接调用这个函数,比如 drivers/block/virtio_blk.cvirtio_queue_rq() 就没有。

bool virtqueue_kick(struct virtqueue *vq)
{
	if (virtqueue_kick_prepare(vq))
		return virtqueue_notify(vq);
	return true;
}

Rusty Russell's Virtio Paper

virtio: Towards a De-Facto Standard For Virtual I/O Devices: virtio-paper.pdf

Virtio: a series of efficient, well-maintained Linux drivers which can be adapted for various different hypervisor implementations using a shim layer.

VirtIO 来鹅城只干三件事:

Finally, we provide an implementation which presents the vring transport and device configuration as a PCI device: this means guest operating systems merely need a new PCI driver, and hypervisors need only add vring support to the virtual devices they implement (currently only KVM does this).

VirtIO Dataplane

What is Dataplane, is it implemented in QEMU or in kernel?

  • dataplane moves virtio-blk device emulation into dedicated thread.

VirtIO in Guest Kernel

virtio_find_vqs() / vp_modern_find_vqs() / vp_find_vqs() Guest kernel

后端对前端的通知,是以中断方式传递到前端的(这就是用 irqfd 的原因)。

static inline
int virtio_find_vqs(struct virtio_device *vdev, unsigned nvqs,
			struct virtqueue *vqs[], vq_callback_t *callbacks[],
			const char * const names[],
			struct irq_affinity *desc)
{
	return vdev->config->find_vqs(vdev, nvqs, vqs, callbacks, names, NULL, desc);
}

对于 PCI 来说,就是函数 vp_modern_find_vqs()。vp 的意思应该是 virt PCI?就像 vm 的意思是 virt MMIO。

static int vp_modern_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)
{
	struct virtio_pci_device *vp_dev = to_vp_device(vdev);
	struct virtqueue *vq;
	int rc = vp_find_vqs(vdev, nvqs, vqs, callbacks, names, ctx, desc);

    //...
	/* Select and activate all queues. Has to be done last: once we do
	 * this, there's no way to go back except reset.
	 */
	list_for_each_entry(vq, &vdev->vqs, list)
		vp_modern_set_queue_enable(&vp_dev->mdev, vq->index, true);

	return 0;
}

VirtIO in QEMU

代码主要都在 hw/virtio 下面。

和 vhost control plane 相关的应该都在 hw/virtio/vhost* 的文件里面。

如果 kernel 里没有相应 vhost 的实现,那是不是就是在 QEMU 里实现的,对应的就是 hw/virtio/virtio* 相关的文件?

如果 dataplane 是实现在了 QEMU 中,那么代码都在 hw/*/dataplane 等等,比如

  • hw/scsi/virtio-scsi-dataplane.c
  • hw/block/dataplane/*

vring_avail_idx() QEMU

static inline uint16_t vring_avail_idx(VirtQueue *vq)
{
    VRingMemoryRegionCaches *caches = vring_get_region_caches(vq);
    hwaddr pa = offsetof(VRingAvail, idx);

    if (!caches) {
        return 0;
    }

    vq->shadow_avail_idx = virtio_lduw_phys_cached(vq->vdev, &caches->avail, pa);
    return vq->shadow_avail_idx;
}

virtio_add_queue() QEMU

给一个设备添加一个 virtqueue(一个设备可以有多个 virtqueue)。

VirtQueue *virtio_add_queue(VirtIODevice *vdev, int queue_size, VirtIOHandleOutput handle_output)
{
    int i;

    for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {
        if (vdev->vq[i].vring.num == 0)
            break;
    }

    //...
    vdev->vq[i].vring.num = queue_size;
    vdev->vq[i].vring.num_default = queue_size;
    vdev->vq[i].vring.align = VIRTIO_PCI_VRING_ALIGN;
    vdev->vq[i].handle_output = handle_output;
    vdev->vq[i].used_elems = g_new0(VirtQueueElement, queue_size);

    return &vdev->vq[i];
}

virtio_queue_notify_vq() / virtio_queue_notify() QEMU

在一个 virtqueue 收到了 Guest Driver (frontend) 下发 IO 的通知(也就是 QEMU 的 iothread 收到了 ioeventfd 得到的)后,我们会调用这个函数 virtio_queue_notify_vq() 进行处理,进一步调用回调函数 handle_output()。这个回调函数是在 virtio_add_queue()^ 里置上的。


virtio_device_start_ioeventfd_impl
event_notifier_set_handler(&vq->host_notifier, virtio_queue_host_notifier_read);
    virtio_queue_host_notifier_read
static void virtio_queue_notify_vq(VirtQueue *vq)
{
    if (vq->vring.desc && vq->handle_output) {
        VirtIODevice *vdev = vq->vdev;
        //...
        vq->handle_output(vdev, vq);
        //...
    }
}

virtio_queue_host_notifier_read() QEMU

这是一个 common 的函数。检查一个 ioeventfd 有没有消息 event_notifier_test_and_clear(),有的话就处理。

void virtio_queue_host_notifier_read(EventNotifier *n)
{
    VirtQueue *vq = container_of(n, VirtQueue, host_notifier);
    if (event_notifier_test_and_clear(n)) {
        virtio_queue_notify_vq(vq);
    }
}

event_notifier_test_and_clear() QEMU

检查 ioeventfd 有没有新的请求,数据内容是什么不重要,因为压根我们没有用。我们做的只是把这个 fd 的要读的内容清空。

int event_notifier_test_and_clear(EventNotifier *e)
{
    int value;
    ssize_t len;
    char buffer[512];

    //...
    /* 清空 the notify pipe.  For eventfd, only 8 bytes will be read.  */
    value = 0;
    do {
        len = read(e->rfd, buffer, sizeof(buffer));
        value |= (len > 0);
    // 如果被打断了,那么重试;如果这次读了很多,那么继续读直到读完所有的数据
    } while ((len == -1 && errno == EINTR) || len == sizeof(buffer));

    return value;
}

struct VRingDesc / struct VRingAvail / struct VRingUsed QEMU

对应 kernel 里的 struct vring_desc^,表示一个描述符表里的 entry(descriptor),也就是一个物理 buffer。可以参考那里的解释,毕竟数据结构是一模一样的。

typedef struct VRingDesc
{
    // buffer 地址以及长度
    uint64_t addr;
    uint32_t len;
    uint16_t flags;
    // 下一个 buffer 在哪里,这样才能形成一个 descriptor chain
    uint16_t next;
} VRingDesc;

一个 Avail Vring 的 entry。指向 descriptor table 的一个 chain 的 head。

typedef struct VRingAvail
{
    uint16_t flags;
    uint16_t idx;
    uint16_t ring[];
} VRingAvail;
typedef struct VRingUsed
{
    uint16_t flags;
    uint16_t idx;
    VRingUsedElem ring[];
} VRingUsed;

struct VirtQueueElement QEMU

QEMU 中用 VirtQueueElement 结构表示一个逻辑 buffer,一个逻辑 buffer 包含了多个物理 buffer。应该可以和一个 descriptor chain 对应。

一个物理 buffer 等价于一个描述符表里的 entry。

这个结构体包含多个描述符表里的 entry。

物理 buffer 是 device driver 也就是前端提供的,有的 buffer 用于 guest 发数据,有的用于 guest 收数据。如果是用来 guest 发数据的,那么对于后端来说就是只读的;如果是用来收数据的,那么后端需要提供数据,对于后端就是可写的。

typedef struct VirtQueueElement
{
    // 在描述符表取描述符时的 起始 索引
    unsigned int index;
    unsigned int len;
    // 当前 element 包含的总描述符个数
    unsigned int ndescs;
    // 一般来说,应该是当前 element 中包含的发送描述符个数,即 descriptor table entry 个数
    // 更精确来说,应该是 iovec 的数量(一个 descriptor 不一定就 map 成了一个 iovec,可能是多个)
    unsigned int out_num;
    // 当前 element 中包含的接受描述符个数
    // 更精确来说,同上。
    unsigned int in_num;
    // 接收 buffer 的起始 GPA,注意这是一个 array,长度由 in_num 决定
    hwaddr *in_addr;
    // 发送 buffer 的起始 GPA,注意这是一个 array,长度由 out_num 决定
    hwaddr *out_addr;
    // 接收 buffer 的起始 HVA(由 QEMU 从 GPA 转化而来)
    // 注意这是一个 array,长度由 in_num 决定
    struct iovec *in_sg;
    // 发送 buffer 的起始 HVA
    // 注意这是一个 array,长度由 out_num 决定
    struct iovec *out_sg;
} VirtQueueElement;

virtqueue_detach_element() / virtqueue_unmap_sg() QEMU

void virtqueue_detach_element(VirtQueue *vq, const VirtQueueElement *elem,
                              unsigned int len)
{
    // 把在用的 descriptor 的数量减去 ndescs
    vq->inuse -= elem->ndescs;
    //
    virtqueue_unmap_sg(vq, elem, len);
}

// 把这个 element 从 vq 里移除,包括这个 element 里的每一个 descriptor。
static void virtqueue_unmap_sg(VirtQueue *vq, const VirtQueueElement *elem,
                               unsigned int len)
{
    AddressSpace *dma_as = vq->vdev->dma_as;
    unsigned int offset;
    int i;

    offset = 0;
    for (i = 0; i < elem->in_num; i++) {
        size_t size = MIN(len - offset, elem->in_sg[i].iov_len);

        dma_memory_unmap(dma_as, elem->in_sg[i].iov_base,
                         elem->in_sg[i].iov_len,
                         DMA_DIRECTION_FROM_DEVICE, size);

        offset += size;
    }

    for (i = 0; i < elem->out_num; i++)
        dma_memory_unmap(dma_as, elem->out_sg[i].iov_base,
                         elem->out_sg[i].iov_len,
                         DMA_DIRECTION_TO_DEVICE,
                         elem->out_sg[i].iov_len);
}

virtio_dev_match() QEMU

/* This looks through all the IDs a driver claims to support.  If any of them
 * match, we return 1 and the kernel will call virtio_dev_probe(). */
static int virtio_dev_match(struct device *_dv, struct device_driver *_dr)
{
	unsigned int i;
    // 把 device 转成 virtio_device
	struct virtio_device *dev = dev_to_virtio(_dv);
	const struct virtio_device_id *ids;

    // driver 有一个 table,存着这个 drvier 支持的所有类型 device id。
	ids = drv_to_virtio(_dr)->id_table;
	for (i = 0; ids[i].device; i++)
		if (virtio_id_match(dev, &ids[i]))
			return 1;
	return 0;
}

virtqueue_map_desc() QEMU

这个函数只 map 一个 buffer (descriptor)。也就是一个 VRing 的 entry。不要把它理解成 map 了一个链。

map_ok = virtqueue_map_desc(vdev, &in_num, addr + out_num,
                            iov + out_num,
                            VIRTQUEUE_MAX_SIZE - out_num, true,
                            desc.addr, desc.len);

static bool virtqueue_map_desc(VirtIODevice *vdev, unsigned int *p_num_sg,
                               hwaddr *addr, struct iovec *iov,
                               unsigned int max_num_sg, bool is_write,
                               hwaddr pa, size_t sz)
{
    unsigned num_sg = *p_num_sg;

    //...
    // 找 buffer 的过程。
    // 每一个 descriptor 对应一个 buffer,这一个 buffer 用多个 iovec 来表示(我也不知道为啥,可能一次分配不了这么多)
    while (sz) {
        hwaddr len = sz;
        //...
        // 这个 buffer 对应的 HVA 的地址
        iov[num_sg].iov_base = dma_memory_map(vdev->dma_as, pa, &len,
                                              is_write ?
                                              DMA_DIRECTION_FROM_DEVICE :
                                              DMA_DIRECTION_TO_DEVICE,
                                              MEMTXATTRS_UNSPECIFIED);
        //...
        iov[num_sg].iov_len = len;
        addr[num_sg] = pa;
        sz -= len;
        pa += len;
        num_sg++;
    }

    *p_num_sg = num_sg;
}

vring_create_virtqueue() Kernel

vring_create_virtqueue_split() Kernel

static struct virtqueue *vring_create_virtqueue_split(
	unsigned int index,
	unsigned int num,
	unsigned int vring_align,
	struct virtio_device *vdev,
	bool weak_barriers,
	bool may_reduce_num,
	bool context,
	bool (*notify)(struct virtqueue *),
	void (*callback)(struct virtqueue *),
	const char *name,
	struct device *dma_dev)
{
	struct vring_virtqueue_split vring_split = {};
	struct virtqueue *vq;
    //...
	vring_alloc_queue_split(&vring_split, vdev, num, vring_align, may_reduce_num, dma_dev);
    //...
	vq = __vring_new_virtqueue(index, &vring_split, vdev, weak_barriers, context, notify, callback, name, dma_dev);
    //...
	to_vvq(vq)->we_own_ring = true;
	return vq;
}

struct VirtIODevice QEMU

struct VirtIODevice
{
    DeviceState parent_obj;
    // Name of the device
    const char *name;
    // VirtIO Device Status field (See VirtIO Spec: 2.1 Device Status Field)
    uint8_t status;
    // In-service register?
    uint8_t isr;
    uint16_t queue_sel;
    /**
     * These fields represent a set of VirtIO features at various
     * levels of the stack. @host_features indicates the complete
     * feature set the VirtIO device can offer to the driver.
     * @guest_features indicates which features the VirtIO driver has
     * selected by writing to the feature register. Finally
     * @backend_features represents everything supported by the
     * backend (e.g. vhost) and could potentially be a subset of the
     * total feature set offered by QEMU.
     */
    uint64_t host_features;
    // VirtIOPCIProxy 也有类似的 field。
    uint64_t guest_features;
    uint64_t backend_features;

    size_t config_len;
    // 
    void *config;
    uint16_t config_vector;
    uint32_t generation;
    int nvectors;
    VirtQueue *vq;
    MemoryListener listener;
    uint16_t device_id;
    /* @vm_running: current VM running state via virtio_vmstate_change() */
    bool vm_running;
    bool broken; /* device in invalid state, needs reset */
    bool use_disabled_flag; /* allow use of 'disable' flag when needed */
    bool disabled; /* device in temporarily disabled state */
    /**
     * @use_started: true if the @started flag should be used to check the
     * current state of the VirtIO device. Otherwise status bits
     * should be checked for a current status of the device.
     * @use_started is only set via QMP and defaults to true for all
     * modern machines (since 4.1).
     */
    bool use_started;
    bool started;
    bool start_on_kick; /* when virtio 1.0 feature has not been negotiated */
    bool disable_legacy_check;
    bool vhost_started;
    VMChangeStateEntry *vmstate;
    char *bus_name;
    uint8_t device_endian;
    /**
     * @user_guest_notifier_mask: gate usage of ->guest_notifier_mask() callback.
     * This is used to suppress the masking of guest updates for
     * vhost-user devices which are asynchronous by design.
     */
    bool use_guest_notifier_mask;
    // virtio_bus_device_plugged
    //     vdev->dma_as = &address_space_memory;
    AddressSpace *dma_as;
    QLIST_HEAD(, VirtQueue) *vector_queues;
    QTAILQ_ENTRY(VirtIODevice) next;
    /**
     * @config_notifier: the event notifier that handles config events
     */
    EventNotifier config_notifier;
    bool device_iotlb_enabled;
};

vdev->status QEMU

// 下面的这些 bit 是让 guest 来 report progress,然后同步 features 的
/* We have seen device and processed generic fields (VIRTIO_CONFIG_F_VIRTIO) */
#define VIRTIO_CONFIG_S_ACKNOWLEDGE	1
/* We have found a driver for the device. */
#define VIRTIO_CONFIG_S_DRIVER		2
/* Driver has used its parts of the config, and is happy */
#define VIRTIO_CONFIG_S_DRIVER_OK	4
/* Driver has finished configuring features */
#define VIRTIO_CONFIG_S_FEATURES_OK	8
/* Device entered invalid state, driver must reset it */
#define VIRTIO_CONFIG_S_NEEDS_RESET	0x40
/* We've given up on this device. */
#define VIRTIO_CONFIG_S_FAILED		0x80

vdev->isr QEMU

virtio_notify_config / virtio_irq / virtio_notify_irqfd
    virtio_set_isr
        qatomic_or(&vdev->isr, value);

vDPA

Virtio 作为一种半虚拟化的解决方案,其性能一直不如设备的 pass-through。

A vDPA device means a type of device whose datapath complies with the virtio specification, but whose control path is vendor specific. vDPA devices can be both physically located on the hardware or emulated by software.

vDPA 网卡,datapath 遵循 virtio 规范,而控制面由厂家驱动提供。这样 datapath 是直接 VM 的 VQ 和 hardware device 直接交互,性能很好。

virtio data path acceleration.

Redhat 提出的硬件 vDPA 架构,目前在 DPDK 和内核程序中均有实现,基本是未来的标准架构。

这里汇总了一些关于 vDPA 的资料,有时间可以看看:vDPA - virtio Data Path Acceleration - vDPA - virtio Data Path Acceleration

Vhost 也是加速 data path 的,这两者的区别是什么?

  • vHost 是把 data path offload 到 kernel。
  • vDPA 更倾向于 hardware accelaration,也就是用 hardware 来 offload 并加速 data path。

VirtIO PCI

struct virtio_pci_legacy_device QEMU

struct virtio_pci_legacy_device {
	struct pci_dev *pci_dev;

	/* Where to read and clear interrupt */
	u8 __iomem *isr;
	/* The IO mapping for the PCI config space (legacy mode only) */
	void __iomem *ioaddr;

	struct virtio_device_id id;
};


// 这个作为 driver 的 .probe() function callback
// driver 的 probe() 会优先被调用而不是 bus 的。
virtio_pci_probe
    virtio_pci_legacy_probe
        vp_legacy_probe
            ldev->ioaddr = pci_iomap(pci_dev, 0, 0);

setup_vq() Guest kernel / virtqueue shared memory allocation

注意这个函数是同时定义在文件

  • drivers/virtio/virtio_pci_legacy.c
  • drivers/virtio/virtio_pci_modern.c

这意味着这个函数并不是仅仅给 modern 用的,legacy 也会用到。

// legacy 版本的实现
// 参数 index:表示我们要 setup 第几个 virtqueue
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)
{
	struct virtqueue *vq;
	u16 num;
	int err;
	u64 q_pfn;

	/* Check if queue is either not available or already active. */
    // vp_dev->ldev: vrtio-pci legacy device
    // 通过 ioread16() 来读一下,看看这个 index 有没有对应的
	num = vp_legacy_get_queue_size(&vp_dev->ldev, index);
	if (!num || vp_legacy_get_queue_enable(&vp_dev->ldev, index))
		return ERR_PTR(-ENOENT);

	info->msix_vector = msix_vec;

	/* create the vring */
	vq = vring_create_virtqueue(index, num,
				    VIRTIO_PCI_VRING_ALIGN, &vp_dev->vdev,
				    true, false, ctx,
				    vp_notify, callback, name);
	if (!vq)
		return ERR_PTR(-ENOMEM);

	vq->num_max = num;

	q_pfn = virtqueue_get_desc_addr(vq) >> VIRTIO_PCI_QUEUE_ADDR_SHIFT;
	if (q_pfn >> 32) {
		dev_err(&vp_dev->pci_dev->dev,
			"platform bug: legacy virtio-pci must not be used with RAM above 0x%llxGB\n",
			0x1ULL << (32 + PAGE_SHIFT - 30));
		err = -E2BIG;
		goto out_del_vq;
	}

	/* activate the queue */
	vp_legacy_set_queue_address(&vp_dev->ldev, index, q_pfn);

	vq->priv = (void __force *)vp_dev->ldev.ioaddr + VIRTIO_PCI_QUEUE_NOTIFY;

	if (msix_vec != VIRTIO_MSI_NO_VECTOR) {
		msix_vec = vp_legacy_queue_vector(&vp_dev->ldev, index, msix_vec);
		if (msix_vec == VIRTIO_MSI_NO_VECTOR) {
			err = -EBUSY;
			goto out_deactivate;
		}
	}

	return vq;

out_deactivate:
	vp_legacy_set_queue_address(&vp_dev->ldev, index, 0);
out_del_vq:
	vring_del_virtqueue(vq);
	return ERR_PTR(err);
}

Modern 版本的实现:

VirtIO Live Migration

#define VMSTATE_VIRTIO_DEVICE \
    {                                         \
        .name = "virtio",                     \
        .info = &virtio_vmstate_info,         \
        .flags = VMS_SINGLE,                  \
    }

const VMStateInfo virtio_vmstate_info = {
    .name = "virtio",
    .get = virtio_device_get,
    // device 也就是 virtio backend 有一些状态需要迁移过去
    // virtio 设备是一种 non-iterative device,non-iterative device 的
    // 迁移调用的就是 put。
    .put = virtio_device_put,
};

Make the shared VRing pages dirty from the virtio host device side (QEMU)

// place to set the used vring to dirty
// 可以看到是 bypass 了 KVM,直接从 QEMU 这里置为 dirty 了。
vring_used_idx_set
    address_space_cache_invalidate
        invalidate_and_set_dirty
            cpu_physical_memory_set_dirty_range

virtio_device_get() / virtio_load() QEMU

int virtio_load(VirtIODevice *vdev, QEMUFile *f, int version_id)
{
    int i, ret;
    int32_t config_len;
    uint32_t num;
    uint32_t features;
    BusState *qbus = qdev_get_parent_bus(DEVICE(vdev));
    VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
    VirtioDeviceClass *vdc = VIRTIO_DEVICE_GET_CLASS(vdev);

    /*
     * We poison the endianness to ensure it does not get used before
     * subsections have been loaded.
     */
    vdev->device_endian = VIRTIO_DEVICE_ENDIAN_UNKNOWN;

    if (k->load_config) {
        ret = k->load_config(qbus->parent, f);
        if (ret)
            return ret;
    }

    qemu_get_8s(f, &vdev->status);
    qemu_get_8s(f, &vdev->isr);
    qemu_get_be16s(f, &vdev->queue_sel);
    //...
    qemu_get_be32s(f, &features);

    /*
     * Temporarily set guest_features low bits - needed by
     * virtio net load code testing for VIRTIO_NET_F_CTRL_GUEST_OFFLOADS
     * VIRTIO_NET_F_GUEST_ANNOUNCE and VIRTIO_NET_F_CTRL_VQ.
     *
     * Note: devices should always test host features in future - don't create
     * new dependencies like this.
     */
    vdev->guest_features = features;

    config_len = qemu_get_be32(f);
    /*
     * There are cases where the incoming config can be bigger or smaller
     * than what we have; so load what we have space for, and skip
     * any excess that's in the stream.
     */
    qemu_get_buffer(f, vdev->config, MIN(config_len, vdev->config_len));

    while (config_len > vdev->config_len) {
        qemu_get_byte(f);
        config_len--;
    }

    // virtqueue 的数量
    num = qemu_get_be32(f);

    //...
    // 对于每一个 virtqueue,我们都要执行的 load 流程
    for (i = 0; i < num; i++) {
        // 这个 virtqueue 的长度
        vdev->vq[i].vring.num = qemu_get_be32(f);
        if (k->has_variable_vring_alignment)
            vdev->vq[i].vring.align = qemu_get_be32(f);
        vdev->vq[i].vring.desc = qemu_get_be64(f);
        qemu_get_be16s(f, &vdev->vq[i].last_avail_idx);
        vdev->vq[i].signalled_used_valid = false;
        vdev->vq[i].notification = true;

        // 表示 desc 的地址是 null,同时又有 avail_idx,那么就是 inconsistent 了
        if (!vdev->vq[i].vring.desc && vdev->vq[i].last_avail_idx) {
            error_report("VQ %d address 0x0 inconsistent with Host index 0x%x",
                         i, vdev->vq[i].last_avail_idx);
            return -1;
        }
        if (k->load_queue)
            k->load_queue(qbus->parent, i, f);
            //...
    }

    virtio_notify_vector(vdev, VIRTIO_NO_VECTOR);

    // specific 的比如 virtio-blk
    if (vdc->load != NULL)
        vdc->load(vdev, f, version_id);
        //...

    if (vdc->vmsd)
        ret = vmstate_load_state(f, vdc->vmsd, vdev, version_id);
        //...

    /* Subsections */
    ret = vmstate_load_state(f, &vmstate_virtio, vdev, 1);
    //...

    if (vdev->device_endian == VIRTIO_DEVICE_ENDIAN_UNKNOWN)
        vdev->device_endian = virtio_default_endian();

    if (virtio_64bit_features_needed(vdev)) {
        /*
         * Subsection load filled vdev->guest_features.  Run them
         * through virtio_set_features to sanity-check them against
         * host_features.
         */
        uint64_t features64 = vdev->guest_features;
        if (virtio_set_features_nocheck(vdev, features64) < 0) {
            error_report("Features 0x%" PRIx64 " unsupported. "
                         "Allowed features: 0x%" PRIx64,
                         features64, vdev->host_features);
            return -1;
        }
    } else {
        if (virtio_set_features_nocheck(vdev, features) < 0) {
            error_report("Features 0x%x unsupported. "
                         "Allowed features: 0x%" PRIx64,
                         features, vdev->host_features);
            return -1;
        }
    }

    if (!virtio_device_started(vdev, vdev->status) &&
        !virtio_vdev_has_feature(vdev, VIRTIO_F_VERSION_1)) {
        vdev->start_on_kick = true;
    }

    RCU_READ_LOCK_GUARD();
    for (i = 0; i < num; i++) {
        if (vdev->vq[i].vring.desc) {
            uint16_t nheads;

            /*
             * VIRTIO-1 devices migrate desc, used, and avail ring addresses so
             * only the region cache needs to be set up.  Legacy devices need
             * to calculate used and avail ring addresses based on the desc
             * address.
             */
            if (virtio_vdev_has_feature(vdev, VIRTIO_F_VERSION_1)) {
                virtio_init_region_cache(vdev, i);
            } else {
                virtio_queue_update_rings(vdev, i);
            }

            if (virtio_vdev_has_feature(vdev, VIRTIO_F_RING_PACKED)) {
                vdev->vq[i].shadow_avail_idx = vdev->vq[i].last_avail_idx;
                vdev->vq[i].shadow_avail_wrap_counter =
                                        vdev->vq[i].last_avail_wrap_counter;
                continue;
            }

            // 表示还有多少个未处理的?
            nheads = vring_avail_idx(&vdev->vq[i]) - vdev->vq[i].last_avail_idx;
            // Check it isn't doing strange things with descriptor numbers.
            if (nheads > vdev->vq[i].vring.num) {
                virtio_error(vdev, "VQ %d size 0x%x Guest index 0x%x "
                             "inconsistent with Host index 0x%x: delta 0x%x",
                             i, vdev->vq[i].vring.num,
                             vring_avail_idx(&vdev->vq[i]),
                             vdev->vq[i].last_avail_idx, nheads);
                vdev->vq[i].used_idx = 0;
                vdev->vq[i].shadow_avail_idx = 0;
                vdev->vq[i].inuse = 0;
                continue;
            }
            vdev->vq[i].used_idx = vring_used_idx(&vdev->vq[i]);
            vdev->vq[i].shadow_avail_idx = vring_avail_idx(&vdev->vq[i]);

            /*
             * Some devices migrate VirtQueueElements that have been popped
             * from the avail ring but not yet returned to the used ring.
             * Since max ring size < UINT16_MAX it's safe to use modulo
             * UINT16_MAX + 1 subtraction.
             */
            vdev->vq[i].inuse = (uint16_t)(vdev->vq[i].last_avail_idx -
                                vdev->vq[i].used_idx);
            if (vdev->vq[i].inuse > vdev->vq[i].vring.num) {
                error_report("VQ %d size 0x%x < last_avail_idx 0x%x - "
                             "used_idx 0x%x",
                             i, vdev->vq[i].vring.num,
                             vdev->vq[i].last_avail_idx,
                             vdev->vq[i].used_idx);
                return -1;
            }
        }
    }

    if (vdc->post_load) {
        ret = vdc->post_load(vdev);
        if (ret) {
            return ret;
        }
    }

    return 0;
}

virtio_device_put() / virtio_save() QEMU

把 virtio device 这边的一些状态发送过去。对于每一个 virtio device 的迁移都会调用一遍这个函数。会首先把 virtio 设备通用的数据发送过去,然后发送 virtio 特有的数据比如 virtio-blk 的数据。

vmstate_save_state_v
    field->info->put
static int virtio_device_put(QEMUFile *f, void *opaque, size_t size,
                              const VMStateField *field, JSONWriter *vmdesc)
{
    return virtio_save(VIRTIO_DEVICE(opaque), f);
}

// 在看这个函数的时候,要始终留意什么是特有的(比如 virtio-blk 自己的特殊的处理方式)
// 什么是通用的(virtio 设备都会有的设备和流程)。
int virtio_save(VirtIODevice *vdev, QEMUFile *f)
{
    BusState *qbus = qdev_get_parent_bus(DEVICE(vdev));
    VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
    VirtioDeviceClass *vdc = VIRTIO_DEVICE_GET_CLASS(vdev);
    uint32_t guest_features_lo = (vdev->guest_features & 0xffffffff);
    int i;

    // -------------------- 通用的 --------------------
    if (k->save_config)
        // see virtio_pci_save_config^()
        // 作为一个 PCI 设备,发送一些 PCI 的状态过去。
        // 比如 PCI configuration space 的内容,比如 PCI 的 irq_state
        k->save_config(qbus->parent, f);

    // 设备的 Device Status Field,就是 device 和 driver
    // 之间协商的状态之类的。
    qemu_put_8s(f, &vdev->status);
    // Interrupt Status Register for virtio
    qemu_put_8s(f, &vdev->isr);
    // 当前选中的是哪一个 queue?发送过去
    qemu_put_be16s(f, &vdev->queue_sel);
    // device 最后被 driver 置上的 features
    // 为啥又要发一遍?之后在 &vmstate_virtio 中应该也会发吧。
    // 难道是先把 low 32bit 的发过去要先用?
    qemu_put_be32s(f, &guest_features_lo);
    // 先把 config 长度发送过去,然后把 config 的 buffer 发送过去
    // config 指的是 device specific configuration space^
    // virtio-pci 和 virtio-mmio 都有的
    qemu_put_be32(f, vdev->config_len);
    qemu_put_buffer(f, vdev->config, vdev->config_len);

    // 找到被使用的 virtqueue 的数量(不一定所有 vring 都被用了),发送过去。
    for (i = 0; i < VIRTIO_QUEUE_MAX; i++)
        if (vdev->vq[i].vring.num == 0)
            break;
    qemu_put_be32(f, i);

    // 对于每一个已经使用的 virtqueue
    for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {
        if (vdev->vq[i].vring.num == 0)
            break;

        // 发送这个 virtqueue 长度
        qemu_put_be32(f, vdev->vq[i].vring.num);

        if (k->has_variable_vring_alignment) {
            qemu_put_be32(f, vdev->vq[i].vring.align);
        }
        // desc 表示 descriptor virtqueue 地址(GPA)
        // 先发送 desc 过去,其他 vring 地址(avail, used)会作为 subsection 发过去
        qemu_put_be64(f, vdev->vq[i].vring.desc);
        qemu_put_be16s(f, &vdev->vq[i].last_avail_idx);
        if (k->save_queue)
            // virtio_pci_save_queue^
            // 主要是为了发送 queue 用来做 notification 的 vector 过去
            // 告诉 destination 我们是用什么 vector 来进行 irqfd 通知的
            k->save_queue(qbus->parent, i, f);
    }
    // -------------------- 特有的(比如 virtio-blk) --------------------
    // save 函数已经重载过了,这里表示特定类型的 save,
    // 比如 virtio blk 的 save (virtio_blk_save_device^)。
    // 会把每一个 request 发过来。
    if (vdc->save)
        vdc->save(vdev, f);

    // virtio-blk 此值为 NULL
    if (vdc->vmsd) 
        vmstate_save_state(f, vdc->vmsd, vdev, NULL);

    /* Subsections */
    return vmstate_save_state(f, &vmstate_virtio, vdev, NULL);
}

virtio_pci_save_queue() QEMU

static void virtio_pci_save_queue(DeviceState *d, int n, QEMUFile *f)
{
    VirtIOPCIProxy *proxy = to_virtio_pci_proxy(d);
    VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);

    if (msix_present(&proxy->pci_dev))
        qemu_put_be16(f, virtio_queue_vector(vdev, n));
}

virtio_pci_save_config() QEMU

static void virtio_pci_save_config(DeviceState *d, QEMUFile *f)
{
    VirtIOPCIProxy *proxy = to_virtio_pci_proxy(d);
    VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);

    // 发送 configuration space 内容过去;发送 irq_state 过去。
    pci_device_save(&proxy->pci_dev, f);
    // 发送 msix 相关的东西
    msix_save(&proxy->pci_dev, f);
    if (msix_present(&proxy->pci_dev))
        qemu_put_be16(f, vdev->config_vector);
}

pci_device_save() / struct vmstate_pci_device QEMU

这个函数发送两个东西到 destination:

  • PCI configuration space 里的内容;
  • irq_state
void pci_device_save(PCIDevice *s, QEMUFile *f)
{
    // PCI_STATUS 是 PCI configuration space header 的一个 register。
    // 因为我们已经在迁移 s->irq_state 了,所以 dst 看了 irq_state 之后就知道
    // 应不应该置上这个 bit。这样我们可以和老的设备版本兼容。
    /* Clear interrupt status bit: it is implicit in irq_state which we are saving.
     * This makes us compatible with old devices which never set or clear this bit. */
    s->config[PCI_STATUS] &= ~PCI_STATUS_INTERRUPT;
    vmstate_save_state(f, &vmstate_pci_device, s, NULL);
    // Restore the interrupt status bit.
    pci_update_irq_status(s);
}

const VMStateDescription vmstate_pci_device = {
    .name = "PCIDevice",
    .version_id = 2,
    .minimum_version_id = 1,
    .fields = (VMStateField[]) {
        VMSTATE_INT32_POSITIVE_LE(version_id, PCIDevice),
        // 下面两个 VMSTATE 是互斥的,因为可以看到有两个互斥的 test 的 functions
        //   - migrate_is_not_pcie()
        //   - migrate_is_pcie()
        // 如果是 PCI 就发送 PCI_CONFIG_SPACE_SIZE 大小(256B);如果是 PCIE,就发送 
        // PCIE_CONFIG_SPACE_SIZE 大小(4KB)。
        VMSTATE_BUFFER_UNSAFE_INFO_TEST(config, PCIDevice,
                                   migrate_is_not_pcie,
                                   0, vmstate_info_pci_config,
                                   PCI_CONFIG_SPACE_SIZE),
        VMSTATE_BUFFER_UNSAFE_INFO_TEST(config, PCIDevice,
                                   migrate_is_pcie,
                                   0, vmstate_info_pci_config,
                                   PCIE_CONFIG_SPACE_SIZE),
        // 把 irq_state 发送过去。
        VMSTATE_BUFFER_UNSAFE_INFO(irq_state, PCIDevice, 2,
                                   vmstate_info_pci_irq_state,
                                   PCI_NUM_PINS * sizeof(int32_t)),
        VMSTATE_END_OF_LIST()
    }
};

vmstate_virtio QEMU

static const VMStateDescription vmstate_virtio = {
    .name = "virtio",
    .version_id = 1,
    .minimum_version_id = 1,
    .fields = (VMStateField[]) {
        VMSTATE_END_OF_LIST()
    },
    .subsections = (const VMStateDescription*[]) {
        // 大端还是小端
        &vmstate_virtio_device_endian,
        // VirtIODevice->guest_features
        &vmstate_virtio_64bit_features,
        // 每一个 virtqueue 的内容
        &vmstate_virtio_virtqueues,
        // 每一个 virtqueue 的 ring size
        &vmstate_virtio_ringsize,
        &vmstate_virtio_broken,
        &vmstate_virtio_extra_state,
        // VirtIODevice->started
        &vmstate_virtio_started,
        &vmstate_virtio_packed_virtqueues,
        // VirtIODevice-disabled
        &vmstate_virtio_disabled,
        NULL
    }
};