Vhost Architecture(基于kernel3.10.0 & qemu2.0.0)

来源:互联网 发布:淘宝女包品牌排行 编辑:程序博客网 时间:2024/06/14 17:59

在前面的文章中在介绍virtio机制中,可以看到在通常的应用中一般使用QEMU用户态程序来模拟I/O访问,而Guest中的数据要通过Guest到Host Userspace的第一次拷贝,再经过Host userspace到Host Kernel的第二次拷贝,这种多次的数据拷贝和CPU特权级的切换对性能有相当大的影响。为加速virtio设备的I/O性能,VHOST通过driver的形式在Host Kernel中直接实现了virtio设备的模拟。通过在Host Kernel中对virtios设备的模拟运行允许Guest与Host Kernel直接进行数据交换,从而避免了用户空间的system call与数据拷贝的性能消耗。

Vhost的初始化

Qemu层

vhost_net的启用是在命令行的-netdev tap,…中指定vhost=on选项,其初始化流程如下:

  1. 根据“Qemu之Network Device全虚拟方案一:前端网络流的建立”一文中,tap设备的创建会调用到net_init_tap()函数;
  2. net_init_tap()其中会检查选项是否指定vhost=on,如果指定,则会调用到vhost_net_init()进行初始化;(net_init_tap->net_init_tap_one->vhost_net_init)
  3. 通过open(“/dev/vhost-net”, O_RDWR)打开了vhost driver;并通过ioctl(vhost_fd)进行了一系列的初始化;
  4. 调用ioctl VHOST_SET_VRING_KICK 设置kick fd(guest -> vhost) (VirtQueue.host_notifier.fd); (vhost.c: vhost_virtqueue_start)  
    (调用路径:
    virtionet_open
    ->virtionet_init
    ->virtio_set_status
    ->k->set_status(vdev, val)(注册在:virtio_net_class_init:vdc->set_status = virtio_net_set_status)
    ->virtio_net_set_status
    ->virtio_net_vhost_status
    ->vhost_net_start
    ->vhost_net_start_one
    ->vhost_dev_start
    ->vhost_virtqueue_start)
  5. 调用ioctl VHOST_SET_VRING_CALL 设置call fd(vhost -> guest) (VirtQueue.guest_notifier.fd);(vhost.c: vhost_virtqueue_mask)
    (调用路径:
    virtionet_open
    -> virtionet_init
    -> virtio_set_status
    -> k->set_status(vdev, val)(注册在:virtio_net_class_init:vdc->set_status = virtio_net_set_status)
    -> virtio_net_vhost_status
    -> vhost_net_start
    -> r = k->set_guest_notifiers(qbus->parent, total_queues * 2, true); (注册在:virtio_pci_bus_class_init:k->set_guest_notifiers = virtio_pci_set_guest_notifiers;)
    -> virtio_pci_set_guest_notifiers
    -> virtio_pci_set_guest_notifier
    -> vdc->guest_notifier_mask(vdev, n, !assign); (注册在virtio_net_class_init:vdc->guest_notifier_mask = virtio_net_guest_notifier_mask;)
    -> virtio_net_guest_notifier_mask
    -> vhost_net_virtqueue_mask
    -> vhost_virtqueue_mask
    )


Kernel层

vhost在kernel中是miscdevice的形态存在的:

