• 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实现

  • 相关阅读:
    springboot文件上传: 单个文件上传 和 多个文件上传
    Eclipse:很不错的插件-devStyle,将你的eclipse变成idea风格
    springboot项目搭建:结构和入门程序
    POJ 3169 Layout 差分约束系统
    POJ 3723 Conscription 最小生成树
    POJ 3255 Roadblocks 次短路
    UVA 11367 Full Tank? 最短路
    UVA 10269 Adventure of Super Mario 最短路
    UVA 10603 Fill 最短路
    POJ 2431 Expedition 优先队列
  • 原文地址:https://www.cnblogs.com/dream397/p/14161802.html
Copyright © 2020-2023  润新知