Hello DLWorld 专注

ivshmem内存共享设备

ivshmem是虚拟的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连接过程:

  1. server发送协议版本,如果客户端不支持,客户端关闭通信。
  2. server为客户端分配ID,并作为第一条消息发送给客户端。
  3. server将共享内存对象的文件描述符(fd)发给客户端。
  4. server创建一组与新客户端相关的主机事件描述符(eventfds)到已经连接的所有客户端。
  5. 最后,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返回准备读数据,之间的通信流程如下:

  1. Guest应用调用ivshmem_send->ioctl,操作/dev/ivshmem设备发起中断,通过特权指令vmalunch陷入内核。 1.内核ivshmem设备驱动kvm_ivshmem_ioctl调用write写设备的bar0的doorbell寄存器,内核发现是kvm虚拟设备,于是vmexit到qemu处理
  2. qemu调用write驱动ivshmem_io_write函数,根据传入地址,如果是写doorbell,调用event_notifier_set(&s->peers[dest].eventfds[vector]),event_notifier_set实际往目标eventfds文件写1。
  3. 接收侧qemu收到eventfd信号,调用ivshmem_receive–> ivshmem_IntrStatus_write 设置Status寄存器为1,并通过hmem_update_irq>qemu_set_irq->kvm_set_irq->kvm_vm_ioctl实现中断注入
  4. 内核的虚拟的中断控制器回调中断处理函数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,从而实现高性能。

使用

服务程序

  1. 启动主机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
  1. 在shm下生成相应设备节点
[root@test-11 ~]# ll /dev/shm/shmem1  -h
-rwxrwxr-x 1 root root 32M 2月  18 17:27 /dev/shm/shmem1

客户端测试

  1. 使用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> 
  1. 客户端基本命令
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)

虚拟机测试

  1. 配置虚拟机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。
  1. 启动虚拟机,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)
    
  2. 虚拟机加载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

中断测试

  1. 接受中断

虚拟机

[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
  1. 发送中断

虚拟机

[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

问题记录

  1. qemu-kvm: /root/rpmbuild/BUILD/qemu-kvm-2.5.0/kvm-all.c:996: kvm_irqchip_commit_routes: Assertion `ret == 0' failed.
    

参考