前端 ivshmem内存共享设备 · Hello DLWorld

mcalf · February 19, 2020 · 351 hits

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.
    

参考

Related Posts

var duoshuoQuery = {short_name: 'dlworld'}; (function() { var ds = document.createElement('script'); ds.type = 'text/javascript';ds.async = true; ds.src = 'http://static.duoshuo.com/embed.js'; ds.charset = 'UTF-8'; (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(ds); })();
No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.