Ivshmem实现分析与性能测试
欢迎转载,转载请参见文章末尾处要求
Ivshmem实现分析
Ivshmem是虚拟机内部共享内存的pci设备。虚拟机之间实现内存共享是把内存映射成guest内的pci设备来实现的。
从代码分析和实际验证,guest与guest之间可以实现中断与非中断2种模式下的通信, host与guest之间只支持非中断模式的通信。
Ivshmem概念
PCI BARS
BAR是PCI配置空间中从0x10 到 0x24的6个register,用来定义PCI需要的配置空间大小以及配置PCI设备占用的地址空间,X86中地址空间分为MEM和IO两类,因此PCI 的BAR在bit0来表示该设备是映射到memory还是IO,bar的bit0是只读的,bit1保留位,bit2 中0表示32位地址空间,1表示64位地址空间,其余的bit用来表示设备需要占用的地址空间大小与设备起始地址。
ivshmem设备支持3个PCI基地址寄存器。BAR0是1kbyte 的MMIO区域,支持寄存器。根据计算可以得到,设备当前支持3个32bits寄存器(下文介绍),还有一个寄存器为每个guest都有,最多支持253个guest(一共256*4=1kbyte),实际默认为16。
BAR1用于MSI-X,BAR2用来从host中映射共享内存体。BAR2的大小通过命令行指定,必须是2的次方。
共享内存服务者
共享内存server是在host上运行的一个应用程序,每启动一个vm,server会指派给vm一个id号,并且将vm的id号和分配的eventfd文件描述符一起发给qemu进程。Id号在收发数据时用来标识vm,guests之间通过eventfd来通知中断。每个guest都在与自己id所绑定的eventfd上侦听,并且使用其它eventfd来向其它guest发送中断。
共享内存服务者代码在nahanni的ivshmem_server.c,后文给出分析。
Ivshmem设备寄存器
Ivshmem设备共有4种类型的寄存器,寄存器用于guest之间共享内存的同步,mask和status在pin中断下使用,msi下不使用:
/*registers for the Inter-VM shared memory device */
enumivshmem_registers {
INTRMASK = 0,
INTRSTATUS = 4,
IVPOSITION = 8,
DOORBELL = 12,
};
Mask寄存器:
与中断状态按位与,如果非0则触发一个中断。因此可以通过设置mask的第一bit为0来屏蔽中断。
Status寄存器:
当中断发生(pin中断下doorbell被设置),目前qemu驱动所实现的寄存器被设置为1。由于status代码只会设1,所以mask也只有第一个bit会有左右,笔者理解可通过修改驱动代码实现status含义的多元化。
IVPosition寄存器:
IVPosition是只读的,报告了guest id号码。Guest id是非负整数。id只会在设备就绪是被设置。如果设备没有准备好,IVPosition返回-1。应用程序必须确保他们有有效的id后才开始使用共享内存。
Doorbell寄存器:
通过写自己的doorbell寄存器可以向其它guest发送中断。为了向其它guest发送中断,需要向其它guest的doorbell写入值,doorbell寄存器为32bit,分为2个16bit域。高16bit是guest id是接收中断的guestid,低16bit是所触发的中断向量。一个指定guest id的doorbell在mmio区域的偏移等于:
guest_id * 32 + Doorbell
写入doorbell的语义取决于设备是使用msi还是pin的中断,下文介绍。
Ivshmem的中断模式
非中断模式直接把虚拟pci设备当做一个共享内存进行操作,中断模式则会操作虚拟pci的寄存器进行通信,数据的传输都会触发一次虚拟pci中断并触发中断回调,使接收方显式感知到数据的到来,而不是一直阻塞在read。
ivshmem中断模式分为Pin-based 中断和msi中断:
MSI的全称是Message Signaled Interrupt。MSI出现在PCI 2.2和PCIe的规范中,是一种内部中断信号机制。传统的中断都有专门的中断pin,当中断信号产生时,中断PIN电平产生变化(一般是拉低)。INTx就是传统的外部中断触发机制,它使用专门的通道来产生控制信息。然而PCIe并没有多根独立的中断PIN,于是使用特殊的信号来模拟中断PIN的置位和复位。MSI允许设备向一段指定的MMIO地址空间写一小段数据,然后chipset以此产生相应的中断给CPU。
从电气机械的角度,MSI减少了对interrupt pin个数的需求,增加了中断号的数量,传统的PCI中断只允许每个device拥有4个中断,并且由于这些中断都是共享的,大部分device都只有一个中断,MSI允许每个device有1,2,4,8,16甚至32个中断。
使用MSI也有一点点性能上的优势。使用传统的PIN中断,当中断到来时,程序去读内存获取数据时有可能会产生冲突。其原因device的数据主要通过DMA来传输,而在PIN中断到达时,DMA传输还未能完成,此时cpu不能获取到数据,只能空转。而MSI不会存在这个问题,因为MSI都是发生在DMA传输完成之后的。
由于具有了如上特性,ivshmem在执行pin中断时,则写入低16位的值(就是1)触发对目标guest的中断。如果使用Msi,则ivshmem设备支持多msi向量。低16bit写入值为0到Guest所支持的最大向量。低16位写入的值就是目标guest上将会触发的msi向量。Msi向量在vm启动时配置。Mis不设置status位,因此除了中断自身,所有信息都通过共享内存区域通信。由于设备支持多msi向量,这样可以使用不同的向量来表示不同的事件。这些向量的含义由用户来定。
原理与实现
数据结构
原理分析
共享内存体建立
host上Linux内核可以通过将tmpfs挂载到/dev/shm,从而通过/dev/shm来提供共享内存作为bar2映射的共享内存体。
mount tmpfs /dev/shm -t tmpfs -osize=32m
也可通过shm_open+ftruncate创建一个共享内存文件/tmp/nahanni。
eventfd字符设备建立
在中断模式下,qemu启动前会启动nahanni的ivshmem_server进程,该进程守候等待qemu的连接,待socket连接建立后,通过socket指派给每个vm一个id号(posn变量),并且将id号同eventfd文件描述符一起发给qemu进程(一个efd表示一个中断向量,msi下有多个efd)。
ivshmem_server启动方式如下:
./ivshmem_server -m 64 -p/tmp/nahanni &
其中-m所带参数为总共享内存大小单位(M),-p代表共享内存体,-n代码msi模式中断向量个数。
在qemu一端通过–chardev socket建立socket连接,并通过-deviceivshmem建立共享内存设备。
./qemu-system-x86_64 -hda mg -L /pc-bios/ --smp 4 –chardev socket,path=/tmp/nahanni,id=nahanni-device ivshmem,chardev=nahanni,size=32m,msi=off -serial telnet:0.0.0.0:4000,server,nowait,nodelay-enable-kvm&
Server端通过select侦听一个qemu上socket的连接。 Qemu端启动时需要设置-chardevsocket,path=/tmp/nahanni,id=nahanni,通过该设置qemu通过查找chardev注册类型register_types会调用qemu_chr_open_socket->unix_connect_opts,实现与server之间建立socket连接,server的add_new_guest会指派给每个vm一个id号,并且将id号同一系列eventfd文件描述符一起发给qemu进程,qemu间通过高效率的eventfd方式通信。
voidadd_new_guest(server_state_t * s) {
struct sockaddr_un remote;
socklen_t t = sizeof(remote);
long i, j;
int vm_sock;
long new_posn;
long neg1 = -1;
//等待qemu的-chardev socket,path=/tmp/nahanni,id=nahanni连接
vm_sock = accept(s->conn_socket, (structsockaddr *)&remote, &t);// qemu 启动时查找chardev注册类型register_types调用qemu_chr_open_socket->unix_connect_opts实现server建立socket连接
if ( vm_sock == -1 ) {
perror("accept");
exit(1);
}
new_posn = s->total_count;
//当new_posn==s->nr_allocated_vms代表一个新的posn的加入
if (new_posn == s->nr_allocated_vms) {
printf("increasing vmslots\n");
s->nr_allocated_vms = s->nr_allocated_vms* 2;
if (s->nr_allocated_vms < 16)
s->nr_allocated_vms = 16;
//server_state_t分配新new_posn信息结构
s->live_vms =realloc(s->live_vms,
s->nr_allocated_vms *sizeof(vmguest_t));
if (s->live_vms == NULL) {
fprintf(stderr, "reallocfailed - quitting\n");
exit(-1);
}
}
//新结构的posn
s->live_vms[new_posn].posn = new_posn;
printf("[NC] Live_vms[%ld]\n",new_posn);
s->live_vms[new_posn].efd = (int *) malloc(sizeof(int));
//分配新结构的新结构的msi_vectors的efd,对于通用pci模式msi_vectors=0
for (i = 0; i < s->msi_vectors; i++){
s->live_vms[ne