



napi()
系统调用。简单总结一下上述流程:

NIC 在接收到数据包之后,首先需要将数据同步到内核中,这中间的桥梁是 rx ringbuffer。它是由 NIC 和驱动程序共享的一片区域,事实上,rx ring buffer 存储的并不是实际的数据包,而是一个描述符,这个描述符指向了它真正的存储地址,具体流程如下:
当 NIC 驱动程序的处理速度跟不上网卡收包速度时,驱动来不及分配缓冲区,NIC 接收到的数据包无法及时写到 sk_buffer,就会产生堆积,当 NIC 内部缓冲区写满后,就会丢弃部分数据,引起丢包。这部分丢包为 rx_fifo_errors,在 /proc/net/dev 中体现为 FIFO 字段增长,在 ifconfig 中体现为 overruns 指标增长。
7.1 因为 igb_ckean_rx_irq 会循环消耗数据包,但存在循环有次数限制,否则 CPU 会一直停留在处理部分。循环次数限制对应的内核参数为 net.core.netdev_budget = 300。
7.2 取出 skb,调用 napi_gro_receive,这个函数先做一些 GRO 包合并动作,然后根据是否开启 RPS 执行如下流程。
net.core.netdev_max_backlog = 1000
决定,也就是当数据包超过 1000 个时,就会被丢弃。所以用户态消耗要跟上才行。后续就由队列的 vCPU 取出数据包并调用
__netif_receive_skb_core
来处理。__netif_receive_skb_core
来处理该数据包。NOTE:由上述描述可知,开启 RPS 后,下半步软中断的数据包可以直接挂到其它 vCPU队列上,这样就能减少 vCPU2 的压力,vCPU2 就能处理更大的流量。可见,RPS 适合单网卡队列,多 vCPU 的使用场景。
NOTE:__netif_receive_skb_core 是内核协议栈处理数据包的入口函数,使用 tcpdump 抓包就是在此处起了作用,也就是说如果 tcpdump 能数据包就代表数据包已经到达内核协议栈入口了。

上图所示是数据包在 Open vSwitch 内部转发的流程,其中 netdev_frame_hook 是 OvS Bridge(Datapath)的入口,被 __netif_receive_skb_core 调用来处理接收到的数据包。

数据流量进入 OvS Bridge 之后根据数据包的各类头部信息(五元组)查找流表,并进行过滤或转发处理。

主要有下列两种处理情况,而判断的依据就是是否能够根据五元组匹配到流表。

dev_hard_star_xmit
、
xmit_one
,最后调用
ndo_start_xmit(skb, dev)
发送数据包。
br-int 与虚机tap之间通过通过一对veth接口互联,一个段加入ovs的br-int,一段加入linux内核的bridge

虚拟“网线”(veth pair):不是一个设备,而是一对设备,用于连接两个虚拟以太端口。veth pair 的本质是反转通讯数据的方向,需要发送的数据会被转换成需要收到的数据重新送入内核协议栈进行处理,从而间接的完成数据的注入。veth pair 的工作原理就相当于当连接 Linux Bridge 的口收到了数据包,内核协议栈会反转将该数据包重新发送给 Linux Bridge。
继续上述的流程:
veth_xmit
将数据包发出。dev_forward_sk
,然后
dev_forward_sk
调用内核协议栈收包入口
netif_rx_internal
,最终
netif_rx_internal
将数据包放入 CPU 收包队列。如此,该数据包就被注入了内核协议栈。
在本文的网络模型中。Linux Bridge 作为安全层,虚拟机的 tap 口通过 veth pair 虚拟设备接入 Linux Bridge。由上文可知,数据包会经过 veth pair 注入到内核协议栈,内核协议栈会再次调用 _netif_reveive_skb 处理该数据包。而因该函数是 veth 口收到的数据,该接口连接的是Linux Bridge,所以协议栈会调用 br_handle_frame 处理该数据包。Linux Bridge 处理数据包会经过以下几个情况:
NF_BR_PRE_ROUTING
钩子,该钩子 处理的就是之前通过 iptables 添加的 PRE ROUTING 策略。NF_BR_PRE_ROUTING
钩子后,会判断是将该数据包当成本机数据包处理,还是继续转发(后文以转发为例进行说明)。NF_BR_FORWARD
钩子函数,所有转发的数据包都会经过该函数,安全组策略就是在此钩子中进行的。br_forward_finish
将数据包发出去,
NF_BR_POST_ROUTING
钩子存在此处,作为数据包发出前最后的处理。dev_hard_start_xmit
函数将数据包发出。
Linux Bridge 将数据包交给 tap 口,最终是将数据包将给用户态进程 KVM-QEMU 的虚拟机进行消耗。所以需要将数据包从内核态转发到用户态,且触发中断告知虚拟机数据包来了。显然,tap 口自己完成不了这样的工作,tap 口仅能处理数据包,而内核态、用户态的运行模式切换以及中断触发,就需要通过另一个层级的内核线程驱动了。本文场景中使用的是 vhost-net 技术。也就是说,实际上是通过 tap 口和 vhost 的配合来完成了将数据包交给虚拟机。
Linux Bridge 最后调用了 dev_hard_start_xmit 将数据包交给 tap 口,所以会调用 tap 口的 xmit_one 将数据包送到其入口函数。最后调用 tun_net_xmit 把将数据包发出,tun_net_xmit 这个函数很重要,承载了 tap 数据的逻辑:
socket.sk-
>
sk_receive_queue
,即 Socket 队列。Socket 队列有长度限制,默认为 tap 口的
tx_queuelen
,通常是 1000 个数据包,如果长度大于 1000 则会丢弃数据包。遇到这种情况,说明虚拟机处理较慢,应该想办法优化虚拟机的处理速度。e.g. txqueuelen:1000 为 tap 口 Socket 队列的长度,单位是包个数

tap 口通过 tun_net_xmit 唤醒 vhost worker 之后,vhost 线程就会将数据包拷贝到虚拟机的 Ring Buffer 中,且触发中断告诉虚拟机数据包已到,虚拟机继而消费这些数据包。
vhost 线程,是创建虚拟机时一并启动的线程,线程名称 vhost-<qemu_PID>。vhost 线程是个死循环,它被唤醒后循环着干两件事情:

如图,vhost 线程会调用 KVM 的接口来触发中断,主要做了两件事情:
也就是说,这两件事情其实只是让 pCPU 退出,方便注入中断请求,和提前注入中断标记。此外,还需要 vCPU 死循环来完成,在循环体中,会有检查中断注入标记的环节,如果发现该标记就调用 kvm_x86_ops->run 立即触发中断。该触发会最终调用中断处理函数 vp_interrupt -> vp_ring_interrupt-> 最终触发内核协议栈 的运行,让协议栈调用 __netif_receive_core 处理数据包。