VIRTIO & VHOST

在虚拟化领域,virtio 随处可见。当前,virtio 可以加速 IO、network子系统。

在 IO 子系统,主要有 virtio-blk, virtio-scsi。同时,有 vhost 相关的 vhost-blk, vhost-scsi, vhost-nvme 这些。

看起来东西很多很乱,其实只要理解了本质,就可以轻松化解如此多virtio*让人困扰的问题了。

本质是什么呢?本质就是 virtio 的数据结构,以及操作。

virtio基本结构

virtio 的基本数据结构是一个 ring,这个 ring 是一段连续的内存。有了一段内存,就可以为所欲为了。

virtio 把这段内存分成3个部分,依次是 desc,avail,used。每一块是一个数组,可以顺序索引。他们的元素个数是一样,也就是是 ring 的 长度。

VIRTIO & VHOST

 

virtio & vhost数据流动

以VHOST为例,来解释一下数据是如何流动的:

  • client(qemu)创建共享内存,然后通过ioctl与内核通信,告知内核共享内存的信息,这种就是kernel作为server的vhost;或者通过Unix domain来跟其他的进程通信,这叫做vhost-user。下面以Unix domain为例。
  • Unix domain可以使用sendmsg/recvmsg来传递文件描述符,这样效率更高一些;client创建好共享内存,发送描述符到server,server直接mmap这个描述符就可以了,少了一个open的操作。
  • Client和Server可以有多段共享内存,每段之间不连续。每一段都是一个vring。
  • Client初始化好每一段共享内存vring,Server不用初始化。
  • Client发送vring的desc,avail,used这些地址给server,然后server在重新mmap之后,可以根据这个地址算出desc,avail,used这些在server用户进程的地址,然后就可以直接读写了。注意,由于数据指针是client地址,在Server处理读的时候需要转换。
  • 读写:以net为例,两个vring,一个tx,一个rx
  • 共享内存存放desc,avail,used这些信息,以及avail->idx, used->idx这些信息。
  • 当client写的时候,数据放在vring->last_avail_idx这个描述符里,注意last_avail_idx、last_used_idx这两个值,在client和server看到的是不一样的,各自维护自己的信息,作为链表头尾。添加id到avail里面,shared.avail.idx++。注意,client此处的last_avail_idx指向的是描述符的id,而不是avail数组的id,这个跟Server上的last_avail_idx的意思不一样。为什么这样做呢?
    • last_avail_idx 表示可用的数据项,可以直接拿出来用,用完之后,取当前的next;
    • 当回收的时候,也就是client在处理used的时候,可以直接插入到last_avail_idx的前面,类似链表插入到表头;
  • Server收到信号后,从自己记录的last_avail_idx开始读数据,读到avail->idx这里,区间就是[server.last_avail_idx, shared.avail.idx)。
  • Server处理每处理完一条请求,自己的 last_avail_idx ++; 同时插入 id 到 used 里面,插入的位置是 shared.used.idx,然后 shared.used.ix+ +。used.idx此时就像avail->idx一样,表示最新的used的位置。
  • Server通知client处理完数据后,client就可以回收used的描述符了,可回收的区间是[client.last_used_idx, shared.used.idx)。
  • Kickfd,callfd都是在client创建,然后通过unix domain发送给server。client通知server叫kick。

Vhost是什么

virtio offload到host叫做vhost。可以在内核态或者用户态实现。在内核态的实现主要在Linux的kernel实现。

kernel代码位置:drivers/vhost Vhost作为字符设备使用,来与qemu进行交互。跟其他的很多driver一样,利用ioctl。vhost-net 驱动会创建一个名为 /dev/vhost-net 的字符型设备,当 QEMU 通过-netdev tap,fd=,id=hostnet0,vhost=on,vhostfd=这样的参数启动时,QEMU 会打开这个设备(你可以通过 lsof -p $PID 查看,$PID 为 QEMU 的进程号)并通过 ioctl 初始化设备。

初始化过程中 vhost 驱动会创建一个内核线程名为 vhost-$PID ($PID 为 QEMU 的进程号),这个线程是 vhost 的工作线程(worker thread),工作线程会始终等待 virtqueue 触发,然后处理 virtqueue 上的 buffer,然后送到 tap 设备的文件描述符。反过来,文件描述符的 polling 也是由工作线程完成的,也就是说 vhost-net 在内核模拟了 tx、rx 队列,而并没有完成整个 virtio PIC 设备的模拟,控制平面例如在线迁移、协商等依旧由 QEMU 实现。

 

VIRTIO & VHOST

 

Vhost-user与vhost的区别

Vhost是client与kernel(server)的交互,client与内核共享内存,局限性是如果client要发送消息到用户进程,很不方便; Vhost-user使用unix domain来进行通信,两个用户进程共享内存,达到和vhost一样的效果。

Virtio-blk与virtio-scsi

他们都是在 virtio spec 里面定义的两种块设备实现。区别是 virtio-blk 是作为 pci 设备挂在 qemu 里面,所以最多只能有16块 virtio-blk 盘。 而 virtio-scsi 作为 scsi 子系统,挂在 scsi 总线上,数量上可以多得多。由于 virtio-scsi 实现了 scsi 的协议 ,所以复杂度来说要高一些。 此时,在 qemu 里面看,这块盘跟普通的 scsi 盘一样,支持 scsi 命令查询,例如 sg3_utils 提供的工具。但是 virtio-blk 盘不支持 scsi 命令。

相关代码:

Kerne
drivers/vhost/vhost.c - common vhost driver code
drivers/vhost/net.c - vhost-net driver
virt/kvm/eventfd.c - ioeventfd and irqfd

Qemu
hw/vhost.c - common vhost initialization code
hw/vhost_net.c - vhost-net initialization

# ps -ef | grep kvm1
libvirt+      3549     1  87 ?        00:22:09 qemu-system-x86_64 -enable-kvm -name kvm1 ... -netdev tap,fd=26,id=hostnet0,vhost=on,vhostfd=28 ...

可以看到,其中网络部分参数,-netdev tap,fd=26 表示的就是连接主机上的 tap 设备。

创建的 fd=26 为读写 /dev/net/tun 的文件描述符。

使用 lsof -p 3549 验证下:

# lsof -p 3549
COMMAND    PID USER   FD      TYPE             DEVICE    SIZE/OFF     NODE NAME
...
qemu-system 3549  libvirt-qemu   26u      CHR             10,200         0t107    135 /dev/net/tun
...

可以看到,PID 为 3549 的进程打开了文件 /dev/net/tun,分配的文件描述符 fd 为 26。

因此,我们可以得出以下结论:在 kvm 虚机启动时,会向内核注册 tap 虚拟网卡,同时打开设备文件 /dev/net/tun,拿到文件描述符 fd,然后将 fd 和 tap 关联,tap 就成了一端连接着用户空间的 qemu-kvm,一端连着主机上的 bridge 的端口,促使两者完成通信。

 
上一篇:多线程案例


下一篇:从内核源码到qemu调试--搭建虚拟机方法