de>static const struct file_operations vhost_net_fops = {    .owner          = THIS_MODULE,    .release        = vhost_net_release,    .unlocked_ioctl = vhost_net_ioctl,#ifdef CONFIG_COMPAT    .compat_ioctl   = vhost_net_compat_ioctl,#endif    .open           = vhost_net_open,    .llseek        = noop_llseek,};static struct miscdevice vhost_net_misc = {    .minor = VHOST_NET_MINOR,    .name = "vhost-net",    .fops = &vhost_net_fops,};static int vhost_net_init(void){    if (experimental_zcopytx)        vhost_net_enable_zcopy(VHOST_NET_VQ_TX);    return misc_register(&vhost_net_misc);}module_init(vhost_net_init);de>

vhost_net_open()中对vhost进行了初始化,主要数据结构关系如下图:

Vhost Architecture(基于kernel3.10.0  qemu2.0.0) - 六六哥 - 六六哥的博客
 
Vhost Architecture(基于kernel3.10.0  qemu2.0.0) - 六六哥 - 六六哥的博客
 
 
在初始化过程中,vhost driver创建了一个名为“vhost-$pid”内核线程,$pid为Qemu的PID。 这个内核线程被称为“vhost worker thread”,该worker thread的任务即为处理virtio的I/O事件。


Guest层

在Guest启动后会调用/drivers/virtio/virtio.c中的virtio_dev_probe进行virtio设备的初始化,

de>static int virtio_dev_probe(struct device *_d){    ......    drv->probe(dev);    ......    add_status(dev, VIRTIO_CONFIG_S_DRIVER_OK);      ......}de>

drv->probe()即virtnet_probe(),其中对vq进行了初始化,此过程与前文中的virtio设备正常初始化过程一致,同时将vq的相关信息通告给了前端,Qemu接管后对vq的信息进行了记录,最后Qemu最终调用到vhost_net_start()将vq的配置下发到vhost中:(QEMU代码)

  1. ioctl VHOST_SET_VRING_NUM 设置 vring size;
  2. ioctl VHOST_SET_VRING_BASE 设置 (VirtQueue.last_avail_idx);
  3. 设置vhost_virtqueue中ring相关的成员(desc,avail, used_size, used_phys, used,
    ring_size, ring_phys,ring);
  4. 调用vhost_virtqueue_set_addr设置相关地址;

这样Virtio的vring空间就映射到了Host的Kernel中。

三者之间的关系如下:

Vhost Architecture(基于kernel3.10.0  qemu2.0.0) - 六六哥 - 六六哥的博客

 

vhost的运行

在上图中可以看到vhost和kvm是两个独立的运行模块,对于Guest来说,vhost并没有模拟一个完整的PCI适配器。它内部只涉及了virtqueue的操作,而virtio设备的适配模拟仍然由Qemu来负责。所以vhost的整个架构中,其并不是一个完整的virtio设备实现,它依赖于用户空间的管理平面处理,而自身完成位于Host Kernel层的数据面处理
vhost与kvm的事件通信通过eventfd机制来实现,主要包括两个方向的event,一个是Guest到Vhost方向的kick event,通过ioeventfd承载;另一个是Vhost到Guest方向的call event,通过irqfd承载。


guest_notifier的使用

  1. vhost在处理完请求(收到数据包),将buffer放到used ring上面之后,往call fd里面写入;
  2. 如果成功设置了irqfd,则kvm会直接中断guest。如果没有成功设置,则走以下的路径:
  3. Qemu通过select调用监听到该事件(因为vhost的callfd就是qemu里面对应vq的guest_notifier,它
    已经被加入到selectablefd列表);
  4. 调用virtio_pci_guest_notifier_read通知guest;
  5. guest从used ring上获取相关的数据;

过程如图:

Vhost Architecture(基于kernel3.10.0  qemu2.0.0) - 六六哥 - 六六哥的博客
 

QEMU2.0.0:

1. set_guest_notifiers初始化流程
static void virtio_pci_bus_class_init(ObjectClass *klass, void *data){
     k->set_guest_notifiers = virtio_pci_set_guest_notifiers;
}


2. guest_notifier的fdread函数初始化为virtio_queue_guest_notifier_read流程:
vhost_net_start
-> r = k->set_guest_notifiers(qbus->parent, total_queues * 2, true); //调用virtio_pci_set_guest_notifiers
-> virtio_pci_set_guest_notifiers
-> virtio_pci_set_guest_notifier
-> virtio_queue_set_guest_notifier_fd_handler

void virtio_queue_set_guest_notifier_fd_handler(VirtQueue *vq, bool assign, bool with_irqfd){
     if (assign && !with_irqfd) {
             event_notifier_set_handler(&vq->guest_notifier,virtio_queue_guest_notifier_read);
         } 
}

-> qemu_set_fd_handler
-> qemu_set_fd_handler2 (将virtio_queue_guest_notifier_read设置为guest_notifier的fdread函数,并加入到iohandlers中)


3. PCI的中断处理函数初始化
PCIBus *i440fx_init(){
     pci_bus_irqs(b, piix3_set_irq, pci_slot_get_pirq, piix3, PIIX_NUM_PIRQS); //设定bus->set_irq为piix3_set_irq
}



4. notify初始化流程 
virtio_pci_bus_class_init(){
     k->notify = virtio_pci_notify;     /*notify注册为virtio_pci_notify*/
}


5. 监听事件FD的过程
在vhost_net_start中,已经将guest_notifier加入到了iohandlers中

main
-> main_loop
-> main_loop_wait
-> qemu_iohandler_fill() //将iohandlers中所有的fd和处理函数加入到监听集合中
-> os_host_main_loop_wait
-> qemu_poll_ns //开始阻塞监听,返回时候说明有监听事件发生



6. Guest收包中断过程

os_host_main_loop_wait
-> qemu_poll_ns返回
-> qemu_iohandler_poll 遍历iohandlers对时间进行处理
-> 遍历iohandlers,处理所有的event
-> ioh->fd_read(ioh->opaque); //调用fdread函数,也就是virtio_queue_guest_notifier_read

-> virtio_queue_guest_notifier_read
-> virtio_irq
-> virtio_notify_vector
-> k->notify(q 
 bus->parent, vector); //调用virtio_pci_notify
-> virtio_pci_notify
-> pci_set_irq
-> pci_irq_handler
-> pci_change_irq_level
-> bus->set_irq //调用的是piix3_set_irq

-> piix3_set_irq
-> piix3_set_irq_level
-> piix3_set_irq_pic
-> qemu_set_irq //产生中断

-----------------------------------------

host_notifier的使用

  1. Guest中的virtio设备将数据放入avail ring上面后,写发送命令至virtio pci配置空间;
  2. Qemu截获寄存器的访问,调用注册的kvm_memory_listener中的eventfd_add回调函数kvm_eventfd_add();
  3. 通过kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &kick)进入k中;
  4. kvm唤醒挂载在ioeventfd上vhost worker thread;
  5. vhost worker thread从avail ring上获取相关数据。

