zoukankan      html  css  js  c++  java
  • ioeventfd创建

     

    Qemu是一个应用程序,所以入口函数当然是main函数,但是一些被type_init修饰的函数会在main函数之前运行。这里分析的代码是emulate x86 的一款i440板子。main函数中会调用在main函数中会调用kvm_init函数来创建一个VM(virtual machine),然后调用机器硬件初始化相关的函数,对PCI,memory等进行emulate。然后调用qemu_thread_create创建线程,这个函数会调用pthread_create创建一个线程,每个VCPU依靠一个线程来运行。在线程的处理函数qemu_kvm_cpu_thread_fn中,会调用kvm_init_vcpu来创建一个VCPU(virtual CPU),然后调用kvm_vcpu_ioctl,参数KVM_RUN,这样就进入KVM中了。进入KVM中第一个执行的函数名字相同,也叫kvm_vcpu_ioctl,最终会调用到kvm_x86_ops->run()进入到Guest OS,如果Guest OS要写某个端口,会产生一条IO instruction,这时会从Guest OS中退出,调用kvm_x86_ops->handle_exit函数,其实这个函数被赋值为vmx_handle_exit,最终会调用到kvm_vmx_exit_handlers[exit_reason](vcpu),kvm_vmx_exit_handlers是一个函数指针,会根据产生事件的类型来匹配使用那个函数。这里因为是ioport访问产生的退出,所以选择handle_io函数。


     

    5549static int (*kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = {
    5550        [EXIT_REASON_EXCEPTION_NMI]           = handle_exception,
    5551        [EXIT_REASON_EXTERNAL_INTERRUPT]      = handle_external_interrupt,
    5552        [EXIT_REASON_TRIPLE_FAULT]            = handle_triple_fault,
    5553        [EXIT_REASON_NMI_WINDOW]              = handle_nmi_window,
    5554        [EXIT_REASON_IO_INSTRUCTION]          = handle_io,
    5555        [EXIT_REASON_CR_ACCESS]               = handle_cr,
    5556        [EXIT_REASON_DR_ACCESS]               = handle_dr,
    5557        [EXIT_REASON_CPUID]                   = handle_cpuid,
    5558        [EXIT_REASON_MSR_READ]                = handle_rdmsr,
    5559        [EXIT_REASON_MSR_WRITE]               = handle_wrmsr,
    5560        [EXIT_REASON_PENDING_INTERRUPT]       = handle_interrupt_window,
    5561        [EXIT_REASON_HLT]                     = handle_halt,
    5562        [EXIT_REASON_INVD]                    = handle_invd,
    5563        [EXIT_REASON_INVLPG]                  = handle_invlpg,
    5564        [EXIT_REASON_VMCALL]                  = handle_vmcall,
    5565        [EXIT_REASON_VMCLEAR]                 = handle_vmclear,
    5566        [EXIT_REASON_VMLAUNCH]                = handle_vmlaunch,
    5567        [EXIT_REASON_VMPTRLD]                 = handle_vmptrld,
    5568        [EXIT_REASON_VMPTRST]                 = handle_vmptrst,
    5569        [EXIT_REASON_VMREAD]                  = handle_vmread,
    5570        [EXIT_REASON_VMRESUME]                = handle_vmresume,
    5571        [EXIT_REASON_VMWRITE]                 = handle_vmwrite,
    5572        [EXIT_REASON_VMOFF]                   = handle_vmoff,
    5573        [EXIT_REASON_VMON]                    = handle_vmon,
    5574        [EXIT_REASON_TPR_BELOW_THRESHOLD]     = handle_tpr_below_threshold,
    5575        [EXIT_REASON_APIC_ACCESS]             = handle_apic_access,
    5576        [EXIT_REASON_WBINVD]                  = handle_wbinvd,
    5577        [EXIT_REASON_XSETBV]                  = handle_xsetbv,
    5578        [EXIT_REASON_TASK_SWITCH]             = handle_task_switch,
    5579        [EXIT_REASON_MCE_DURING_VMENTRY]      = handle_machine_check,
    5580        [EXIT_REASON_EPT_VIOLATION]           = handle_ept_violation,
    5581        [EXIT_REASON_EPT_MISCONFIG]           = handle_ept_misconfig,
    5582        [EXIT_REASON_PAUSE_INSTRUCTION]       = handle_pause,
    5583        [EXIT_REASON_MWAIT_INSTRUCTION]       = handle_invalid_op,
    5584        [EXIT_REASON_MONITOR_INSTRUCTION]     = handle_invalid_op,
    5585};

     

    QEMU虚拟机网络通信

    • 主机vhost驱动加载时调用vhost_net_init注册一个MISC驱动,生成/dev/vhost-net的设备文件。

    • 主机qemu-kvm启动时调用open对应的vhost_net_open做主要创建队列和收发函数的挂载,接着调用ioctl启动内核线程vhost,做收发包的处理。

    • 主机qemu通过ioctl配置kvm模块,主要设置通信方式,因为主机vhost和virtio只进行报文的传输,kvm进行提醒。

    • 虚拟机virtio模块注册,生成虚拟机的网络设备,配置中断和NAPI。

    • 虚拟机发包流程如下:

      • 直接从应用层走协议栈最后调用发送接口ndo_start_xmit对应的start_xmit,将报文放入发送队列,vp_notify通知kvm。
      • kvm通过vmx_handle_exit一系列调用到wake_up_process唤醒vhost线程。
      • vhost模块的线程激活并且拿到报文,在通过之前绑定的发送接口handle_tx_kick进行发送,调用虚拟网卡的tun_sendmsg最终到netif_rx接口进入主机内核协议栈。
    • 虚拟机收包流程如下:

      • tap设备的ndo_start_xmit对应的tun_net_xmit最终调用到wake_up_process激活vhost线程,调用handle_rx_kick,将报文放入接收队列。
      • 通过一系列的调用到kvm模块的接口kvm_vcpu_kick,向qemu虚拟机注入中断
      • 虚拟机virtio模块中断调用接口vp_interrupt,调用virtnet_poll,再调用到netif_receive_skb进入虚拟机的协议栈。

    vhost 与 kvm 的事件通信通过 eventfd 机制来实现,主要包括两个方向的 event,一个是 guest 到 vhost 方向的 kick event,通过 ioeventfd 实现;另一个是 vhost 到 guest 方向的 call event,通过 irqfd 实现。

    在使用virtio-blk的情况时,virtio notify使用的ioeventfd机制,原因是为了提高性能,能够较快速的回到guest中运行。具体是如何建立这个ioeventfd的呢?流程理出来了,细节没看:

    - 在guest中,virtio-blk的初始化或者说是在探测virtio-blk之前

     
    1. virtio_dev_probe  
    2. |-->add_status  
    3. |-->dev->config->set_status[vp_set_status]  
    4. |-->iowrite8(status, vp_dev->ioaddr + VIRTIO_PCI_STATUS)  



    这里就产生VM exit到Qemu中了,而在Qemu中有如下的处理:

    - Qemu中建立ioeventfd的处理流程:

     
    1. virtio_pci_config_write  
    2. |-->virtio_ioport_write  
    3. |-->virtio_pci_start_ioeventfd  
    4. |-->virtio_pci_set_host_notifier_internal  
    5. |-->virtio_queue_set_host_notifier_fd_handler  
    6. |-->memory_region_add_eventfd  
    7. |-->memory_region_transaction_commit  
    8. |-->address_space_update_ioeventfds  
    9. |-->address_space_add_del_ioeventfds  
    10. |-->eventfd_add[kvm_mem_ioeventfd_add]  
    11. |-->kvm_set_ioeventfd_mmio  
    12. |-->kvm_vm_ioctl(...,KVM_IOEVENTFD,...)  



    最后这一步就切换到kvm内核模块中来通过KVM_IOEVENT来建立ioeventfd:
    - kvm内核模块中建立ioeventfd:

     
    1. kvm_ioeventfd  
    2. |-->kvm_assign_ioeventfd  



    在这个流程中为某段区域建立了一个ioeventfd,这样的话guest在操作这块区域的时候就会触发ioeventfd(这是fs的eventfd机制),从而通知到Qemu,Qemu的main loop原先是阻塞的,现在有ioevent发生之后就可以得到运行了,也就可以做对virtio-blk相应的处理了。

    那么当guest对该块区域内存区域进行写的时候,势必会先exit到kvm内核模块中,kvm内核模块又是怎么知道这块区域是注册了event的呢?是怎么个流程呢?

    只使用EPT的情况下,guest对一块属于MMIO的区域进行读写操作引起的exit在kvm中对应的处理函数是handle_ept_misconfig,下面就看下具体的流程:

     
    1. handle_ept_misconfig  
    2. |-->x86_emulate_instruction  
    3. |-->x86_emulate_insn  
    4. |-->writeback  
    5. |-->segmented_write  
    6. |-->write_emulated[emulator_write_emulated]  
    7. |-->emulator_read_write  
    8. |-->emulator_read_write_onepage  
    9. |-->ops->read_write_mmio[write_mmio]  
    10. |-->vcpu_mmio_write  
    11. |-->kvm_io_bus_write  
    12. |-->__kvm_io_bus_write  
    13. |-->kvm_iodevice_write  
    14. |-->ops->write[ioeventfd_write]  



    在ioeventfd_write函数中会调用文件系统eventfd机制的eventfd_signal函数来触发相应的事件。

    上述就是整个ioeventfd从创建到触发的流程!

    1. 什么是eventfd?

    eventfd是只存在于内存中的文件,通过系统调用sys_eventfd可以创建新的文件,它可以用于线程间、进程间的通信,无论是内核态或用户态。其实现机制并不复杂,参考内核源码树的fs/eventfd.c文件,看数据结构struct eventfd_ctx的定义:

     struct eventfd_ctx {
             struct kref kref;
             wait_queue_head_t wqh;
             /*
              * Every time that a write(2) is performed on an eventfd, the
              * value of the __u64 being written is added to "count" and a
              * wakeup is performed on "wqh". A read(2) will return the "count"
              * value to userspace, and will reset "count" to zero. The kernel
              * side eventfd_signal() also, adds to the "count" counter and
              * issue a wakeup.
              */
             __u64 count;
             unsigned int flags;
     }

    eventfd的信号实际上就是上面的count,write的时候对其++,read的时候则清零(并不绝对正确)。wait_queue_head wqh则是用来保存监听eventfd的睡眠进程,每当有进程来epoll、select且此时不存在有效的信号(count <= 0),则sleep在wqh上。当某一进程对该eventfd进行write的时候,则会唤醒wqh上的睡眠进程。代码细节参考eventfd_read(), eventfd_write()

    virtio用到的eventfd其实是kvm中的ioeventfd机制,是对eventfd的又一层封装(eventfd + iodevice)。该机制不进一步细说,下面结合virtio的kick操作使用来分析,包括两部分:
    1. 如何设置:如何协商该eventfd?
    2. 如何产生kick信号:是谁来负责写从而产生kick信号?

    2. 如何设置?

    即qemu用户态进程是如何和kvm.ko来协商使用哪一个eventfd来kick通信。
    这里就要用到kvm的一个系统调用:ioctl(KVM_IOEVENTFD, struct kvm_ioeventfd),找一下qemu代码中执行该系统调用的路径:

    memory_region_transaction_commit() {
        address_space_update_ioeventfds() {
            address_space_add_del_ioeventfds() {
                MEMORY_LISTENER_CALL(eventfd_add, Reverse, &section,
                fd->match_data, fd->data, fd->e);
            }
        }
    }

    上面的eventfd_add有两种可能的执行路径:
    1. mmio(Memory mapping I/O): kvm_mem_ioeventfd_add()
    2. pio(Port I/O): kvm_io_ioeventfd_add()

    通过代码静态分析,上面的调用路径其实只找到了一半,接下来使用gdb来查看memory_region_transaction_commit()可能的执行路径,结合vhost_blk(qemu用户态新增的一项功能,跟qemu-virtio或dataplane在I/O链路上的层次类似)看一下设置eventfd的执行路径:

    #0  memory_region_transaction_commit () at /home/gavin4code/qemu-2-1-2/memory.c:799
    #1  0x0000000000462475 in memory_region_add_eventfd (mr=0x1256068, addr=16, size=2, match_data=true, data=0, e=0x1253760)
        at /home/gavin4code/qemu-2-1-2/memory.c:1588
    #2  0x00000000006d483e in virtio_pci_set_host_notifier_internal (proxy=0x1255820, n=0, assign=true, set_handler=false)
        at hw/virtio/virtio-pci.c:200
    #3  0x00000000006d6361 in virtio_pci_set_host_notifier (d=0x1255820, n=0, assign=true) at hw/virtio/virtio-pci.c:884
    #4  0x00000000004adb90 in vhost_dev_enable_notifiers (hdev=0x12e6b30, vdev=0x12561f8)
        at /home/gavin4code/qemu-2-1-2/hw/virtio/vhost.c:932
    #5  0x00000000004764db in vhost_blk_start (vb=0x1256368) at /home/gavin4code/qemu-2-1-2/hw/block/vhost-blk.c:189
    #6  0x00000000004740e5 in virtio_blk_handle_output (vdev=0x12561f8, vq=0x1253710)
        at /home/gavin4code/qemu-2-1-2/hw/block/virtio-blk.c:456
    #7  0x00000000004a729e in virtio_queue_notify_vq (vq=0x1253710) at /home/gavin4code/qemu-2-1-2/hw/virtio/virtio.c:774
    #8  0x00000000004a9196 in virtio_queue_host_notifier_read (n=0x1253760) at /home/gavin4code/qemu-2-1-2/hw/virtio/virtio.c:1265
    #9  0x000000000073d23e in qemu_iohandler_poll (pollfds=0x119e4c0, ret=1) at iohandler.c:143
    #10 0x000000000073ce41 in main_loop_wait (nonblocking=0) at main-loop.c:485
    #11 0x000000000055524a in main_loop () at vl.c:2031
    #12 0x000000000055c407 in main (argc=48, argv=0x7ffff99985c8, envp=0x7ffff9998750) at vl.c:4592

    整体执行路径还是很清晰的,vhost模块的vhost_dev_enable_notifiers()来告诉kvm需要用到的eventfd。

    3. 如何产生kick信号?

    大体上来说,guest os觉得有必要通知host对virtqueue上的请求进行处理,就会执行vp_notify(),相当于执行一次port I/O(或者mmio),虚拟机则会退出guest mode。这里假设使用的是intel的vmx,当检测到pio或者mmio会设置vmcs中的exit_reason,host内核态执行vmx_handle_eixt(),检测exit_reason并执行相应的handler函数(kernel_io()),整体的执行路径如下:

    vmx_handle_eixt() {
        /* kvm_vmx_exit_handlers[exit_reason](vcpu); */
        handle_io() {
            kvm_emulate_pio() {
                kernel_io() {
                    if (read) {
                        kvm_io_bus_read() {
    
                        }
                    } else {
                        kvm_io_bus_write() {
                            ioeventfd_write();
                    }
                }
            }
        }
    }

    最后会执行到ioeventfd_write(),这样就产生了一次kick信号。
    如果该eventfd是由qemu侧来监听的,则会执行对应的qemu函数kvm_handle_io();如果是vhost来监听的,则直接在vhost内核模块执行vhost->handle_kick()。

    qemu kmv_handle_io()的调用栈如下所示:

    Breakpoint 1, virtio_ioport_write (opaque=0x1606400, addr=18, val=0) at hw/virtio/virtio-pci.c:270
    270   {
    (gdb) t
    [Current thread is 4 (Thread 0x414e7940 (LWP 29695))]
    (gdb) bt
    #0  virtio_ioport_write (opaque=0x1606400, addr=18, val=0) at hw/virtio/virtio-pci.c:270
    #1  0x00000000006d4218 in virtio_pci_config_write (opaque=0x1606400, addr=18, val=0, size=1) at hw/virtio/virtio-pci.c:435
    #2  0x000000000045c716 in memory_region_write_accessor (mr=0x1606c48, addr=18, value=0x414e6da8, size=1, shift=0, mask=255) at /home/gavin4code/qemu/memory.c:444
    #3  0x000000000045c856 in access_with_adjusted_size (addr=18, value=0x414e6da8, size=1, access_size_min=1, access_size_max=4, access=0x45c689 <memory_region_write_accessor>, mr=0x1606c48)
        at /home/gavin4code/qemu/memory.c:481
    #4  0x000000000045f84f in memory_region_dispatch_write (mr=0x1606c48, addr=18, data=0, size=1) at /home/gavin4code/qemu/memory.c:1138
    #5  0x00000000004630be in io_mem_write (mr=0x1606c48, addr=18, val=0, size=1) at /home/gavin4code/qemu/memory.c:1976
    #6  0x000000000040f030 in address_space_rw (as=0xd05d00 <address_space_io>, addr=49170, buf=0x7f4994f6b000 "", len=1, is_write=true) at /home/gavin4code/qemu/exec.c:2114
    #7  0x0000000000458f62 in kvm_handle_io (port=49170, data=0x7f4994f6b000, direction=1, size=1, count=1) at /home/gavin4code/qemu/kvm-all.c:1674
    #8  0x00000000004594c6 in kvm_cpu_exec (cpu=0x157ec50) at /home/gavin4code/qemu/kvm-all.c:1811
    #9  0x0000000000440364 in qemu_kvm_cpu_thread_fn (arg=0x157ec50) at /home/gavin4code/qemu/cpus.c:930
    #10 0x0000003705e0677d in start_thread () from /lib64/libpthread.so.0
    #11 0x00000037056d49ad in clone () from /lib64/libc.so.6
    #12 0x0000000000000000 in ?? ()

    至此,整个virtio的evenfd机制分析结束

    vhost的控制面由qemu来控制,通过ioctl操作vhost_xxx的内核模块

    static long vhost_net_ioctl(struct file *f, unsigned int ioctl,
                    unsigned long arg)
    {
    ....
        case VHOST_GET_FEATURES:
            features = VHOST_FEATURES;
            if (copy_to_user(featurep, &features, sizeof features))
                return -EFAULT;
            return 0;
        case VHOST_SET_FEATURES:
            if (copy_from_user(&features, featurep, sizeof features))
                return -EFAULT;
            if (features & ~VHOST_FEATURES)
                return -EOPNOTSUPP;
            return vhost_net_set_features(n, features);
    ....

    VHOST_SET_VRING_CALL,设置irqfd,把中断注入guest

    VHOST_SET_VRING_KICK,设置ioeventfd,获取guest notify

       case VHOST_SET_VRING_KICK:
            if (copy_from_user(&f, argp, sizeof f)) {
                r = -EFAULT;
                break;
            }
            eventfp = f.fd == -1 ? NULL : eventfd_fget(f.fd);
            if (IS_ERR(eventfp)) {
                r = PTR_ERR(eventfp);
                break;
            }
            if (eventfp != vq->kick) { /* eventfp不同于vq->kick,此时需要stop vq->kick同时start eventfp */
                pollstop = filep = vq->kick;
                pollstart = vq->kick = eventfp;
            } else
                filep = eventfp;  /* 两者相同,无需stop & start */
            break;
        case VHOST_SET_VRING_CALL:
            if (copy_from_user(&f, argp, sizeof f)) {
                r = -EFAULT;
                break;
            }
            eventfp = f.fd == -1 ? NULL : eventfd_fget(f.fd);
            if (IS_ERR(eventfp)) {
                r = PTR_ERR(eventfp);
                break;
            }
            if (eventfp != vq->call) {  /* eventfp不同于vq->call,此时需要stop vq->call同时start eventfp */
                filep = vq->call;
                ctx = vq->call_ctx;
                vq->call = eventfp;
                vq->call_ctx = eventfp ?
                    eventfd_ctx_fileget(eventfp) : NULL;
            } else
                filep = eventfp;
            break;
        if (pollstop && vq->handle_kick)
            vhost_poll_stop(&vq->poll);
     
        if (ctx)
            eventfd_ctx_put(ctx); /* pollstop之后,释放之前占用的ctx */
        if (filep)
            fput(filep);  /* pollstop之后,释放之前占用的filep */
     
        if (pollstart && vq->handle_kick)
            vhost_poll_start(&vq->poll, vq->kick);
     
        mutex_unlock(&vq->mutex);
     
        if (pollstop && vq->handle_kick)
            vhost_poll_flush(&vq->poll);
        return r;

    下面来看下vhost的数据流,vhost与kvm模块之间通过eventfd来实现,guest到host方向的kick event,通过ioeventfd实现,host到guest方向的call event,通过irqfd实现

  • 相关阅读:
    [大数据从入门到放弃系列教程]在IDEA的Java项目里,配置并加入Scala,写出并运行scala的hello world
    [大数据从入门到放弃系列教程]第一个spark分析程序
    Mac配置Scala和Spark最详细过程
    Mac配置Hadoop最详细过程
    [从零开始搭网站八]CentOS使用yum安装Redis的方法
    CentOS磁盘用完的解决办法,以及Tomcat的server.xml里无引用,但是项目仍启动的问题
    Mysql 删除重复数据只保留id最小的
    bootstrap媒体查询常用写法
    Arduino Uno 在win7 64位下的驱动问题
    VS项目模板文件位置
  • 原文地址:https://www.cnblogs.com/dream397/p/14161802.html
Copyright © 2011-2022 走看看