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_vectorsefd,对于通用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_posnefd等发送给新的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;

/*BAR0MemoryRegion结构初始化,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注册的地址空间申请内存空间,配置ivshmemmemory资源

          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,或回复


测试结论

测试结果如下表所示:

传输数据块大小*块个数

Ivshmem传输时间(ns)

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 
[ 转载请保留原文出处、作者和链接。]

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