版本说明


Linux版本: 3.10.103
网卡驱动: ixgbe

报文收发简单流程


网卡驱动默认采用的是NAPI的报文处理方式。即中断+轮询的方式,网卡收到一个报文之后会产生接收中断,并且屏蔽中断,直到收够了netdev_max_backlog个报文(默认300)或者收完网卡上的所有报文之后,重新打开中断。

网卡数据处理


网卡初始化

  • 内核启动时会调用do_initcalls,从而调用注册的初始化接口net_dev_initnet_dev_init注册软中断的回调函数,分别为接收和发送的:NET_RX_SOFTIRQ = net_rx_actionNET_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进行下一步的处理。
  • 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进入虚拟机的协议栈。

请输入图片描述

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也支持多协议。
最后修改:2021 年 08 月 20 日
如果觉得我的文章对你有用,请随意赞赏