ivshmem(Nahanni)实现分析与性能测试
Ivshmem实现分析与性能测试欢迎转载,转载请参见文章末尾处要求Ivshmem实现分析Ivshmem是虚拟机内部共享内存的pci设备。虚拟机之间实现内存共享是把内存映射成guest内的pci设备来实现的。从代码分析和实际验证,guest与guest之间可以实现中断与非中断2种模式下的通信, host与guest之间只支持非中断模式的通信。Ivshmem概念
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[new_posn].efd[i] =eventfd(0, 0);
printf("\tefd[%ld] = %d\n",i, s->live_vms[new_posn].efd[i]);
}
s->live_vms[new_posn].sockfd = vm_sock;
s->live_vms[new_posn].alive = 1;
//将new_posn和efd等发送给新的vm_sock
sendPosition(vm_sock, new_posn);
sendUpdate(vm_sock, neg1, sizeof(long),s->shm_fd);
printf("[NC] trying to send fds to newconnection\n");
sendRights(vm_sock, new_posn,sizeof(new_posn), s->live_vms, s->msi_vectors);
printf("[NC] Connected (count =%ld).\n", new_posn);
//将new_posn的加入信息告知老的vm_sock
for (i = 0; i < new_posn; i++) {
if (s->live_vms[i].alive) {
// ping all clients that a newclient has joined
printf("[UD] sending fd[%ld]to %ld\n", new_posn, i);
for (j = 0; j msi_vectors; j++) {
printf("\tefd[%ld] =[%d]", j, s->live_vms[new_posn].efd[j]);
sendUpdate(s->live_vms[i].sockfd, new_posn,
sizeof(new_posn),s->live_vms[new_posn].efd[j]);
}
printf("\n");
}
}
//total_count保存连接总次数
s->total_count++;
}
Qemu侧Pci设备ivshmem初始化时通过ivshmem_read接收server端发来的posn和efd信息,在通过create_eventfd_chr_device创建eventfd字符设备。
staticvoid ivshmem_read(void *opaque, const uint8_t * buf, int flags)
{
… //相关参数检查
/* if the position is -1, then it's sharedmemory region fd */
//-1表示共享内存的fd
if (incoming_posn == -1) {
void * map_ptr;
s->max_peer = 0;
if (check_shm_size(s, incoming_fd) ==-1) {
exit(-1);
}
/* mmap the region and map into theBAR2 */
map_ptr = mmap(0, s->ivshmem_size,PROT_READ|PROT_WRITE, MAP_SHARED,
incoming_fd, 0);
//创建 MemoryRegions结构
memory_region_init_ram_ptr(&s->ivshmem, OBJECT(s),
"ivshmem.bar2", s->ivshmem_size, map_ptr);
vmstate_register_ram(&s->ivshmem, DEVICE(s));
IVSHMEM_DPRINTF("guest h/w addr =%" PRIu64 ", size = %" PRIu64 "\n",
s->ivshmem_attr,s->ivshmem_size);
//添加到guestos的地址空间
memory_region_add_subregion(&s->bar,0, &s->ivshmem);
/* only store the fd if it issuccessfully mapped */
s->shm_fd = incoming_fd;
return;
}
/* each guest has an array of eventfds, andwe keep track of how many
* guests for each VM */
guest_max_eventfd =s->peers[incoming_posn].nb_eventfds;
if (guest_max_eventfd == 0) {
/* one eventfd per MSI vector */
s->peers[incoming_posn].eventfds =g_new(EventNotifier, s->vectors);
}
/* this is an eventfd for a particularguest VM */
IVSHMEM_DPRINTF("eventfds[%ld][%d] =%d\n", incoming_posn,
guest_max_eventfd, incoming_fd);
//初始化eventfd
event_notifier_init_fd(&s->peers[incoming_posn].eventfds[guest_max_eventfd],
incoming_fd);
/* increment count for particular guest */
s->peers[incoming_posn].nb_eventfds++;
/* keep track of the maximum VM ID */
if (incoming_posn > s->max_peer) {
s->max_peer = incoming_posn;
}
if (incoming_posn == s->vm_id) {
//接收端创建eventfd的字符设备
s->eventfd_chr[guest_max_eventfd] = create_eventfd_chr_device(s,
&s->peers[s->vm_id].eventfds[guest_max_eventfd],
guest_max_eventfd);
}
if (ivshmem_has_feature(s,IVSHMEM_IOEVENTFD)) {
ivshmem_add_eventfd(s, incoming_posn,guest_max_eventfd);
}
}
create_eventfd_chr_device创建并初始化efd接收设备:
staticCharDriverState* create_eventfd_chr_device(void * opaque, EventNotifier *n,
int vector)
{
if (ivshmem_has_feature(s, IVSHMEM_MSI)) {
s->eventfd_table[vector].pdev =PCI_DEVICE(s);
s->eventfd_table[vector].vector =vector;
//msi字符设备注册
qemu_chr_add_handlers(chr,ivshmem_can_receive, fake_irqfd,
ivshmem_event,&s->eventfd_table[vector]);
} else {
//pin字符设备注册
qemu_chr_add_handlers(chr, ivshmem_can_receive, ivshmem_receive,
ivshmem_event, s);
}
Eventfd建立流程图如下:
Qemu实现ivshmem虚拟设备及内存映射
在非中断版本中,无需通过–chardevsocket建立连接,但同样需要支持-device ivshmem建立共享内存:
./qemu-system-x86_64-dyn -hda Img -L /pc-bios/ --smp 4 -device ivshmem,shm=nahanni,size=32m -serial telnet:0.0.0.0:4001,server,nowait,nodelay&
其中-device ivshmem后的参数通过ivshmem_properties被传递给了IVShmemState结构:
staticProperty ivshmem_properties[] = {//传入IVShmemState参数
DEFINE_PROP_CHR("chardev",IVShmemState, server_chr),
DEFINE_PROP_STRING("size", IVShmemState,sizearg),
DEFINE_PROP_UINT32("vectors",IVShmemState, vectors, 1),
DEFINE_PROP_BIT("ioeventfd",IVShmemState, features, IVSHMEM_IOEVENTFD, false),
DEFINE_PROP_BIT("msi",IVShmemState, features, IVSHMEM_MSI, true),
DEFINE_PROP_STRING("shm",IVShmemState, shmobj),///dev/shm/nahanni
DEFINE_PROP_STRING("role",IVShmemState, role),
DEFINE_PROP_END_OF_LIST(),
};
IVShmemState是用来保存ivshmem状态的结构,用作pci设备创建:
typedefstruct IVShmemState {
PCIDevice dev;
uint32_t intrmask;
uint32_t intrstatus;
uint32_t doorbell;
CharDriverState **eventfd_chr; //保存efd字符设备
CharDriverState *server_chr;//保存server字符设备
MemoryRegion ivshmem_mmio;//bar0映射的内存区,MemoryRegion结构为qemu分配内存使用
MemoryRegion bar;//在guest没有分配实际内存空间之前使用
MemoryRegionivshmem;//bar2的实际内存区
uint64_tivshmem_size; //共享内存区大小
int shm_fd; /* shared memory filedescriptor 共享内存文件描述符*/
Peer *peers;//对应guest数组的指针,保存其中断向量
int nb_peers;// 预备多少个guest空间,默认16
int max_peer; //最大支持的空间数
int vm_id;//vm_id<max_peer <="" p="" style="word-wrap: break-word;">
uint32_t vectors;//msi注册的中断向量数
uint32_t features;//包含IVSHMEM_MSI、IVSHMEM_IOEVENTFD等特性
EventfdEntry *eventfd_table;// eventfd表
Error *migration_blocker;
char * shmobj;//nahanni
char * sizearg;//共享体大小含单位
char * role;
int role_val; /* scalar to avoid multiple stringcomparisons */
}IVShmemState;
Qemu通过ivshmem设备类型的注册:ivshmem_class_init->pci_ivshmem_init
pci_ivshmem_init对传入参数进行合法性检查并申请并初始化IVShmemState结构
staticint pci_ivshmem_init(PCIDevice *dev)
{
IVShmemState *s = DO_UPCAST(IVShmemState,dev, dev);
uint8_t *pci_conf;
//一些参数合法性检查
if (s->sizearg == NULL)
s->ivshmem_size = 4 << 20; /*4 MB default *//*如果没有指定大小*/
else {
s->ivshmem_size =ivshmem_get_size(s);//读出sizearg连同单位,写入IVShmemState
}
//注册可迁移虚拟pci设备ivshmem
register_savevm(&s->dev.qdev,"ivshmem", 0, 0, ivshmem_save, ivshmem_load, dev);
/* IRQFD requires MSI */
if (ivshmem_has_feature(s,IVSHMEM_IOEVENTFD) &&
!ivshmem_has_feature(s, IVSHMEM_MSI)){//IVSHMEM_IOEVENTFD需要与IVSHMEM_MSI同时配置
fprintf(stderr, "ivshmem:ioeventfd/irqfd requires MSI\n");
exit(1);
}
/* check that role is reasonable */
if (s->role) {
if (strncmp(s->role,"peer", 5) == 0) {
s->role_val = IVSHMEM_PEER;
} else if (strncmp(s->role,"master", 7) == 0) {
s->role_val = IVSHMEM_MASTER;
} else {
fprintf(stderr, "ivshmem:'role' must be 'peer' or 'master'\n");
exit(1);
}
} else {
s->role_val = IVSHMEM_MASTER; /*default */
}
if (s->role_val == IVSHMEM_PEER) {
error_set(&s->migration_blocker,QERR_DEVICE_FEATURE_BLOCKS_MIGRATION,
"peer mode","ivshmem");
migrate_add_blocker(s->migration_blocker);
}
pci_conf = s->dev.config;
pci_conf[PCI_COMMAND] = PCI_COMMAND_IO |PCI_COMMAND_MEMORY;
pci_config_set_interrupt_pin(pci_conf, 1);
s->shm_fd = 0;
/*BAR0的MemoryRegion结构初始化,qemu对共享内存区的读写就由ivshmem_mmio_ops实现*/
memory_region_init_io(&s->ivshmem_mmio, &ivshmem_mmio_ops, s,
"ivshmem-mmio", IVSHMEM_REG_BAR_SIZE);
/* region for registers*/
/*pci的BAR0地址的映射,第二参数表示bar0*/
pci_register_bar(&s->dev, 0,PCI_BASE_ADDRESS_SPACE_MEMORY,
&s->ivshmem_mmio);
/*BAR2的MemoryRegion结构初始化*/
memory_region_init(&s->bar,"ivshmem-bar2-container", s->ivshmem_size);
……..
if((s->server_chr != NULL) && (strncmp(s->server_chr->filename, "unix:",5) == 0)) {
//中断模式
pci_register_bar(dev, 2,s->ivshmem_attr, &s->bar); /*pci的BAR2地址的映射*/
s->eventfd_chr =g_malloc0(s->vectors * sizeof(CharDriverState *));
qemu_chr_add_handlers(s->server_chr,ivshmem_can_receive, ivshmem_read,
ivshmem_event, s);
//非中断模式
int fd;
if (s->shmobj == NULL) {
fprintf(stderr, "Must specify'chardev' or 'shm' to ivshmem\n");
}
IVSHMEM_DPRINTF("using shm_open(shm object = %s)\n", s->shmobj);
//获取共享内存体文件句柄
if ((fd = shm_open(s->shmobj,O_CREAT|O_RDWR|O_EXCL,
S_IRWXU|S_IRWXG|S_IRWXO)) > 0) {
/* truncate file to length PCIdevice's memory */
if (ftruncate(fd, s->ivshmem_size)!= 0) {
fprintf(stderr, "ivshmem:could not truncate shared file\n");
}
} else if ((fd = shm_open(s->shmobj,O_CREAT|O_RDWR,
S_IRWXU|S_IRWXG|S_IRWXO)) < 0) {
fprintf(stderr, "ivshmem:could not open shared file\n");
exit(-1);
}
//检查
if (check_shm_size(s, fd) == -1) {
exit(-1);
}
/*创建并映射pci共享内存bar2*/
create_shared_memory_BAR(s, fd);
}
ivshmem_mmio_ops所注册了bar0读写函数,其中对doorbell的写操作ivshmem_io_write-> event_notifier_set转换为对eventfd的写操作:
intevent_notifier_set(EventNotifier *e)
{
… …
ret = write(e->wfd, &value,sizeof(value));
} while (ret < 0 && errno ==EINTR);
create_shared_memory_BAR也是共享内存的核心函数,对于非中断模式只需将bar2映射进guestos地址空间,即完成了内存共享:
staticvoid create_shared_memory_BAR(IVShmemState *s, int fd) {
void * ptr;
s->shm_fd = fd;
ptr = mmap(0, s->ivshmem_size,PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//创建 Memory Regions结构
memory_region_init_ram_ptr(&s->ivshmem, "ivshmem.bar2",
s->ivshmem_size, ptr);
vmstate_register_ram(&s->ivshmem,&s->dev.qdev);
//添加到guestos的地址空间
memory_region_add_subregion(&s->bar,0, &s->ivshmem);
//完成地址空间向bar寄存器的注册
pci_register_bar(&s->dev, 2,PCI_BASE_ADDRESS_SPACE_MEMORY, &s->bar);
}
每个MemoryRegions结构通过构造函数memory_region_init*()来创建,并通过析构函数memory_region_destrory()来销毁,然后,通过memory_region_add_subregion()将其添加到guestos的地址空间中,并通过memory_region_del_subregion()从地址空间中删除,另外,每个MR的属性在任何地方都可以被改变。之前说过pci的bar寄存器用来记录设备需要占用的地址空间大小与设备起始地址,pci_register_bar最后是将该地址写入bar寄存器。
至此在qemu完成了模拟一个pci设备并实现内存映射。
内核中Ivshmem设备的注册和初始化下一步在guest os上通过自己写一个模块,用来驱动这个pci设备,首先注册kvm_ivshmem这个字符设备,得到主设备号。
register_chrdev(0, "kvm_ivshmem", &kvm_ivshmem_ops);
并且实现该pci字符设备的文件操作:
staticconst struct file_operations kvm_ivshmem_ops = {
.owner = THIS_MODULE,
.open =kvm_ivshmem_open,
.mmap =kvm_ivshmem_mmap,
.read =kvm_ivshmem_read,
.ioctl = kvm_ivshmem_ioctl,
.write = kvm_ivshmem_write,
.llseek = kvm_ivshmem_lseek,
.release = kvm_ivshmem_release,
};
其中以设备写操作为例:
staticssize_t kvm_ivshmem_write(struct file * filp, const char * buffer,
size_tlen, loff_t * poffset)
{
int bytes_written = 0;
unsigned long offset;
offset = *poffset;
printk(KERN_INFO "KVM_IVSHMEM:trying to write\n");
//下文会介绍,指的bar2映射到线性地址空间,ioctl中可以实现对kvm_ivshmem_dev.regs的操作,即对bar0的操作。
if (!kvm_ivshmem_dev.base_addr) {
printk(KERN_ERR"KVM_IVSHMEM: cannot write to ioaddr (NULL)\n");
return 0;
}
if (len >kvm_ivshmem_dev.ioaddr_size - offset) {
len =kvm_ivshmem_dev.ioaddr_size - offset;
}
printk(KERN_INFO "KVM_IVSHMEM:len is %u\n", (unsigned) len);
if (len == 0) return 0;
//将数据拷贝至kvm_ivshmem_dev.base_addr+offset地址
bytes_written =copy_from_user(kvm_ivshmem_dev.base_addr+offset,
buffer,len);
if (bytes_written > 0) {
return -EFAULT;
}
printk(KERN_INFO "KVM_IVSHMEM:wrote %u bytes at offset %lu\n", (unsigned) len, offset);
*poffset += len;
return len;
}
内核模块注册kvm_ivshmem这个字符设备之后,通过pci_register_driver(&kvm_ivshmem_pci_driver)实现字符型pci设备注册,随后由pci_driver数据结构中的probe函数指针所指向的侦测函数来初始化该PCI设备:
staticint kvm_ivshmem_probe_device (struct pci_dev *pdev,
conststruct pci_device_id * ent) {
int result;
printk("KVM_IVSHMEM: Probing forKVM_IVSHMEM Device\n");//插入模块时,探测pci设备,pci设备在启动qemu时已存在
result = pci_enable_device(pdev);//初始化设备,唤醒设备
if (result) {
printk(KERN_ERR "Cannotprobe KVM_IVSHMEM device %s: error %d\n",
pci_name(pdev), result);
return result;
}
result =pci_request_regions(pdev, "kvm_ivshmem");//根据pci_register_bar注册的地址空间申请内存空间,配置ivshmem的memory资源
if (result < 0) {
printk(KERN_ERR"KVM_IVSHMEM: cannot request regions\n");
goto pci_disable;
} else printk(KERN_ERR"KVM_IVSHMEM: result is %d\n", result);
//bar2的映射
kvm_ivshmem_dev.ioaddr= pci_resource_start(pdev, 2);// bar2的映射内存的启始地址
kvm_ivshmem_dev.ioaddr_size= pci_resource_len(pdev, 2);// bar2的映射内存的大小
kvm_ivshmem_dev.base_addr= pci_iomap(pdev, 2, 0);//将bar2映射到线性地址空间
printk(KERN_INFO "KVM_IVSHMEM:iomap base = 0x%lu \n",
(unsignedlong) kvm_ivshmem_dev.base_addr);
if (!kvm_ivshmem_dev.base_addr) {
printk(KERN_ERR"KVM_IVSHMEM: cannot iomap region of size %d\n",
kvm_ivshmem_dev.ioaddr_size);
goto pci_release;
}
printk(KERN_INFO "KVM_IVSHMEM:ioaddr = %x ioaddr_size = %d\n",
kvm_ivshmem_dev.ioaddr,kvm_ivshmem_dev.ioaddr_size);
//bar0的映射
kvm_ivshmem_dev.regaddr= pci_resource_start(pdev, 0);// bar0的映射内存的启始地址
kvm_ivshmem_dev.reg_size= pci_resource_len(pdev, 0);// bar0的映射内存的大小
kvm_ivshmem_dev.regs= pci_iomap(pdev, 0, 0x100);//将bar0映射到线性地址空间
kvm_ivshmem_dev.dev= pdev;
if (!kvm_ivshmem_dev.regs) {
printk(KERN_ERR"KVM_IVSHMEM: cannot ioremap registers of size %d\n",
kvm_ivshmem_dev.reg_size);
goto reg_release;
}
/*设置IntrMask使能*/
writel(0xffffffff,kvm_ivshmem_dev.regs + IntrMask);
/* by default initialize semaphore to0 */
sema_init(&sema, 0);
init_waitqueue_head(&wait_queue);
event_num = 0;
//对于msix模式根据所注册向量nvectors数,分别注册中断回调kvm_ivshmem_interrupt
if(request_msix_vectors(&kvm_ivshmem_dev, 4) != 0) {
printk(KERN_INFO"regular IRQs\n");
//对于普通中断模式注册中断回调kvm_ivshmem_interrupt
if (request_irq(pdev->irq,kvm_ivshmem_interrupt, IRQF_SHARED,
"kvm_ivshmem",&kvm_ivshmem_dev)) {
printk(KERN_ERR"KVM_IVSHMEM: cannot get interrupt %d\n", pdev->irq);
printk(KERN_INFO"KVM_IVSHMEM: irq = %u regaddr = %x reg_size = %d\n",
pdev->irq,kvm_ivshmem_dev.regaddr, kvm_ivshmem_dev.reg_size);
}
} else {
printk(KERN_INFO "MSI-Xenabled\n");
}
return 0;
至此完成了pci设备的注册和初始化,重点步骤的流程如下图所示:
至此可以查询到pci设备
cat /proc/devices | grep kvm_ivshmem
然后创建设备节点:
mknod -m=666 /dev/ivshmem c 245 0
中断处理流程
Guest应用一端调用ioctl发起写数据请求,另一端ioctl返回准备读数据,之间的通信流程如下:
一、 Guest应用调用ivshmem_send->ioctl,操作/dev/ivshmem设备发起中断,通过特权指令vmalunch陷入内核。
二、 内核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 返回应用。
调测与性能
非中断模式调测
针对ivshmem的分析和调测资料有限,故将调测通过的方法也记录下来:
Host os:
./qemu-system-x86_64 -hda img -L /pc-bios/ --smp 2 -device ivshmem,shm=nahanni,size=32m -serial telnet:0.0.0.0:4001,server,nowait,nodelay-enable-kvm&
Guest os:
insmod kvm_ivshmem.ko
Guest os:
mknod -m=666 /dev/ivshmem b 245 0
host os:
./grablocknahanni
Guest os:
./guestlock /dev/ivshmem
以上是guestos与hostos之间通信,对于guestos与guestos之间通信原理一致。
以上操作完毕,应用的发送和接收端分别打开ivshmem设备、并将该共享内存体mmap到各自的用户空间后,即可通过文件读写实现共享内存通信。
ivshmem流控
考虑到大数据量的数据传输,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)
中断模式调测
Host os:
./ivshmem_server-m 64 -p /tmp/nahanni &
Host os:
./qemu-system-x86_64 -hda Img -L /pc-bios/ --smp 4 -chardevsocket,path=/tmp/nahanni,id=nahanni -deviceivshmem,chardev=nahanni,size=32m,msi=off -serialtelnet:0.0.0.0:4000,server,nowait,nodelay -enable-kvm&
./qemu-system-x86_64 -hda Img -L /pc-bios/ --smp 4 -chardevsocket,path=/tmp/nahanni,id=nahanni -deviceivshmem,chardev=nahanni,size=32m,msi=off -serial telnet:0.0.0.0:4001,server,nowait,nodelay-enable-kvm&
(/dev/shm/下若有nahanni同名文件需先删除)
Guest os1、2:
insmod kvm_ivshmem.ko
Guest os1、2:
mknod -mode=666 /dev/ivshmem c 245 0
Guest os1:
./ftp_recv /dev/ivshmem ./wat 0
Guest os2:
./ftp_send /dev/ivshmem ./lht 1
性能测试
测试环境X86虚拟机环境,虚拟机设置单核,虚拟机内存大小110M。
Ivshmem采用中断模式,流控窗口大小16。
Virtio采用Bridge+vhost+vio最优化模式的。
测试步骤Ivshmem调测步骤如下:
一、Host侧启动ivshmem_server,共享内存设置为32M
./ivshmem_server-m 32 -p /tmp/nahanni &
二、host侧qemu启动:
./qemu-system-x86_64 -hda Img -L /pc-bios/ -chardevsocket,path=/tmp/nahanni,id=nahanni -deviceivshmem,chardev=nahanni,size=32m,msi=off -serialtelnet:0.0.0.0:4000,server,nowait,nodelay -enable-kvm&
接着端口号改为4001在启动一个guestos。
三、分别telnet上127.0.0.1:4000和4001后插入内核模块
insmodkvm_ivshmem.ko
四、分别创建节点
mknod-mode=666 /dev/ivshmem c 245 0
五、接收侧guestos执行:
./ftp_recv /dev/ivshmem ./ recvfile 0 1024 1000
六、发送侧guestos执行:
./ftp_send /dev/ivshmem ./sendfile 1 1024 1000
这里的0、1表示vmid,1024为数据块大小,1000为块个数
Virtio调测步骤如下:
一、host侧qemu启动,使用vhost提升性能:
./qemu-system-x86_64-hda Img -L /pc-bios/ -balloon virtio -serial telnet:0.0.0.0:4002,server,nowait,nodelay-enable-kvm -net nic,macaddr=52:54:00:94:78:e9,model=virtio -nettap,script=no,vhost=on &
./qemu-system-x86_64-hda Img -L /pc-bios/ -balloon virtio -serialtelnet:0.0.0.0:4003,server,nowait,nodelay -enable-kvm -netnic,macaddr=52:52:00:54:68:e8,model=virtio -net tap,script=no,vhost=on &
二、host侧网桥配置:
brctladdbr br0
brctladdif br0 tap0
brctladdif br0 tap1
brctladdif br0 eth0
ifconfigtap0 up
ifconfigtap1 up
ifconfigeth0 up
ifconfigbr0 10.74.154.3 up
ifconfigeth0 0.0.0.0
三、两个guest侧上分别运行
ifconfigeth0 10.74.154.4
ifconfigeth0 10.74.154.5
四、接收侧guestos执行:
./tcp_server / recvfile 1024 1000
五、发送侧guestos执行:
./tcp_client10.74.154.4 /sendfile 1024 1000
这里的1024为数据块大小,1000为块个数
测试用例
请联系www.lht@gmail.com,或回复
测试结果如下表所示:
传输数据块大小*块个数 | Virtio传输时间(ns) | |
256*100 | 700 | 2000 |
256*1000 | 1200 | 6000 |
256*10000 | 7000 | 37000 |
4096*100 | 1200 | 7500 |
4096*1000 | 2600 | 38000 |
4096*10000 | 20000 | 380000 |
65536*100 | 3000 | 50000 |
65536*1000 | 30000 | 500000 |
需要指出的是,非中断模式测试结果与中断模式性能一致,这里没有单列性能对比数据。
从以上测试数据可以得到如下结论:
1、 ivshmem在传输1m以上数据时,性能基本稳定,数据量与时间基本成线性关系
2、 1m以上数据传输性能,ivshmem性能是virtio性能10~20倍
3、小数据量传输ivshmem性能比virtio有明显优势。
原文链接: http://blog.csdn.net/u014358116/article/details/22753423
作者:爱海tatao
[ 转载请保留原文出处、作者和链接。]
更多推荐
所有评论(0)