一、技术简介

1.1 virtio-net简介

virtio-net在guest前端驱动kick对其驱动时,采用I/O指令方式退出到主机kvm。kvm通过eventfd_signal重新交换的qemu线程。qemu通过vring处理报文。qemu将报文从用户态传送给tap口。

1.2 vhost-net简介

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

1.3 ovs转发涉及的模块概要

VM-> VM流程:

二、virtio-net.ko前端驱动部分

2.1 guest->主机数据发送

当前端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->主机代码流程

2.3 host-> guest数据发送

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

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

2.4 host-> guest代码流程

三、kvm.ko部分

3.1 eventfd注册


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

3.2 eventfd通知流程

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

下面介绍eventfd通知的使用。

eventfd核心数据结构:

struct eventfd_ctx {struct kref kref;wait_queue_head_t wqh;__u64 count;unsigned int flags;
};

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

__u64 eventfd_signal(struct eventfd_ctx *ctx, __u64 n)
{unsigned long flags;spin_lock_irqsave(&ctx->wqh.lock, flags);if (ULLONG_MAX - ctx->count < n)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);return n;
}
#define wake_up_locked_poll(x, m)                       \__wake_up_locked_key((x), TASK_NORMAL, (void *) (m))
void __wake_up_locked_key(struct wait_queue_head *wq_head, unsigned int mode, void *key)
{__wake_up_common(wq_head, mode, 1, 0, key, NULL);
}
static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,int nr_exclusive, int wake_flags, void *key,wait_queue_entry_t *bookmark)
{...list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {unsigned flags = curr->flags;int ret;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;}}return nr_exclusive;
}
static int vhost_poll_wakeup(wait_queue_entry_t *wait, unsigned mode, int sync,void *key)
{struct vhost_poll *poll = container_of(wait, struct vhost_poll, wait);if (!((unsigned long)key & poll->mask))return 0;vhost_poll_queue(poll);return 0;
}
void vhost_poll_queue(struct vhost_poll *poll)
{vhost_work_queue(poll->dev, &poll->work);
}
void vhost_work_queue(struct vhost_dev *dev, struct vhost_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设备进行初始化。

static int vhost_net_open(struct inode *inode, struct file *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;return 0;
}
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);  /* 给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()。

long vhost_vring_ioctl(struct vhost_dev *d, int ioctl, void __user *argp)
{...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) {pollstop = (filep = vq->kick) != NULL;pollstart = (vq->kick = eventfp) != NULL;} elsefilep = eventfp;break;
...if (pollstart && vq->handle_kick)r = vhost_poll_start(&vq->poll, vq->kick);
...
}
int vhost_poll_start(struct vhost_poll *poll, struct file *file)
{unsigned long mask;int ret = 0;if (poll->wqh)return 0;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;}return ret;
}
static unsigned int eventfd_poll(struct file *file, poll_table *wait)
{struct eventfd_ctx *ctx = file->private_data;unsigned int events = 0;u64 count;poll_wait(file, &ctx->wqh, wait);...
}
static inline void poll_wait(struct file * 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 */
}
static void vhost_poll_func(struct file *file, wait_queue_head_t *wqh,poll_table *pt)
{struct vhost_poll *poll;poll = container_of(pt, struct vhost_poll, table);poll->wqh = wqh;add_wait_queue(wqh, &poll->wait);
}

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

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

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

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

3.5 主机给guest注入中断

通过注入中断通知guest接收报文。这里要为虚拟机的virtio-net设备模拟一个MSI中断,并且准备了中断向量号。调用vmx_deliver_posted_interrupt给目的VCPU所在的物理核注入中断。

流程将逐渐到达virtio-net.ko前端驱动的第3小节。

3.6 主机给guest注入中断代码流程

四、vhost.ko部分

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

4.1 vhost_worker线程创建

long vhost_dev_set_owner(struct vhost_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);goto err_worker;}dev->worker = worker;wake_up_process(worker);  /* avoid contributing to loadavg */err = vhost_attach_cgroups(dev);if (err)goto err_cgroup;err = vhost_dev_alloc_iovecs(dev);if (err)goto err_cgroup;...
}

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

4.2 vhost_worker实现

static int vhost_worker(void *data)
{struct vhost_dev *dev = data;struct vhost_work *work, *work_next;struct llist_node *node;mm_segment_t oldfs = 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);return 0;
}

代码从可以看到在循环的开始部分的摘除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小节。

