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中)

void pci_set_irq(PCIDevice *pci_dev, intlevel)

{int intx =pci_intx(pci_dev);

pci_irq_handler(pci_dev, intx, level);

}

/*0 <= irq_num <= 3. level must be 0 or 1*/

static void pci_irq_handler(void *opaque, int irq_num, intlevel)

{

PCIDevice*pci_dev =opaque;intchange;

change= level -pci_irq_state(pci_dev, irq_num);if (!change)return;

pci_set_irq_state(pci_dev, irq_num, level);

pci_update_irq_status(pci_dev);if(pci_irq_disabled(pci_dev))return;

pci_change_irq_level(pci_dev, irq_num, change);

}

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(qbus->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 //产生中断

一. 概述

在上半部已经将GuestOS驱动与QEMU设备交互的过程描述了一下,描述的目的是便于理解Vhost-blk的工作原理,如果想从另外一个角度了解。

分享一个博文链接:http://blog.csdn.net/zhuriyuxiao/article/details/8824735

结合这篇文章,应该可以更好的理解virtio-block的原理。

言归正传,上部分总结到,GuestOS中virtio-block驱动其实只是一个请求触发,并且要一个请求处理结果,对于GuestOS virtio-blk驱动的对外接口如下:

1. virtio-blk驱动将IO请求通过virtio_queue同步给QEMU后,通过iowrite16写一个pci地址。

2.等虚拟硬件处理完IO请求以后,将请求结果通过virtio_queue同步回来,给GuestOS一个中断,调用中断处理函数,处理IO请求的结果就OK。

既然virtio-blk对外接口我们能确定,就算使用vhost-blk,也要遵循上面的接口,才能让GuestOS驱动正常运行。

后面就围绕着vhost-blk如何完成这些工作描述它的工作原理。

二. Vhost-blk架构

按照惯例,先上图:

如上图,与上部分virtio-blk的架构图有些区别,主要还是在handle_output到Disk的部分.

从GuestOS到kernel 和 kernel到GuestOS驱动 两个黑色箭头无论是否有vhost-blk都时一样的,就是上面介绍的两个接口。

值得关注的是在kernel部分,有一个vhost-blk模块,他是在驱动层(上半部分已经提到过)。

如果QEMU开启vhost-blk,handle_output就会跳过vfs,fs等kernel层,通过vhost-blk模块直接将请求提交给硬件,所以要补充,vhost-blk开启后,QEMU后端只能是block描述符,不能是一个文件,在vhost-blk内核模块中,会检查。

当vhost-blk执行完毕会返回到QEMU,但我用白色箭头表示,意思是vhost-blk将IO请求结果返回给GuestOS,并没有真正等到内核态切回QEMU的用户态再执行,而是直接从vhost-blk层就提交了中断,至于大家好奇,怎么从内核态就能通知QEMU用户态程序触发中断给GuestOS呢,这里QEMU利用了一个巧妙的通知机制,慢慢为大家分享。

三. IO的传送流

首先,因为上半部分已经为大家介绍过,virtio-blk是通过一个virtio_queue将IO请求同步给QEMU,当开启vhost-blk后,QEMU又将virtio_queue的数据,解析到vhost_queue的结构体中,QEMU通过vhost_queue将IO请求同步给host kernel的vhost_blk模块中,然后vhost_blk将vhost_queue的IO请求解析成一个bio,submit_io提交一个IO请求给硬件,二话不说,再上个图:

上面这个图不需要更多语言描述,看不懂就私信给我,我是不会回的^_^

广说原理也要有点依据,要依据,分享两个函数:

Fall in kernel

vhost_blk_ioctl

Virtio Queue  ----> Vhost Queue

void vhost_virtqueue_start(struct vhost_dev *dev,

struct VirtIODevice *vdev,

struct vhost_virtqueue *vq,

unsigned idx)

Qemu Vhost Queue ---> Kernel Vhost Queue

int vhost_virtqueue_set_addr(struct vhost_dev *dev,

struct vhost_virtqueue *vq,

unsigned idx, bool enable_log)

第一个函数,不用说,QEMU与vhost-blk都要通过ioctl来完成。

第二个函数和第三个函数,在QEMU中,看懂自然明白。

如果您确实把这三个函数看懂了,我再上个图,你会更清晰。

这个图的左半部就是virtio-blk的机制,当增加了vhost-blk,就增加了右半部。

GuestOS与QEMU之间有个VirtioQ,而QEMU与Vhost-blk之间有个VhostQ,VirtioQ通过QEMU转化成VhostQ,用于与Vhost-Blk同步请求。

当Vhost将请求处理完,再将结果放到VhostQ中的used中后,通知QEMU给GuestOS发送一个中断,Guest中断处理函数的处理方法参考上半部分享。

那么要补充的是,VirtioQ与VhostQ的同步也只是地址的同步,并没有数据的同步,所以VhostQ中的数据,也是VirtioQ中的数据。

目前为止,一个IO请求如何通过vhost-blk提交到硬件层,应该有个大致的了解。

上图又引入两个重要函数:

Host_kick与Guest_kick,在第四章为大家分享。

四. 重要函数

1. Vhost_blk重要函数

vhost_blk_handle_guest_kick:当一个IO请求同步到vhost_blk的vhost_queue中时,vhost-blk会调用此函数,将vhost_queue中的IO请求解析成一个bio,通过submit_io提交给硬件层。注意:这个函数在vhost_blk初始化的时候注册给【work_poll->work->fn】

vhost_blk_handle_host_kick:当一个IO请求处理完毕后,会调用此函数,将IO请求的处理结果同步到vhost_queue中,也就是同步到virtio_queue中,并 通知QEMU触发一个GuestOS的中断,通知GuestOS调用中断处理函数,处理IO返回的结果。注意:这个函数在vhost_blk初始化的时候注册给【blk->work->fn】

红色部分是一个遗留问题,后面为大家讲解如何在内核中通知用户程序的QEMU触发中断的。

先看vhost-blk是如何调用这两个函数的?引入了一个重要的内核线程【Vhost Worker thread】,再上个图:

如上图所示:首先紫色模块vhost_dev_set_owner,创建一个vhost_worker的线程(调用kthread_create),但不是一直运行的,使用通过唤醒函数进行唤醒。

这个线程的主要工作就是不停的在work_list取出之前被注册进来的work,调用work->fn函数,那整个流程如下:

1. 当IO请求被同步到vhost_queue中时,QEMU通过ioctl通知vhost_blk调用vhost_poll_start。

2. vhost_poll_start将poll->work通过vhost_work_queue注册给work_list,并调用唤醒函数唤醒vhost_worker线程。

3. vhost_worker取出work_list头的work,并调用work的fn,这个work->fn就是 vhost_blk_handle_guest_kick。

4. 当vhost_blk_handle_guest_kick后,会调用vhost_blk_req_done。

5. vhost_blk_req_done函数将blk->work通过vhost_work_queue添加到work_list的队尾,并启动进程。

6. 与第四步一样,此时work->fn就是 vhost_blk_handle_host_kick。

2. QEMU中的重要函数

到目前位置,已经描述了Vhost-blk最重要的两个函数,其中 vhost_blk_handle_host_kick在处理完请求后,需要通知QEMU向GuestOS发送一个中断,那么这个通知机制是如何完成的呢?

请看下面两个函数

virtio_queue_host_notifier_read:当GuestOS的virtio_blk把IO请求同步到virtio_queue中时,会调用此函数,此函数实际就是调用handle_output去处理IO请求。

virtio_queue_guest_notifier_read:当Vhost-blk处理完请求时,通过vhost_blk_handle_host_kick发送信号,让Qemu调用此函数,为GuestOS发送一个中断。

这里必须要引入一个概念eventfd。

它是一个系统调用,它会返回一个描述符,描述符实际是一个对象,这个对象包含一个由内核维护的计数器。

当read这个描述符的时候,如果这个描述符的对象计数器大于0,read将会返回,并且将计数器清0。

当write这个描述符的时候,就是将一个uint64的数值写到计数器中。

当poll这个描述符的时候,如果计数器大于0,poll会认为它是可读的,如果计数器等于0,poll认为它是不可读的。(select亦如此)

QEMU中有个轮询io_handler的主循环,还是要贴一个链接:http://blog.csdn.net/zhuriyuxiao/article/details/8835593

简而言之,这个主循环在poll这个io_handler列表中所有的描述符,当poll返回值为真的,就会调用该描述符对应的函数。

说回来,QEMU就是分别创建了一个host_notifier和一个guest_notifier两个eventfd,并将这两个eventfd绑定上面的两个函数:

virtio_queue_host_notifier_read -> host_notifier

virtio_queue_guest_notifier_read -> guest_notifier

并将这两个eventfd注册到io_handler里面。

最关键的是,QEMU将host_notifier通过ioctl注册给内核vhost-blk中的kick。将guest_notifer通过ioctl注册给内核vhost-blk中的call。

到这,大家是不是看出点门道,当在内核vhost-blk中,给call的计数器做+1操作,QEMU中的guest_notifier就会被poll认为是可读的,io_handler主循环中就会调用 virtio_queue_guest_notifier_read给GuestOS发中断。

那么vhost-blk的kick又是干什么的呢?

每次vhost-blk启动vhost_worker线程之前,都要检测一下kick是不是可读,如果可读,才会启动线程,处理vhost_queue中的IO请求,而在QEMU中的 virtio_queue_host_notifier_read函数中,调用handle_output之前,需要将host_notifier的计数器清零,言外之意,就是QEMU中的handle_output与vhost-blk的 vhost_blk_handle_guest_kick不能同时进行,因为他们都会操作vhost_queue,这是保证一个vhost_queue处理的完整性。

五. 总结

总结必不可少,那就是vhost-blk到底优化空间又多少,先把两张老图拿出来对比下:

第一张图是没有vhos-blk架构图,红色箭头发出一次IO请求,要走完第三张图的vfs层一直到Block Device层,而请求处理完毕后,要返回到QEMU,将IO请求的处理结果填充到Virtio_queue中,再触发中断,让GuestOS中断处理函数对IO请求的结果进行处理。

第二张图是有vhost-blk的架构图,与红色箭头处对应的是,直接跳过其他层,到Block Device Driver层,提交IO请求,当IO请求的结果到Vhost-blk驱动中,在block device driver层将请求结果填充到vhost_queue中,就通知QEMU触发一次中断,让GuestOS中断处理函数对IO请求结果进行处理。

也就是说开启vhost-blk的IO路径 从请求发出到请求结果返回,两次都有缩短。

但是!!但是优化空间到底有多少?

在上文提到过,vhost-blk开启后,QEMU后端必须是block设备(可以是逻辑卷LV),那么上面第三个图,表示QEMU后端为一个Raw文件的IO路径。

这样是不在一个基准线上的竞争,假如不开启vhost-blk,QEMU的virtio-blk后端为一个LV的话, 我们知道LVM是建立在硬盘和分区之上的一个逻辑层,接近于驱动层。

大家应该明白我的结论,我接触QEMU不久,而且以上都是基于代码分析出来的,没有官网给出结论,所以有任何不同意见,欢迎提出来,大家一起把这个社区没接受的 非官方的 大家都好奇的vhost-blk搞清楚。

virtio_queue_set_guest_notifier_fd_handler

host是virtio的另一种方案,用于跳过qemu,减少qemu和内核之间上下文切换的开销,对于网络IO而言提升尤其明显。vhost目前有两种实现方案,内核态和用户态,本文重点讨论内核态的vhost

vhost内核模块主要处理数据面的事情,控制面上还是交给qemu,

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

host到guest方向

-> r = k->set_guest_notifiers(qbus->parent, total_queues * 2, true); //调用virtio_pci_set_guest_notifiers

1、-> virtio_pci_set_guest_notifiers --> kvm_vm_ioctl

2、 -> virtio_pci_set_guest_notifier-> virtio_queue_set_guest_notifier_fd_handler

1、有中断irqfd通过kvm_vm_ioctl来设置kvm模块的irqfd

首先host处理used ring,然后判断如果KVM_IRQFD成功设置,kvm模块会通过irqfd把中断注入guest。qemu是通过virtio_pci_set_guest_notifiers -> kvm_virtio_pci_vector_use -> kvm_virtio_pci_irqfd_use -> kvm_irqchip_add_irqfd_notifier ->kvm_irqchip_assign_irqfd最终调用kvm_vm_ioctl来设置kvm模块的irqfd的,包含write fd和read fd(可选)

static int kvm_virtio_pci_vector_use(VirtIOPCIProxy *proxy, intnvqs)

{

PCIDevice*dev = &proxy->pci_dev;

VirtIODevice*vdev = virtio_bus_get_device(&proxy->bus);

VirtioDeviceClass*k =VIRTIO_DEVICE_GET_CLASS(vdev);

unsignedintvector;intret, queue_no;

MSIMessage msg;for (queue_no = 0; queue_no < nvqs; queue_no++) {if (!virtio_queue_get_num(vdev, queue_no)) {break;

}

vector=virtio_queue_vector(vdev, queue_no);if (vector >=msix_nr_vectors_allocated(dev)) {continue;

}

msg=msix_get_message(dev, vector);

ret=kvm_virtio_pci_vq_vector_use(proxy, queue_no, vector, msg);if (ret < 0) {gotoundo;

}/*If guest supports masking, set up irqfd now.

* Otherwise, delay until unmasked in the frontend.*/

if (k->guest_notifier_mask) {

ret=kvm_virtio_pci_irqfd_use(proxy, queue_no, vector);if (ret < 0) {

kvm_virtio_pci_vq_vector_release(proxy, vector);gotoundo;

}

}

}return 0;

undo:while (--queue_no >= 0) {

vector=virtio_queue_vector(vdev, queue_no);if (vector >=msix_nr_vectors_allocated(dev)) {continue;

}if (k->guest_notifier_mask) {

kvm_virtio_pci_irqfd_release(proxy, queue_no, vector);

}

kvm_virtio_pci_vq_vector_release(proxy, vector);

}returnret;

}

2、如果没有设置irqfd,则guest notifier fd会通知到等待fd的qemu进程,进入注册函数virtio_queue_guest_notifier_read,调用virtio_irq,最终调用到virtio_pci_notify

static void virtio_queue_guest_notifier_read(EventNotifier *n)

{

VirtQueue*vq =container_of(n, VirtQueue, guest_notifier);if(event_notifier_test_and_clear(n)) {

virtio_irq(vq);

}

}void virtio_irq(VirtQueue *vq)

{

trace_virtio_irq(vq);

vq->vdev->isr |= 0x01;

virtio_notify_vector(vq->vdev, vq->vector);

}static void virtio_notify_vector(VirtIODevice *vdev, uint16_t vector)

{

BusState*qbus =qdev_get_parent_bus(DEVICE(vdev));

VirtioBusClass*k =VIRTIO_BUS_GET_CLASS(qbus);if (k->notify) {

k->notify(qbus->parent, vector);

}

}static void virtio_pci_notify(DeviceState *d, uint16_t vector)

{

VirtIOPCIProxy*proxy =to_virtio_pci_proxy_fast(d);if (msix_enabled(&proxy->pci_dev))

msix_notify(&proxy->pci_dev, vector);else{

VirtIODevice*vdev = virtio_bus_get_device(&proxy->bus);

pci_set_irq(&proxy->pci_dev, vdev->isr & 1);

}

}

1 技术简介

1.1 virtio-net 简介

virtio-net 在 guest 前端驱动 kick 后端驱动时,采用 I/O 指令方式退出到 host KVM。kvm 通过 eventfd_signal 唤醒阻塞的 qemu 线程。qemu 通过 vring 处理报文。qemu 把报文从用户态传送给 tap 口。

1.2 vhost-net 简介

与 virtio-net 不同的是,eventfd_signal 唤醒的是内核 vhost_worker 进程。vhost_worker 从 vring 提取报文数据,然后发送给 tap。与 virtio-net 相比,vhost-net 处理数据在内核态,在发送到 tap 口的时候少了一次数据的拷贝。

1.3 ovs 转发涉及的模块概要

VM->VM 流程:

2 virtio-net.ko 前端驱动部分

2.1 guest->host 数据发送

当前端 virtio-net 有想发送的报文数据时将会 kick 后端,右面是前端 kick 后端的流程。前端调用 xmit_skb 发送数据,virtqueue_add_outbuf 是把 sk_buff 里的内容(frag[]数组)逐一的填入 scatterlist 数组中。这里可以理解成填写分散聚合描述符表。

但前端和后端数据传递是通过 struct vring_desc 传递的,所以 virtqueue_add() 再把 struct scatterlist 里的数据填写到 struct vring_desc 里。

struct vring_desc 这个数据结构的使用,后面我们再详细说。

最后通过 vq->notify(&vq->vq) (vp_notify()) kick 后端,后续流程到了 kvm.ko 部分的第 4 小节。

2.2 guest->host 代码流程

2.3 host->guest 数据发送

guest 通过 NAPI 接口的 virtnet_poll 接收数据,通过 virtqueue_get_buf_ctx 从 Vring 中获取报文数据。再通过 receive_buf 把报文数据保存到 skb 中。

这样目的端就成功接收了来自源端的报文。

2.4 host->guest 代码流程

3 kvm.ko 部分

3.1 eventfd 注册

由上图可见 eventfd 的注册是在 qemu 中发起的。qemu 调用 kvm 提供的系统调用。

3.2 eventfd 通知流程

eventfd 一半的用法是用户态通知用户态,或者内核态通知用户态。例如 virtio-net 的实现是 guest 在 kick host 时采用 eventfd 通知的 qemu,然后 qemu 在用户态做报文处理。但 vhost-net 是在内核态进行报文处理,guest 在 kick host 时采用 eventfd 通知的是内核线程 vhost_worker。所以这里的用法就跟常规的 eventfd 的用法不太一样。

下面介绍 eventfd 通知的使用。

eventfd 核心数据结构:

structeventfd_ctx {

structkref kref;

wait_queue_head_twqh;

__u64 count;

unsignedintflags;

};

eventfd 的数据结构其实就是包含了一个等待队列头。当调用 eventfd_signal 函数时就是唤醒 wgh 上等待队列。

__u64 eventfd_signal(structeventfd_ctx *ctx,__u64 n)

{

unsignedlongflags;

spin_lock_irqsave(&ctx->wqh.lock,flags);

if(ULLONG_MAX -ctx->count

n =ULLONG_MAX -ctx->count;

ctx->count +=n;

if(waitqueue_active(&ctx->wqh))

wake_up_locked_poll(&ctx->wqh,POLLIN);

spin_unlock_irqrestore(&ctx->wqh.lock,flags);

returnn;

}

#definewake_up_locked_poll(x,m)\

__wake_up_locked_key((x),TASK_NORMAL,(void*)(m))

void__wake_up_locked_key(structwait_queue_head *wq_head,unsignedintmode,void*key)

{

__wake_up_common(wq_head,mode,1,0,key,NULL);

}

staticint__wake_up_common(structwait_queue_head *wq_head,unsignedintmode,

intnr_exclusive,intwake_flags,void*key,

wait_queue_entry_t*bookmark)

{

...

list_for_each_entry_safe_from(curr,next,&wq_head->head,entry){

unsignedflags =curr->flags;

intret;

if(flags &WQ_FLAG_BOOKMARK)

continue;

ret =curr->func(curr,mode,wake_flags,key);/* 调用vhost_poll_wakeup */

if(ret <0)

break;

if(ret &&(flags &WQ_FLAG_EXCLUSIVE)&&!--nr_exclusive)

break;

if(bookmark &&(++cnt >WAITQUEUE_WALK_BREAK_CNT)&&

(&next->entry !=&wq_head->head)){

bookmark->flags =WQ_FLAG_BOOKMARK;

list_add_tail(&bookmark->entry,&next->entry);

break;

}

}

returnnr_exclusive;

}

staticintvhost_poll_wakeup(wait_queue_entry_t*wait,unsignedmode,intsync,

void*key)

{

structvhost_poll *poll =container_of(wait,structvhost_poll,wait);

if(!((unsignedlong)key &poll->mask))

return0;

vhost_poll_queue(poll);

return0;

}

voidvhost_poll_queue(structvhost_poll *poll)

{

vhost_work_queue(poll->dev,&poll->work);

}

voidvhost_work_queue(structvhost_dev *dev,structvhost_work *work)

{

if(!dev->worker)

return;

if(!test_and_set_bit(VHOST_WORK_QUEUED,&work->flags)){

/* We can only add the work to the list after we're

* sure it was not in the list.

* test_and_set_bit() implies a memory barrier.

*/

llist_add(&work->node,&dev->work_list);/* 添加到 dev->work_list)*/

wake_up_process(dev->worker);/* 唤醒vhost_worker线程 */

}

}

这里有一个疑问,就是 vhost_worker 什么时候加入到 eventfd 的 wgh 字段的,__wake_up_common 函数里 curr->func 又是什么时候被设置成 vhost_poll_wakeup 函数的呢?请看下一节。

3.3 eventfd 与 vhost_worker 绑定

vhost.ko 创建了一个字符设备,vhost_net_open 在打开这个设备文件的时候会调用 vhost_net_open 函数。这里为 vhost_dev 设备进行初始化。

staticintvhost_net_open(structinode *inode,structfile *f)

{

...

dev =&n->dev;

vqs[VHOST_NET_VQ_TX]=&n->vqs[VHOST_NET_VQ_TX].vq;

vqs[VHOST_NET_VQ_RX]=&n->vqs[VHOST_NET_VQ_RX].vq;

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);

f->private_data =n;

return0;

}

voidvhost_poll_init(structvhost_poll *poll,vhost_work_fn_tfn,

unsignedlongmask,structvhost_dev *dev)

{

init_waitqueue_func_entry(&poll->wait,vhost_poll_wakeup);/* 给curr->fn赋值 vhost_poll_wakeup */

init_poll_funcptr(&poll->table,vhost_poll_func);/* 给poll_table->_qproc赋值vhost_poll_func */

poll->mask =mask;

poll->dev =dev;

poll->wqh =NULL;

vhost_work_init(&poll->work,fn);/* 给 work->fn 赋值为handle_tx_net和handle_rx_net */

}

qemu 使用 ioctl 系统调用 VHOST_SET_VRING_KICK 时会把 eventfd 的 struct file 指针付给 pollstart 和 pollstop,同时调用 vhost_poll_start()。

longvhost_vring_ioctl(structvhost_dev *d,intioctl,void__user *argp)

{

...

caseVHOST_SET_VRING_KICK:

if(copy_from_user(&f,argp,sizeoff)){

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){

pollstop =(filep =vq->kick)!=NULL;

pollstart =(vq->kick =eventfp)!=NULL;

}else

filep =eventfp;

break;

...

if(pollstart &&vq->handle_kick)

r =vhost_poll_start(&vq->poll,vq->kick);

...

}

intvhost_poll_start(structvhost_poll *poll,structfile *file)

{

unsignedlongmask;

intret =0;

if(poll->wqh)

return0;

mask =file->f_op->poll(file,&poll->table);/* 执行eventfd_poll */

if(mask)

vhost_poll_wakeup(&poll->wait,0,0,(void*)mask);

if(mask &POLLERR){

vhost_poll_stop(poll);

ret =-EINVAL;

}

returnret;

}

staticunsignedinteventfd_poll(structfile *file,poll_table *wait)

{

structeventfd_ctx *ctx =file->private_data;

unsignedintevents =0;

u64 count;

poll_wait(file,&ctx->wqh,wait);

。。。

}

staticinlinevoidpoll_wait(structfile *filp,wait_queue_head_t*wait_address,poll_table *p)

{

if(p &&p->_qproc &&wait_address)

p->_qproc(filp,wait_address,p);/* 调用vhost_poll_func */

}

staticvoidvhost_poll_func(structfile *file,wait_queue_head_t*wqh,

poll_table *pt)

{

structvhost_poll *poll;

poll =container_of(pt,structvhost_poll,table);

poll->wqh =wqh;

add_wait_queue(wqh,&poll->wait);

}

关键数据结构关系如下图:

3.4 guest->host 的通知流程(唤醒 vhost_worker 线程)

Kick host 的原理是通过 io 指令实现的。前端执行 io 指令,就会发生 vm exit。KVM 捕捉到 vm exit 会去查询退出原因,由于是 io 指令,所以执行对应的 handle_io 处理。handle_io() 从 exit_qualification 中得到 io 操作地址。kvm_fast_pio_out() 会根据 io 操作的地址找到对应的处理函数。第 1 小节 eventfd 注册的流程可知,kvm_fast_pio_out() 最终会调用 eventfd 对应的回调函数 ioeventfd_write()。再根据第 3 小节可知 eventfd 最终会唤醒 vhost_worker 内核进程。

流程进入 vhost.ko 的第3小节。

3.5 host 给 guest 注入中断

到目前位置,发送给 guest 的报文已经准备好了。通过注入中断通知 guest 接收报文。这里要为虚机的 virtio-net 设备模拟一个 MSI 中断,并且准备了中断向量号。调用 vmx_deliver_posted_interrupt 给目的 VCPU 线程所在的物理核注入终端。

流程将跳转到 virtio-net.ko 前端驱动的第3小节。

3.6 host 给 guest 注入中断代码流程 ------------------------非host方式通过virtio_notify_irqfd

4 vhost.ko 部分

前面有提到 vhost_worker 线程被唤醒后将执行 vhost_poll_init() 函数这册的 handle_tx_net 和 handle_rx_net 函数。

4.1 vhost_worker 线程创建

longvhost_dev_set_owner(structvhost_dev *dev)

{

...

/* No owner, become one */

dev->mm =get_task_mm(current);

worker =kthread_create(vhost_worker,dev,"vhost-%d",current->pid);

if(IS_ERR(worker)){

err =PTR_ERR(worker);

gotoerr_worker;

}

dev->worker =worker;

wake_up_process(worker);/* avoid contributing to loadavg */

err =vhost_attach_cgroups(dev);

if(err)

gotoerr_cgroup;

err =vhost_dev_alloc_iovecs(dev);

if(err)

gotoerr_cgroup;

...

}

让 vhost-dev 的 worker 指向刚创建出的 worker 线程。

4.2 vhost_worker 实现

staticintvhost_worker(void*data)

{

structvhost_dev *dev =data;

structvhost_work *work,*work_next;

structllist_node *node;

mm_segment_toldfs =get_fs();

set_fs(USER_DS);

use_mm(dev->mm);

for(;;){

/* mb paired w/ kthread_stop */

set_current_state(TASK_INTERRUPTIBLE);

if(kthread_should_stop()){

__set_current_state(TASK_RUNNING);

break;

}

node =llist_del_all(&dev->work_list);/*vhost_work_queue 添加 */

if(!node)

schedule();

node =llist_reverse_order(node);

/* make sure flag is seen after deletion */

smp_wmb();

llist_for_each_entry_safe(work,work_next,node,node){

clear_bit(VHOST_WORK_QUEUED,&work->flags);

__set_current_state(TASK_RUNNING);

work->fn(work);/* 由vhost_poll_init赋值 handle_tx_net和handle_rx_net*/

if(need_resched())

schedule();

}

}

unuse_mm(dev->mm);

set_fs(oldfs);

return0;

}

从代码可以看到在循环的开始部分是摘除 dev->work_list 链表中的头表项。这里如果链表为空则返回 NULL,如果链表不为空则返回头结点。如果链表为空则调用 schedule() 函数 vhost_worker 进程进入阻塞状态,等待被唤醒。

当 vhost_worker 被唤醒后将执行 fn 函数,对于 vhost-net 将被赋值为 handle_tx_net 和 handle_rx_net。

4.3 从 guest->host 方向的发送报文函数 handle_tx_net

handle_tx_net 的代码逻辑比较短,里面直接调用了 tun.ko 的接口函数发送报文。流程走到了 tun.ko 章节的第1小节。

staticvoidhandle_tx_net(structvhost_work *work)

{

structvhost_net *net =container_of(work,structvhost_net,

poll[VHOST_NET_VQ_TX].work);

handle_tx(net);

}

staticvoidhandle_tx(structvhost_net *net)

{

...

for(;;){

...

/* TODO: Check specific error and bomb out unless ENOBUFS? */

err =sock->ops->sendmsg(sock,&msg,len);/* tup.c中定义 tup_sendmsg ()*/

if(unlikely(err <0)){

...

}

out:

mutex_unlock(&vq->mutex);

}

4.4 guest->host 代码流程

4.5 从 host->guest 方向的接收

vhost-worker 进程调用 handle_rx_net。vhost_add_used_and_signal_n 负责从 vring 中接收报文,vhost_signal 函数通知 guest 报文的到来。目前都是通过注入中断的方式通知 guest。 流程将跳转到 kvm.ko 的第5小节。

4.6 host->guest 代码流程

host->guest 方向:

5 tun.ko 部分

5.1 报文发送处理流程

tun 模块首先通过调用 __napi_schedue() 接口去挂起 NET_RX_SOFTIRQ 软中断的,并且调度的是 sd->backlog 这个 struct napi。然后在 tun_rx_batched() 函数在使能中断下半部时会调用 do_softirq(),从而执行刚刚挂起的 NET_RX_SOFTIRQ 对应的 net_rx_action 软中断响应函数 net_rx_aciton。net_rx_action 会执行 sd->backlog 对应的 napi 接口函数。process_backlog 是内核的 netdev 在初始化时在每 CPU 变量中填入的 struct napi_struct 结构体。最后从 process_backlog 执行到 openvswitch 注册的 hook 函数 netdev_frame_hook (openvswitch.ko 第 2小节)。

流程将跳转到 openvswitch.ko 第3小节。

5.2 process_backlog 的注册

staticint__init net_dev_init(void)

{

inti,rc =-ENOMEM;

for_each_possible_cpu(i){/* 遍历各个CPU的每CPU变量 */

structwork_struct *flush =per_cpu_ptr(&flush_works,i);

structsoftnet_data *sd =&per_cpu(softnet_data,i);/* sd是个每CPU变量 */

INIT_WORK(flush,flush_backlog);

skb_queue_head_init(&sd->input_pkt_queue);

skb_queue_head_init(&sd->process_queue);

INIT_LIST_HEAD(&sd->poll_list);

sd->output_queue_tailp =&sd->output_queue;

#ifdefCONFIG_RPS

sd->csd.func =rps_trigger_softirq;

sd->csd.info =sd;

sd->cpu =i;

#endif

sd->backlog.poll =process_backlog;/* 定义napi_struct的poll函数 */

sd->backlog.weight =weight_p;

}

if(register_pernet_device(&loopback_net_ops))

gotoout;

if(register_pernet_device(&default_device_ops))

gotoout;

open_softirq(NET_TX_SOFTIRQ,net_tx_action);/* 设置软中断NET_TX_SOFTIRQ的响应函数 */

open_softirq(NET_RX_SOFTIRQ,net_rx_action);/*设置软中断NET_RX_SOFTIRQ的响应函数 */

rc =cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD,"net/dev:dead",

NULL,dev_cpu_dead);

WARN_ON(rc <0);

rc =0;

out:

returnrc;

}

6 openvswitch 部分

openvswitch.ko 作为 openvswitch 的一个内核模块内核态报文的接收和转发。通过给 tun 设备挂接 hook 函数,来处理 tun 接收和发送的报文。在创建虚机时给虚机分配的 vnet 口会暴露给 host,我们一般通过 xml 文件指定到桥入那个 ovs 网桥。在桥入的时候,用户态代码通过 netlink 与 openvswitch.ko 进行通信。把 vnet 口桥入 ovs 网桥时会给 vnet 这个设备挂 netdev_frame_hook 钩子函数。

6.1 netlink 注册

当 ovs 添加一个 vport 时会通过 netlink 发送到 openvswitch.ko,openvswitch 注册的 netlink 处理函数负责处理相关命令。

staticstructgenl_ops dp_vport_genl_ops[]={

{.cmd =OVS_VPORT_CMD_NEW,

.flags =GENL_UNS_ADMIN_PERM,/* Requires CAP_NET_ADMIN privilege. */

.policy =vport_policy,

.doit =ovs_vport_cmd_new /* OVS_VPORT_CMD_NEW消息的 */

},

{.cmd =OVS_VPORT_CMD_DEL,

.flags =GENL_UNS_ADMIN_PERM,/* Requires CAP_NET_ADMIN privilege. */

.policy =vport_policy,

.doit =ovs_vport_cmd_del

},

{.cmd =OVS_VPORT_CMD_GET,

.flags =0,/* OK for unprivileged users. */

.policy =vport_policy,

.doit =ovs_vport_cmd_get,

.dumpit =ovs_vport_cmd_dump

},

{.cmd =OVS_VPORT_CMD_SET,

.flags =GENL_UNS_ADMIN_PERM,/* Requires CAP_NET_ADMIN privilege. */

.policy =vport_policy,

.doit =ovs_vport_cmd_set,

},

};

structgenl_family dp_vport_genl_family __ro_after_init ={

.hdrsize =sizeof(structovs_header),

.name =OVS_VPORT_FAMILY,

.version =OVS_VPORT_VERSION,

.maxattr =OVS_VPORT_ATTR_MAX,

.netnsok =true,

.parallel_ops =true,

.ops =dp_vport_genl_ops,

.n_ops =ARRAY_SIZE(dp_vport_genl_ops),

.mcgrps =&ovs_dp_vport_multicast_group,

.n_mcgrps =1,

.module=THIS_MODULE,

};

staticstructgenl_family *dp_genl_families[]={

&dp_datapath_genl_family,

&dp_vport_genl_family,

&dp_flow_genl_family,

&dp_packet_genl_family,

&dp_meter_genl_family,

};

staticint__init dp_register_genl(void)

{

interr;

inti;

for(i =0;i

err =genl_register_family(dp_genl_families[i]);注册netlink处理函数

if(err)

gotoerror;

}

return0;

error:

dp_unregister_genl(i);

returnerr;

}

6.2 netdev_frame_hook 函数的注册

6.3 ovs 对报文的转发流程

OVS 首先通过 key 值找到对应的流表,然后转发到对应的端口。这篇文章的重点是讲解 vhost 的流程,OVS 具体流程并不是我们的讲解的重点。所以这方面有什么疑问请大家自行搜索一下 OVS 的资料。

这段代码的大体目的就是找到目的虚机所在的端口,也就是目的虚机所在的 vnet 端口。

流程跳转到内核部分第1小节。

7 内核部分

7.1 发送报文唤醒目的端的 vhost-worker 进程

内核的发送函数 __dev_queue_xmit 将会找到 vnet 设备对应的等待队列,并唤醒等待队列里对应的进程。这里将唤醒的进程就是 vhost_worker 进程了。

流程跳转到 vhost.ko 的第5小节。

7.2 代码流程

block中断 virtio_virtio event fd + 中断 前后端通信机制 +class_init {vhost worker方式}(二)...相关推荐

  1. 从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 二十三║Vue实战:Vuex 其实很简单

    前言 哈喽大家周五好,马上又是一个周末了,下周就是中秋了,下下周就是国庆啦,这里先祝福大家一个比一个假日嗨皮啦~~转眼我们的专题已经写了第 23 篇了,好几次都坚持不下去想要中断,不过每当看到群里的交 ...

  2. 如何使用websocket实现前后端通信

    如何使用websocket实现前后端通信 websocket通信是很好玩的,也很有用的的通信方式,使用方式如下: 第一步由于springboot很好地集成了websocket,所以先在在pom.xml ...

  3. 05-ET框架的前后端通信1

    TIPS: 本系列贴仅用于博主学习ET框架的记录 文章目录 前言 一.ET框架的前后端如何进行通信的? 二.前后端的通信使用 1.编写protobuf消息体 2.编写C#代码 3.运行结果 总结 前言 ...

  4. 解决Vue前后端跨域问题的多种方式

    1 前言 本文主要介绍借助解决Vue前后端跨域问题的几种方式 说到ajax请求,就不得不说下xhr(XMLHttpRequest)了,它可以说是鼻祖,但是实际开发中,我们不会直接使用它,而是进行二次封 ...

  5. 【基于SSH框架的个人博客系统06】头像文件上传与前后端分页机制

    注意:本项目为博主初学Web开发时所写,所使用的方法都比较笨,不符合主流开发方法.例如,包管理应该使用Maven进行管理而不是手动导入,对前端后端代码的架构也并不是很清晰.大家学习思想即可,可以不用浪 ...

  6. 06-ET框架的前后端通信2

    TIPS: 本系列贴仅用于博主学习ET框架的记录 文章目录 前言 一.IMessage是什么? 二.使用步骤 1.编写proto消息体 2.编写C#代码 3.运行结果 总结 前言 这篇文章记录ET框架 ...

  7. 前后端通信,这中间到底发生了什么?

    前两天面试,面试官问了我一个问题,刨根到底的问,把我问的快哭了,事后回来赶紧补功课! 面试官:前后端是怎么通信的 我: 通过http发送一个请求,根据请求的地址,找到对应的SQL,返回最终的数据 面试 ...

  8. Flask+Axios+jQuery构建前后端通信的小例子

    比较暴力但好理解的方法,下面详细说一下. 工具准备 Flask pip install flask Axios https://cdnjs.cloudflare.com/ajax/libs/axios ...

  9. 从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 二十║Vue基础终篇:组件详解+项目说明...

    缘起 新的一天又开始啦,大家也应该看到我的标题了,是滴,Vue基础基本就到这里了,咱们回头看看这一路,如果你都看了,并且都会写了,那么现在你就可以自己写一个Demo了,如果再了解一点路由,ajax请求 ...

最新文章

  1. 力扣(LeetCode)刷题,简单题(第27期)
  2. php连接数据库提示could not find driver 问题解决
  3. 网络游戏中用到哪些计算机技术,美术设计中计算机技术的应用论文
  4. [Java] 蓝桥杯ADV-175 算法提高 三个整数的排序
  5. Python九九乘法表(作业)
  6. 二维联合分布(X,Y)求(U,V)
  7. dev chart 绘制图形
  8. Denoise Autoencoder
  9. PHP管理虚拟机,用phpvirtualbox管理vbox虚拟机
  10. 宝塔面板+小鸟云服务器+wordpress建站使用
  11. 怎样把PDF格式转换成可编辑的PPT幻灯片?
  12. 合同管理数据库设计mysql_工程合同管理信息系统的数据设计理念
  13. seo和sem是什么意思
  14. HM编码器代码阅读(30)——帧间预测之AMVP模式(五)运动估计
  15. 《神经网络与深度学习》中文版推荐
  16. 使用NOWSMS搭建自己的彩信中心- -
  17. android6.0 全局4k,Android6.0全局4K大法好?索尼Z5P升级体验
  18. Nilsson's sequence score算法解决八数码问题解释
  19. 使用微软Office组件读取Excel文件
  20. InfoQ Qclub参与活动感悟

热门文章

  1. 今日剪辑妙招分享:剪辑抖音短视频可以用哪些工具剪辑?
  2. SecureCRT使用(转)
  3. Vscode配置gopath
  4. JavaScript仿win8高端滑动旋转效果
  5. JavaScript什么是原型和原型链JS
  6. 训练仿真系统软件开发公司有哪些呢
  7. 面渣逆袭:Java基础五十三问,快来看看有没有你不会的
  8. 网游代练向欧美市场渗透:代练员月入5000元
  9. Istio微服务治理网格的全方面可视化监控(微服务架构展示、资源监控、流量监控、链路监控)
  10. IoT黑板报:中移动净利润是电信联通总和6倍