kvm相关流程如图:

Vhost Architecture(基于kernel3.10.0  qemu2.0.0) - 六六哥 - 六六哥的博客
 


vhost中数据结构关系与函数调用流程如下图:

Vhost Architecture(基于kernel3.10.0  qemu2.0.0) - 六六哥 - 六六哥的博客
 
整理函数调用关系如下

1. 系统中的eventfd_add(), 以及memory_listener的注册

static MemoryListener kvm_memory_listener = {
    .eventfd_add = kvm_mem_ioeventfd_add,
};

static MemoryListener kvm_io_listener = {
    .eventfd_add = kvm_io_ioeventfd_add,
};

int kvm_init(QEMUMachine *machine){
    memory_listener_register(&kvm_memory_listener, &address_space_memory);
    memory_listener_register(&kvm_io_listener, &address_space_io);
}

net_init_tap
-> net_init_tap_one
-> vhost_net_init
-> vhost_dev_init
int vhost_dev_init(){
    hdev->memory_listener = (MemoryListener) {
        .eventfd_add = vhost_eventfd_add,
    };

    ...

    memory_listener_register(&hdev->memory_listener, &address_space_memory);

}



2. virtio pci设备的读写操作注册

static const MemoryRegionOps virtio_pci_config_ops = {
    .write = virtio_pci_config_write,

};


3. eventfd_add的触发,用于向内核中注册VIRTIO PCI设备
Qemu截获寄存器的访问,调用注册的kvm_memory_listener中的eventfd_add回调函数kvm_eventfd_add();

virtio_pci_config_ops->write //调用virtio_pci_config_write
-> virtio_pci_config_write
-> virtio_ioport_write
-> virtio_pci_start_ioeventfd
-> virtio_pci_set_host_notifier_internal
-> memory_region_add_eventfd
-> memory_region_transaction_commit
-> address_space_update_topology
-> address_space_update_ioeventfds
-> address_space_add_del_ioeventfds

address_space_add_del_ioeventfds(){
     MEMORY_LISTENER_CALL(eventfd_add, ......); //调用eventadd, 起作用的是kvm_io_ioeventfd_add
}


3. kvm_io_ioeventfd_add将事件下发到内核,初始化注册VIRTIO_PCI的IO设备
eventadd
-> kvm_io_ioeventfd_add
-> kvm_set_ioeventfd_pio
-> r = kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &kick);


5. KVM模块响应并初始化增加IO_DEVICE的eventfd
kvm_vm_ioctl
-> kvm_ioeventfd 
-> kvm_assign_ioeventfd //创建IO设备的,并分配eventfd
-> kvm_iodevice_init(&p->dev, &ioeventfd_ops);  //初始化write方法为ioeventfd_write
-> kvm_io_bus_register_dev() 
-> list_add_tail(&p->list, &kvm->ioeventfds); 

static const struct kvm_io_device_ops ioeventfd_ops = {
     .write      = ioeventfd_write,

};


6.VHOST内核线程的建立
vhost_net_ioctl() //VHOST_SET_OWNER
-> vhost_net_set_owner
-> vhost_dev_set_owner
-> worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid); //处理函数为vhost_worker