static void handle_tx_net(struct vhost_work *work)
{struct vhost_net *net = container_of(work, struct vhost_net,poll[VHOST_NET_VQ_TX].work);handle_tx(net);
}
static void handle_tx(struct vhost_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->主机代码流程

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 方向:

五、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 的注册

static int __init net_dev_init(void)
{int i, rc = -ENOMEM;
…for_each_possible_cpu(i) {                                /* 遍历各个CPU的每CPU变量 */struct work_struct *flush = per_cpu_ptr(&flush_works, i);struct softnet_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;
#ifdef CONFIG_RPSsd->csd.func = rps_trigger_softirq;sd->csd.info = sd;sd->cpu = i;
#endifsd->backlog.poll = process_backlog;      /* 定义napi_struct的poll函数 */sd->backlog.weight = weight_p;}
…if (register_pernet_device(&loopback_net_ops))goto out;if (register_pernet_device(&default_device_ops))goto out;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:return rc;
}

六、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 处理函数负责处理相关命令。

static struct genl_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,},
};
struct genl_family dp_vport_genl_family __ro_after_init = {.hdrsize = sizeof(struct ovs_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,
};
static struct genl_family *dp_genl_families[] = {&dp_datapath_genl_family,&dp_vport_genl_family,&dp_flow_genl_family,&dp_packet_genl_family,&dp_meter_genl_family,
};
static int __init dp_register_genl(void)
{int err;int i;for (i = 0; i < ARRAY_SIZE(dp_genl_families); i++) {err = genl_register_family(dp_genl_families[i]);    注册netlink处理函数if (err)goto error;}return 0;
error:dp_unregister_genl(i);return err;
}

6.2 netdev_frame_hook 函数的注册

6.3 ovs 对报文的转发流程

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

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

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

七、内核部分

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

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

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

7.2 代码流程

内核部分代码流程

原文链接:http://tinylab.org/qemu-vhost/

OVS之vhost-net中VM通信(十)相关推荐

  1. OVS之vhost-net中VM通信(九)

    一.版本说明 qemu版本:2.6.0 内核版本:3.10.102 二.简而言之 vhost模块需要提前加载,注册一个misc的设备,供虚拟机启动时候使用. 虚拟机创建的时候,会初始化一个tap设备, ...

  2. OVS DPDK vhost-user搭建全过程(四十四)

    系统:ubuntu14.04.1 系统:centos7 内核:4.4.0 qemu : >=2.7 Install DPDK 1. Download DPDK cd /usr/src/ wget ...

  3. 转:通信十年:前辈对通信行业的分析与经历

    离开通信行业已经有一段日子了.换了行业之后,慢慢的淡忘了通信的各种艰辛和酸甜苦辣. 前天晚上,突然在梦中回到以前做移动通信(核心网)技术支持的日子,回到了7*12小时工作,每天提着笔记本,没有一天(包 ...

  4. 快速了解Kubernetes微服务中的通信

    by Adam Henson 亚当·汉森(Adam Henson) 快速了解Kubernetes微服务中的通信 (A quick look at communication in Kubernetes ...

  5. C# 中串口通信 serialport1.DataReceived 函数无法触发或者出发延时等等问题解决方法

    C# 中串口通信 serialport1.DataReceived 函数无法触发或者出发延时等等问题解决方法 参考文章: (1)C# 中串口通信 serialport1.DataReceived 函数 ...

  6. Xcode 7中http通信出现如下错误

    Xcode 7中http通信出现如下错误:Application Transport Security has blocked a cleartext HTTP (http://) resource ...

  7. qint64转为qstring qt_Qt项目中TCP通信的实现方式经验总结(服务端部分)

    总第20篇 本文接第19篇,继续梳理TCP通信过程中的重要知识.本文主要系统地讲解通信服务端部分,以供在以后的项目开发过程中参考.如果觉得不错可以关注专栏 面向加薪编程C/C++ ,第一时间接收文章更 ...

  8. Java中Socket通信-客户端与服务端相互传输对象数据

    场景 Java中Socket通信-服务端和客户端双向传输字符串实现: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/1084885 ...

  9. Java中Socket通信-客户端向服务端发送照片

    场景 Java中Socket通信-服务端和客户端双向传输字符串实现: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/1084885 ...

最新文章

  1. Qt入门(10)——调试技术
  2. 深入浅出 JavaScript 内存管理,垃圾回收
  3. 广告出价--如何使用PID控制广告投放成本
  4. mysql配置以及性能优化(转)
  5. Struts2中Result类型介绍
  6. 重构图书馆惊魂夜(理解模型,关注设计)
  7. 对于纯Java项目,JVM 各个类加载器的加载目标是什么?
  8. Windows下给Git配置SSH
  9. python 漏洞扫描器_自动扫描全网漏洞的扫描器
  10. Rhino在java中的用法
  11. 阿里云OSS文件上传
  12. 编译错误(拓补排序)
  13. 仿苹果官网首页面(Hbuilder X+CSS)
  14. 数据库原理及应用习题三
  15. 【linux内核分析与应用-陈莉君】设备驱动概述
  16. 语义分割重制版1——Pytorch 搭建自己的Unet语义分割平台
  17. Midjourney API 接口对接历程
  18. 高红梅 第二章 第二节 性别焦虑、两性关系与个人身份认同的困境
  19. 基于Java高校校园设备报修系统
  20. 半路接手项目有多难?教你做个接盘侠高手!

热门文章

  1. python自学攻略-Python自学攻略
  2. python电脑配置要求cpu-Python限制内存和CPU使用量的方法(Unix系统适用)
  3. python语言命令大全-Python常用命令之集合
  4. python介绍和用途-python应用领域介绍
  5. python在哪里学比较好-Python哪里学习好?老男孩python入门
  6. python输出数据到excel-使用python将大量数据导出到Excel中的小技巧分享
  7. ControlAdvice和ExceptionHandler处理异常的原理与设计
  8. LeetCode Binary Tree Right Side View(搜索)
  9. UVa1335 Beijing Guards(二分查找)
  10. libevent中事件的添加与删除