ivshmem内存共享设备
06 Feb 2017ivshmem是虚拟的PCI设备,在不同qemu进程间共享由主机创建的内存区域实现相互通信。
背景
几种虚拟机间或虚拟机与主机通信方式:
- 串口
- 共享内存设备
- virtio设备(vhost-user,vsocket)
virtio-serial
- 低速
- 应用场景:监控、粘贴板
IVSHMEM
通过虚拟的PCI设备,在不同qemu进程间共享由主机创建的内存区域。
优点:
- 通用内存共享框架
- 适合虚拟机之间共享
缺点:
- 缺少使用案例。目前主要在DPDK中使用,但也有应用于HPC场景的论文。
- 缺少维护。原作者最后更新时间是2012年9月,2014年由新的维护者加入qemu主干。
- 没有上层接口。
- server program
- 主机与guest之间有一次内存拷贝。
vhost-user
创建虚拟队列(virtqueue),让主机上用户空间进程之间共享。协议定义通信的两端,主和从。主是共享虚拟队列的应用,如QEMU,从使用虚拟队列。同样被DPDK使用
优点:
- 不需要hugetlbfs
- 不需要内核驱动
- 不仅共享内存
- 可直接访问guest内存,避免memcopy。
缺点:
- 仅针对网络应用
- 主要是主机与VM通信,不适合虚拟机之间共享
VSOCKET
2013由vmware贡献到社区,qemu-2.8已合并到主干。 https://lists.nongnu.org/archive/html/qemu-devel/2014-06/msg02835.html
ivshmem原理
在VM中,ivshmem PCI设备提供3个BARs
- BAR0 是1Kb 的MMIO,支持寄存器和传统中断。
- BAR1 用于MSI-X。
- BAR2 访问共享内存对象。
BAR2用于内存共享,BAR0、1通过中断可以实现额外的通信机制。
使用方法:
- 如果不需要额外的通信机制,在主机上创建共享内存,然后让QEMU进程使用。
- 如果需要通信机制,在QEME进程启动前要先启动ivshmem server,然后每个QEMU进程连接到server的unix socket。
ivshmem server
server在QEMU进程启动前在主机上运行,创建一个共享内存对象,然后等待客户端连接unix socket(默认/tmp/ivshmem_socket)。所有消息的都是int64_t 小端格式。
客户端(QEMU进程)与server连接过程:
- server发送协议版本,如果客户端不支持,客户端关闭通信。
- server为客户端分配ID,并作为第一条消息发送给客户端。
- server将共享内存对象的文件描述符(fd)发给客户端。
- server创建一组与新客户端相关的主机事件描述符(eventfds)到已经连接的所有客户端。
- 最后,server将所有客户端的事件描述符发给新的客户端。
当一个客户端断开连接时,通知所有客户端。
由于PCI设备寄存器的显示,客户端ID长度是16位,最多支持65536个客户端。
所有文件描述符(共享内存,客户端事件)用SCM_RIGHTS通过server unix socket传递。
PCI 设备寄存器
ivshmem设备在VM端有4个32位寄存器。
enum ivshmem_registers {
IntrMask = 0,
IntrStatus = 4,
IVPosition = 8,
Doorbell = 12
};
- IntrMask,中断掩码寄存器,与IntrStatus与操作后触发中断。
- IntrStatus,中断状态寄存器被置1时,表明产生中断。前两个都是传统中断寄存器。
- IVPosition,只读,报告客户ID号(guest ID)。如果设备没准备好,返回-1。
- Doorbell,逻辑上分为2个16-bit,高16位是要中断的guest ID,低16位是要触发的中断向量。该寄存器的值与设备使用的中断方式有关,如果MSI ,就使用向量,如果传统中断,设置状态寄存器。
中断
中断处理流程
Guest应用一端调用ioctl发起写数据请求,另一端ioctl返回准备读数据,之间的通信流程如下:
- Guest应用调用ivshmem_send->ioctl,操作/dev/ivshmem设备发起中断,通过特权指令vmalunch陷入内核。 1.内核ivshmem设备驱动kvm_ivshmem_ioctl调用write写设备的bar0的doorbell寄存器,内核发现是kvm虚拟设备,于是vmexit到qemu处理
- qemu调用write驱动ivshmem_io_write函数,根据传入地址,如果是写doorbell,调用event_notifier_set(&s->peers[dest].eventfds[vector]),event_notifier_set实际往目标eventfds文件写1。
- 接收侧qemu收到eventfd信号,调用ivshmem_receive–> ivshmem_IntrStatus_write 设置Status寄存器为1,并通过hmem_update_irq>qemu_set_irq->kvm_set_irq->kvm_vm_ioctl实现中断注入
- 内核的虚拟的中断控制器回调中断处理函数kvm_ivshmem_interrupt,检查bar0寄存器的值,根据相应含义中断处理,触发接收端所阻塞的kvm_ivshmem_ioctl 返回应用。
MSI中断
MSI(Message Signaled Interrupt)允许设备向一段指定的MMIO地址空间写数据,然后产生相应的中断给cpu。 ivshmem设备可以支持多个MSI向量,向量数在虚拟机启动时设置。MSI只是一个信号,不设置状态。除了中断以外的其它信息都应该通过共享内存交互。支持多MSI向量的设备可以用不同的向量表示不同的事件。中断向量的语义由用户定义。
流控
考虑到大数据量的数据传输,ivshmem开辟内存可能不够一次性传输的情况,需要考虑收发同步的流控问题。实现可参考tcp的实现方式,将ivshmem的共享内存分成若干(比如16个)区域,内存第一个块用作流控,具体来说: 在收发端使用full和empty参数来表示后面15个内存块的读写情况,empty表示有多少空余块可以写,当empty等于0则暂停写操作,full表示有多少数据块可以读,当full==0表示无数据可读。同时各有一把pthread_spinlock_t锁对这两个变量进行保护,从而实现流量控制。
#defineCHUNK_SZ (1024)
#defineNEXT(i) ((i + 1) % 15)
#defineOFFSET(i) (i * CHUNK_SZ)
#defineFLOCK_LOC memptr
#defineFULL_LOC FLOCK_LOC +sizeof(pthread_spinlock_t)
#defineELOCK_LOC FULL_LOC + sizeof(int)
#defineEMPTY_LOC ELOCK_LOC + sizeof(pthread_spinlock_t)
#defineBUF_LOC (memptr + CHUNK_SZ)
MEMNIC PMD
PMD(Poll-Mode Driver)是DPDK的扩展,允许创建基于共享内存的虚拟NICs。在客户端,使用ivshmem虚拟PCI设备。在服务端,共享内存是一个映射的文件。虚拟NIC通过在主机与VMs之间直接使用共享缓冲区,避免hypercalls,从而实现高性能。
使用
服务程序
- 启动主机server程序
[root@test-11 ~]# ivshmem-server -v -F -S /tmp/ivshmem_socket1 -l 32M -n 32
Using POSIX shared memory: ivshmem
create & bind socket /tmp/ivshmem_socket1
accept()=5
new peer id = 0
peer->sock_fd=5
- 在shm下生成相应设备节点
[root@test-11 ~]# ll /dev/shm/shmem1 -h
-rwxrwxr-x 1 root root 32M 2月 18 17:27 /dev/shm/shmem1
客户端测试
- 使用ivshmem-client客户端测试
[root@test-11 host]# ivshmem-client -v -S /tmp/ivshmem_socket1
dump: dump peers (including us)
int <peer> <vector>: notify one vector on a peer
int <peer> all: notify all vectors of a peer
int all: notify all vectors of all peers (excepting us)
cmd> connect to client /tmp/ivshmem_socket1
our_id=1
shm_fd=4
listen on server socket 3
new peer id = 0
new vector 0 (fd=5) for peer id 0
new vector 0 (fd=6) for peer id 1
cmd>
- 客户端基本命令
cmd> help
dump: dump peers (including us)
int <peer> <vector>: notify one vector on a peer
int <peer> all: notify all vectors of a peer
int all: notify all vectors of all peers (excepting us)
虚拟机测试
- 配置虚拟机XML,使用shmem设备
<shmem name='shmem0'>
<server path='/tmp/socket-ivshmem0'/>
<size unit='M'>32</size>
<msi vectors='32' ioeventfd='on'/>
</shmem>
- vectors可以为1、2、4、8、16和32。
- 启动虚拟机,server程序检测到连接
[NC] new connection increasing vm slots [NC] Live_vms[0] efd[0] = 6 [NC] trying to send fds to new connection [NC] Connected (count = 0). Live_count is 1 vm_sockets (1) = [5|6] Waiting (maxfd = 5)
- 虚拟机加载ivshmem驱动
yum install gcc gcc-c++ kernel-devel cmake openssl-devel -y
[root@node201 coyote]# cmake .
[root@node201 coyote]# make
[root@node201 uio]# modprobe uio
[root@node201 uio]# insmod uio_ivshmem.ko
中断测试
- 接受中断
虚拟机
[root@node201 VM]# ./uio_read /dev/uio0 1
[UIO] opening file /dev/uio0
[UIO] reading
[UIO] buf is 2
主机
cmd> int 2 0
notify peer 2 on vector 0, fd 7
- 发送中断
虚拟机
[root@node201 VM]# ./uio_send /dev/uio0 1 1 2
[UIO] opening file /dev/uio0
[UIO] count is 1
[UIO] writing 131073
[UIO] ping #0
[UIO] Exiting...
主机
cmd> received event on fd 38 vector 1: 1
receive notification from peer_id=2 vector=1
问题记录
-
qemu-kvm: /root/rpmbuild/BUILD/qemu-kvm-2.5.0/kvm-all.c:996: kvm_irqchip_commit_routes: Assertion `ret == 0' failed.