7.VHOST设备进行收发poll的注册,当被open的时候,注册读写的处理函数为handle_tx_net和handle_tx_net,并且使用默认的poll方法
对vhost-net文件进行open时
static const struct file_operations vhost_net_fops = {
     .open           = vhost_net_open,
     //使用默认的poll方法,???怎么和eventfd_fops联系起来的?
};

默认的poll方法
static const struct file_operations eventfd_fops = {
     .poll          = eventfd_poll,
};

static struct miscdevice vhost_net_misc = {
     .name = "vhost-net",
     .fops = &vhost_net_fops,
};


/*
* 在vhost_net_open()中对vhost进行了初始化
*/
static int vhost_net_open(struct inode *inode, struct file *f)
{
     n->vqs[VHOST_NET_VQ_TX].vq.handle_kick = handle_tx_kick;
     n->vqs[VHOST_NET_VQ_RX].vq.handle_kick = handle_rx_kick;

     vhost_poll_init(n->poll + VHOST_NET_VQ_TX, handle_tx_net, POLLOUT, dev);
     vhost_poll_init(n->poll + VHOST_NET_VQ_RX, handle_rx_net, POLLIN, dev);
}

vhost_poll_init(){
     init_waitqueue_func_entry(&poll->wait, vhost_poll_wakeup);
     init_poll_funcptr(&poll->table, vhost_poll_func); //注册poll的proc方法为vhost_poll_func
}

void vhost_poll_init(struct vhost_poll *poll, vhost_work_fn_t fn, unsigned long mask, struct vhost_dev *dev)
{
     init_waitqueue_func_entry(&poll->wait, vhost_poll_wakeup);
     init_poll_funcptr(&poll->table, vhost_poll_func);

     vhost_work_init(&poll->work, fn);
}

vhost_net_ioctl //default
-> vhost_vring_ioctl
-> vhost_poll_start
-> file->f_op->poll(file, &poll->table); //??怎么和eventfd联系起来的,这里用默认的eventfd_poll,会阻塞在这里,一直等到有事件才返回vhost_poll_start
-> eventfd_poll
-> poll_wait
-> p->_qproc(filp, wait_address, p); //这里是vhost_poll_func
-> vhost_poll_func
-> add_wait_queue


8. KVM模块响应IO设备写操作
当GUEST有IO设备写操作,会触发write,也就是触发ioeventfd_write

ioeventfd_write
-> eventfd_signal
-> wake_up_locked_poll(&ctx->wqh, POLLIN); //这里通知到了vhost work thread

9. vhost模块内核线程报文收发流程
vhost的线程在vhost_poll_start里阻塞在file->f_op->poll(file, &poll->table),直到wake_up_locked_poll(&ctx->wqh, POLLIN);后才返回
vhost_poll_start //从mask = file->f_op->poll(file, &poll->table);返回
-> vhost_poll_wakeup
-> vhost_poll_wakeup
-> vhost_poll_queue
-> vhost_work_queue
-> wake_up_process(dev->worker); //让worker运行,每个worker是一个vhost的内核线程,这里的处理函数为vhost_worker 
-> vhost_worker
-> work->fn(work); //fn就是open时注册的handle_tx_net和handle_rx_net, 这里调用fn进行收发


handle_rx_net //收包
-> handle_rx
-> sock->ops->recvmsg(NULL, sock, &msg, sock_len, MSG_DONTWAIT | MSG_TRUNC); //标准socket接收方法


handle_tx_net //发包
-> handle_tx
-> sock->ops->sendmsg(NULL, sock, &msg, len);



数据发送的完整流程

在上面过程中可以看到vhost最核心处就在于将Guest中的virtio用于传输层的vring队列空间通过mapping方式与Host Kernel进行了共享,这样数据就不需要通过多次的跨态拷贝,直接进入了Kernel;通过io event事件机制进行了收发方向的通告,使vhost与Guest达到很好的配合。

而数据在kernel中最终是如何发送出去的呢?

看如下的图就明白了,在vhost的使能时,我们创建了tap,tap设备的用法和前文介绍的一致,通过加入Bridge来实现数据的交换。而vhost中的数据直接使用tap设备在内核中的sock接口进行了发送和接收,这些动作均是在vhost的worker thread中进行的。

Vhost Architecture(基于kernel3.10.0  qemu2.0.0) - 六六哥 - 六六哥的博客

 

相关code

Kernel相关

  • drivers/vhost/vhost.c - common vhost driver code
  • drivers/vhost/net.c - vhost-net driver
  • virt/kvm/eventfd.c - ioeventfd and irqfd

Qemu相关

  • hw/vhost.c - common vhost initialization code
  • hw/vhost_net.c - vhost-net initialization
0 0
原创粉丝点击