版本说明
Linux版本: 3.10.103
网卡驱动: ixgbe
报文收发简单流程
网卡驱动默认采用的是NAPI的报文处理方式。即中断+轮询的方式,网卡收到一个报文之后会产生接收中断,并且屏蔽中断,直到收够了netdev_max_backlog个报文(默认300)或者收完网卡上的所有报文之后,重新打开中断。
网卡数据处理
网卡初始化
- 内核启动时会调用
do_initcalls
,从而调用注册的初始化接口net_dev_init
,net_dev_init
注册软中断的回调函数,分别为接收和发送的:NET_RX_SOFTIRQ = net_rx_action
,NET_TX_SOFTIRQ = net_tx_action
。 - 网卡驱动加载时调用
ixgbe_init_module
注册一个PCI驱动。 接着调用probe对应的
ixgbe_probe
做一些准备工作如下:- 创建
ixgbe_adapter
数据结构,这是网卡的一个实例,其中包含了网卡的所有数据和接口(netdev)。 - netdev注册了网卡的所有操作:
ixgbe_netdev_ops
和网卡的features。 ixgbe_init_interrupt_scheme
主要是设置网卡的NAPI对应的poll接口,这里主要是ixgbe_poll。
- 创建
网卡激活时会调用netdev上的open函数
ixgbe_open
,完成工作如下:- 设置接收和发送队列,通过DMA讲PCI网卡地址和队列建立映射。
- 给网卡注册硬中断:
ixgbe_open-->ixgbe_request_irq-->ixgbe_request_msix_irqs
注册中断回调ixgbe_msix_clean_rings
。(MSIX中断时)
ixgbe_close
函数所做的事情:- 关闭中断并且释放中断向量号。
- 释放申请的相应空间。
网卡收发数据
- 当网卡接收到数据帧时,DMA将数据搬运到对应的
rx_ring
,然后产生一个硬中断,调用接口ixgbe_msix_clean_rings
。 硬中断回调函数
ixgbe_msix_clean_rings
会调用当前CPU对应的NET_RX_SOFTIRQ
,即net_rx_action
。其中的主要实现NAPI操作如下:- 关闭硬中断。
- 调用该CPU的
poll_list
中的所有poll函数,即ixgbe_poll
。 - 开启硬中断。
轮询函数
ixgbe_poll
主要是处理接收发送任务:- 循环处理每个ring中的数据,
ixgbe_clean_tx_irq
,此处发送tx_work_limit个报文(128)或者没有报文后完成,并且调用netif_wake_subqueue
触发发送软中断NET_TX_SOFTIRQ
,即net_tx_action
。 - 循环处理每个ring中的数据,
ixgbe_clean_rx_irq
,此处接收weight个报文(64),总共收取netdev_budget(300)个,并且调用ixgbe_rx_skb
进行下一步的处理。
- 循环处理每个ring中的数据,
ixgbe_rx_skb
最终会处理过GRO之后调用到netif_receive_skb
,此处忽略rps。netif_receive_skb
最终调用__netif_receivve_skb_core
进入协议栈处理各种协议。- 首先根据下面介绍的rx_handler类型调用ovs或者桥处理函数进入L2的处理。
- 然后根据pttype_base处理相关的L3协议处理。
备注
- ixgbe拥有64个rx_ring,每个ring默认有512个buffer,最小64,最大4096。
- 由以上可知网卡默认可以缓存32768个报文,当网卡接收报文的速度过快,接收软中断无法处理的时候,系统进入扼流(throttle)状态,所有后续的报文都被丢弃,直到rx_ring有空间了。
- 通过DMA地址映射的方式,网卡接收数据相当于零拷贝。
TUN虚拟网卡
- 网卡驱动加载时调用
tun_init
注册一个MISC驱动,生成/dev/net/tun
的设备文件。 - 接着调用open对应的
tun_chr_open
做主要创建socket。 - 随后调用ioctl对应的
tun_chr_ioctl
,完成一些需要的配置和信息获取,包括校验、连接状态、offload、mac等。 - 通过调用write发送报文,主要是调用
tun_chr_aio_write
生成skb格式报文,最终通过调用netif_rx
进入协议栈,通过三层转发到对应的端口发送。 - 通过调用read接收报文,主要是调用
tun_chr_aio_read
,从内核对应的sock队列上拿到报文,然后将数据上传到用户层。
注:
因为网卡接收报文后最终经过四层的时候,会将报文根据协议几元组找到对应的sock结构,放入队列。上层调用read或者recv的时候都是从socket对应的sock结构队列上取数据。
VETH虚拟网卡
- 网卡驱动加载时调用
veth_init
注册一个netlink驱动。 - 接着调用newlink对应的
veth_newlink
,创建两个网络设备,两者互为对方的peer。 - 发送报文时会调用
ndo_start_xmit
对应的veth_xmit
,实际操作为将skb的dev设置为自己的peer,调用到netif_rx
接收到协议栈。
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
进入虚拟机的协议栈。
- tap设备的
Offload技术
发送数据
- TSO(TCP Segmentation Offload)使得网络协议栈能够将大块buffer推送至网卡,然后网卡执行分片工作,这样减轻了CPU 的负荷,但TSO需要硬件来实现分片功能,也叫LSO。
- UFO(UDP Fragmentation Offload)类似。
- GSO(Generic Segmentation Offload),它比TSO更通用,基本思想就是尽可能的推迟数据分片直至发送到网卡驱动之前,此时会检查网卡是否支持分片功能(如TSO、UFO),如果支持直接发送到网卡,如果不支持就进行回调分片后再发往网卡。这样大数据包只需走一次协议栈,而不是被分割成几个数据包分别走,这就提高了效率。
接收数据
- LRO(Large Receive Offload)通过将接收到的多个TCP数据聚合成一个大的数据包,然后传递给网络协议栈处理,以减少上层协议栈处理开销,提高系统接收TCP数据包的能力。
- GRO(Generic Receive Offloading),LRO使用发送方和目的地IP地址,IP封包ID,L4协议三者来区分段,对于从同一个SNAT域的两个机器发向同一目的IP的两个封包可能会存在相同的IP封包ID(因为不是全局分配的ID),这样会有所谓的卷绕的bug。GRO采用发送方和目的地IP地址,源/目的端口,L4协议三者来区分作为改进。所以对于后续的驱动都应该使用GRO的接口,而不是LRO。另外,GRO也支持多协议。