ivshmem(Nahanni)實現分析與性能測試

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爲塊個數

 

測試用例


請聯繫[email protected],或回覆


測試結論

測試結果如下表所示:

傳輸數據塊大小*塊個數

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 
[ 轉載請保留原文出處、作者和鏈接。]

發佈了35 篇原創文章 · 獲贊 9 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章