ivshmem(Inter-VM shared memory device)是QEMU提供的一种宿主机与虚拟机之间或多个虚拟机之间共享内存的特殊设备。这种设备在虚拟机内部表现为PCI设备,共享的内存区域则以 PCI BAR 的形式存在。最近项目需要基于 ivshmem 实现一个虚拟机之间的通信,这里记录一下自己从零学习的过程。这篇文章我们先从上层角度看看在虚拟机内部如何识别和使用 ivshmem。
概述 ivshmem 被设计为在运行不同 guest 的多个QEMU进程和 host 之间共享内存区域。为了让所有 guest 都能够获取共享内存区域,QEMU将其建模为PCI设备,将所述内存作为 PCI BAR 公开给 guest。
ivshmem 有两种形式:
ivshmem-plain
: 简单的共享内存区域
ivshmem-doorbell
: 除了共享内存,还能提供基于中断的通信机制
这种设备在虚拟机内部表现为 PCI
设备,共享的内存区域则以 PCI BAR
的形式存在。ivshmem
PCI设备提供3个 BAR
:
BAR0
: 设备寄存器
BAR1
: MSI-X
表
BAR2
: 共享内存区域
简单共享内存的场景只使用 BAR2
就足够了。如果需要基于中断实现额外通信,需要用到BAR0
和BAR1
。这可能需要编写内核驱动在虚拟机内处理中断,宿主机上QEMU
进程在启动前需要先启动 ivshmem server
, 然后让 QEMU
进程连接到 server
的 unix socket
。
添加 ivshmem 设备 本文目前只讨论 ivshmem-plain
模式。宿主机上添加 ivshmem
设备后,虚拟机应用如何找到相应的 ivshmem
设备呢?
Linux的 /sys/bus/pci/devices/
目录会列出所有的PCI设备, ivshmem
设备也会包含在其中。PCI设备都存在 vendor
号和 device
两个标识,vendor
表示厂商,device
表示厂商内的设备类型。ivshmem
设备的 vendor
号为 0x1af4
, device
号为 0x1110
,PCI设备的 vendor
和 device
号可在这里 进行查询。
虚拟机中应用可通过遍历该目录下的具体设备,通过读取 vendor
和 device
文件来识别 ivshmem
设备。
但如果有两种应用都需要使用一个独立的 ivshmem
设备,虚拟机应用如何识别出应该使用哪个 ivshmem
设备呢?
因为每个 PCI 设备都可以由 BDF:(Bus, Device, Function)
来唯一标识,简单做法可以为每个应用预留好固定 BDF
地址。 BDF
地址中,BUS
占用 8
位,Device
占用 5
位, Function
占用 3
位。比如,预留总线 pci0
的最后两个设备地址 0000:00:1e.0
和 0000:00:1f.0
。
有时候无法预留,不同虚拟机上的 ivshmem
地址可能不同。这种情况可以通过与宿主机上的应用约定好相应的固定内容做为 signature
写入共享内存头部,虚拟机应用读取共享内存头部的 signature
信息来识别相应设备。
我们使用可以 QEMU
的监控机制动态添加 ivshmem
设备。
首先,识别虚拟机当前的PCI设备:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 $ virsh qemu-monitor-command --hmp 4 info pci Bus 0, device 0, function 0: Host bridge: PCI device 8086:1237 id "" Bus 0, device 1, function 0: ISA bridge: PCI device 8086:7000 id "" Bus 0, device 1, function 1: IDE controller: PCI device 8086:7010 BAR4: I/O at 0xc0a0 [0xc0af]. id "" Bus 0, device 1, function 2: USB controller: PCI device 8086:7020 IRQ 11. BAR4: I/O at 0xc040 [0xc05f]. id "usb" Bus 0, device 1, function 3: Bridge: PCI device 8086:7113 IRQ 9. id "" Bus 0, device 2, function 0: VGA controller: PCI device 1013:00b8 BAR0: 32 bit prefetchable memory at 0xfc000000 [0xfdffffff]. BAR1: 32 bit memory at 0xfebd0000 [0xfebd0fff]. BAR6: 32 bit memory at 0xffffffffffffffff [0x0000fffe]. id "video0" Bus 0, device 3, function 0: Ethernet controller: PCI device 1af4:1000 IRQ 11. BAR0: I/O at 0xc060 [0xc07f]. BAR1: 32 bit memory at 0xfebd1000 [0xfebd1fff]. BAR4: 64 bit prefetchable memory at 0xfe000000 [0xfe003fff]. BAR6: 32 bit memory at 0xffffffffffffffff [0x0003fffe]. id "net0" Bus 0, device 4, function 0: SCSI controller: PCI device 1af4:1001 IRQ 11. BAR0: I/O at 0xc000 [0xc03f]. BAR1: 32 bit memory at 0xfebd2000 [0xfebd2fff]. BAR4: 64 bit prefetchable memory at 0xfe004000 [0xfe007fff]. id "virtio-disk0" Bus 0, device 5, function 0: Class 0255: PCI device 1af4:1002 IRQ 10. BAR0: I/O at 0xc080 [0xc09f]. BAR4: 64 bit prefetchable memory at 0xfe008000 [0xfe00bfff]. id "balloon0"
PCI地址使用到了 0000:00:05.0
, 我们使用未使用的PCI地址添加两个 ivshmem
设备, 0000:00:10.0
大小为 16M
, 0000:00:11.0
大小为 8M
:
1 2 3 4 $ virsh qemu-monitor-command --hmp 4 "object_add memory-backend-file,size=16M,share,mem-path=/dev/shm/shm1,id=shm1" $ virsh qemu-monitor-command --hmp 4 "device_add ivshmem-plain,memdev=shm1,bus=pci.0,addr=0x10,master=on" $ virsh qemu-monitor-command --hmp 4 "object_add memory-backend-file,size=8M,share,mem-path=/dev/shm/shm2,id=shm2" $ virsh qemu-monitor-command --hmp 4 "device_add ivshmem-plain,memdev=shm2,bus=pci.0,addr=0x11,master=on"
登录到虚拟机上查看PCI设备,可以看到:
1 2 3 4 5 6 7 8 9 10 11 12 [root@host-172-16-0-29 ~]# lspci 00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02) 00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II] 00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II] 00:01.2 USB controller: Intel Corporation 82371SB PIIX3 USB [Natoma/Triton II] (rev 01) 00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03) 00:02.0 VGA compatible controller: Cirrus Logic GD 5446 00:03.0 Ethernet controller: Red Hat, Inc. Virtio network device 00:04.0 SCSI storage controller: Red Hat, Inc. Virtio block device 00:05.0 Unclassified device [00ff]: Red Hat, Inc. Virtio memory balloon 00:10.0 RAM memory: Red Hat, Inc. Inter-VM shared memory (rev 01) 00:11.0 RAM memory: Red Hat, Inc. Inter-VM shared memory (rev 01)
查看目录/sys/bus/pci/devices/
, 也可以看这些设备:
1 2 3 4 5 6 7 8 9 10 11 12 13 $ ls -l /sys/bus/pci/devices/total 0 lrwxrwxrwx. 1 root root 0 Feb 28 08:39 0000:00:00.0 -> ../../../devices/pci0000:00/0000:00:00.0 lrwxrwxrwx. 1 root root 0 Feb 28 08:39 0000:00:01.0 -> ../../../devices/pci0000:00/0000:00:01.0 lrwxrwxrwx. 1 root root 0 Feb 28 08:39 0000:00:01.1 -> ../../../devices/pci0000:00/0000:00:01.1 lrwxrwxrwx. 1 root root 0 Feb 28 08:39 0000:00:01.2 -> ../../../devices/pci0000:00/0000:00:01.2 lrwxrwxrwx. 1 root root 0 Feb 28 08:39 0000:00:01.3 -> ../../../devices/pci0000:00/0000:00:01.3 lrwxrwxrwx. 1 root root 0 Feb 28 08:39 0000:00:02.0 -> ../../../devices/pci0000:00/0000:00:02.0 lrwxrwxrwx. 1 root root 0 Feb 28 08:39 0000:00:03.0 -> ../../../devices/pci0000:00/0000:00:03.0 lrwxrwxrwx. 1 root root 0 Feb 28 08:39 0000:00:04.0 -> ../../../devices/pci0000:00/0000:00:04.0 lrwxrwxrwx. 1 root root 0 Feb 28 08:39 0000:00:05.0 -> ../../../devices/pci0000:00/0000:00:05.0 lrwxrwxrwx. 1 root root 0 Feb 28 08:47 0000:00:10.0 -> ../../../devices/pci0000:00/0000:00:10.0 lrwxrwxrwx. 1 root root 0 Feb 28 08:47 0000:00:11.0 -> ../../../devices/pci0000:00/0000:00:11.0
分别查看两个 ivshmem
设备目录下 vendor
和 device
文件的内容,可以看到 vendor
都是 0x1af4
, device
都是 0x1110
:
1 2 3 4 5 6 7 8 $ cat /sys/bus/pci/devices/0000\:00\:10.0/vendor 0x1af4 $ cat /sys/bus/pci/devices/0000\:00\:10.0/device 0x1110 $ cat /sys/bus/pci/devices/0000\:00\:11.0/vendor 0x1af4 $ cat /sys/bus/pci/devices/0000\:00\:11.0/device 0x1110
使用 ivshmem 设备 ivshmem
设备的共享内存以设备目录下的resource2
文件存在,虚拟机应用可以通过mmap
调用来读写该内存区域。查看两个ivshmem
共享内存的大小,可以看到0000:00:10.0
的大小为16M
, 0000:00:11.0
的大小为8M
:
1 2 3 4 $ ls -l /sys/bus/pci/devices/0000\:00\:10.0/resource2 -rw-------. 1 root root 16777216 2月 12 09:23 /sys/bus/pci/devices/0000:00:10.0/resource2 $ ls -l /sys/bus/pci/devices/0000\:00\:11.0/resource2 -rw-------. 1 root root 8388608 2月 12 11:11 /sys/bus/pci/devices/0000:00:11.0/resource2
在宿主机上分别在两个共享内存区域中写入 8
字节(含字符串换行符)的不同标识信息:
1 2 echo "1234567" > /dev/shm/shm1 echo "7654321" > /dev/shm/shm2
在虚拟机上编写一个简单的程序来读出第一个ivshmem
设备的前8
字节, C
语言代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <assert.h> #define SHM_SIZE (16 * 1024 * 1024) int main (int argc, char **argv) { char *p; int fd; int i; fd = open("/sys/bus/pci/devices/0000:00:10.0/resource2" , O_RDWR); assert(fd != -1 ); p = mmap(0 , SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ); assert(p != NULL ); for (i = 0 ; i < 8 ; i++) { printf ("%c" , p[i]); } munmap(p, SHM_SIZE); close(fd); return 0 ; }
在虚拟机上编译执行, 可以看到宿主机上写入的标识信息。
1 2 3 [root@host-172-16-0-29 ~]# gcc test.c [root@host-172-16-0-29 ~]# ./a.out 1234567
在真实的生产应用中,对于共享内存的使用不会这么简单,而是要构造相当复杂的数据结构。比如,可以在共享内存中构造基于偏移量的环形队列结构,用于双向的信息发送。
基于中断方式的通信方式,后续再专门文章来介绍。