本文主要探讨linux环境下,数据包从网卡接收到协议栈处理的处理流程和对应的代码逻辑。

分析的内核代码版本为4.17.6,涉及到的网卡硬件功能特性和逻辑均以intel的82599以太网控制器为例,驱动为ixgbe。本文仅讨论physical function的驱动代码逻辑。


数据包从网卡接收开始,其总体处理流程如下:

  1. 网卡接收光/电信号,将其转换为数据帧内容,如果帧符合以太网地址等过滤条件,则保存到FIFO缓存中。82599控制器中共有8个FIFO缓存队列。

  2. 网卡解析FIFO中数据帧的2/3/4层信息,进行流过滤、流定向、RSS队列分流,计算出帧对应的分流队列号。82599控制器支持最多16个RSS分流队列。

  3. 网卡将数据帧内容通过DMA方式写入驱动程序指定的内存空间,并将帧的基本信息写入报文描述信息队列的寄存器(descriptor ring)中。

  4. 网卡发起硬件中断,系统响应硬件中断,进入驱动中的顶半部处理流程。

  5. 顶半部处理流程通过NAPI调度接口(napi_schedule)发起软件中断后就结束了,帧的具体处理逻辑在响应软中断的底半部流程中完成。

  6. 底半部流程中,驱动从DMA内存空间和网卡寄存器中获取帧信息和内容。之后重新分配新的DMA内存空间并更新网卡寄存器,使网卡能够继续处理并写入数据帧。

  7. 对于每个数据帧,驱动根据报文类型调用协议栈注册的处理接口函数进行协议栈解析处理。


下面具体介绍一下每一步的具体代码逻辑。

  1. 网卡接收光/电信号,将其转换为数据帧内容,如果帧符合以太网地址等过滤条件,则保存到FIFO缓存中。82599控制器中共有8个FIFO缓存队列。

    这一步是完全由网卡硬件完成的。但是L2的报文过滤可以通过驱动修改过滤地址的方式加以控制。一般情况下,只有以太网地址符合本地网卡以太网地址时帧才能通过过滤,82599控制器支持最多设置128个以太网地址。此外,可以设置打开网卡的混杂模式(promisc mode)来接收所有MAC地址帧,这个操作可以通过ixgbe_set_rx_mode函数实现,该函数修改了IXGBE_FCTRL_UPE 和 IXGBE_FCTRL_MPE寄存器来跳过L2报文过滤。


  2. 网卡解析FIFO中数据帧的2/3/4层信息,进行流过滤、流定向、RSS队列分流,计算出帧对应的分流队列号。82599控制器支持最多16个RSS分流队列。

    这一步也是由网卡硬件实现的,驱动可以设置流过滤、流定向的规则,具体的方式可参见82599 datasheet第7.1.2节。RSS队列分流的具体逻辑参见82599 datasheet的7.1.2.8节,驱动在启动网卡时,需要修改MRQC、R×××K、RETA等寄存器来打开RSS分流功能、设置分流算法、哈希种子、分流映射表等。相关的代码可在ixgbe_setup_mrqc中找到。

    PS:由于第1步和第2步完全由硬件实现,无从验证,其功能步骤执行的具体顺序不一定完全与本文相符。


  3. 网卡将数据帧内容通过DMA方式写入驱动程序指定的内存空间,并将帧的基本信息写入接收报文描述信息队列(receive descriptor ring)中。

    这一步同样由网卡硬件实现。但网卡寄存器的初始化,以及可DMA访问的内存缓存空间申请是由网卡驱动在启动网卡时完成的。每个队列的具体初始化代码在ixgbe_configure_rx_ring中。该函数首先初始化IXGBE_RDH、IXGBE_RDT等缓存队列寄存器,然后调用ixgbe_alloc_rx_buffers分配缓存队列的内存空间,并进行DMA映射。接收报文描述信息队列(descriptor ring)同样是一系列DMA映射的内存空间,每个队列的ring空间是连续的,这个空间地址在ixgbe_setup_rx_resources函数中分配,在ixgbe_configure_rx_ring的起始部分写入到IXGBE_RDBA寄存器中。

    receive descriptor有两种格式,Legacy Receive Descriptor和Advanced Receive Descriptor,一般使用后者。数据结构的定义在ixgbe_type.h的ixgbe_adv_rx_desc函数中,字段解释可以参见datasheet的7.1.6节。需要注意的是这个数据结构是网卡和驱动公用的接口数据结构,因此其结构定义是不能在驱动中修改的。这个结构分成两个部分:read部分由驱动负责写入,网卡负责读取,用于向网卡传递每个报文的DMA缓存空间地址;wb(write-back)部分由网卡写入,驱动读取,用于网卡写入与报文相关的信息,例如报文长度等。


  4. 网卡发起硬件中断,系统响应硬件中断,进入驱动中的顶半部处理流程。

    在驱动打开网卡的函数ixgbe_open过程中,会在ixgbe_request_msix_irqs函数中调用request_irq(entry->vector, &ixgbe_msix_clean_rings, 0,
          q_vector->name, q_vector)函数注册硬件中断号和中断处理函数ixgbe_msix_clean_rings。这里的中断号在ixgbe_acquire_msix_vectors函数中使用pci_enable_msix_range函数分配。

    网卡发起硬件中断后,系统调用中断处理函数ixgbe_msix_clean_rings进行处理


  5. 顶半部处理流程通过NAPI调度接口(napi_schedule)发起软件中断后就结束了,帧的具体处理逻辑在响应软中断的底半部流程中完成。

    ixgbe_msix_clean_rings函数的流程非常简单,函数判断一下这个中断是否有对应的rx或tx队列,如果有则调用napi_schedule_irqoff发起napi调度,将具体的处理工作交给napi的底半部处理函数。


  6. 底半部流程中,驱动从DMA内存空间和网卡寄存器中获取帧信息和内容。之后重新分配新的DMA内存空间并更新网卡寄存器,使网卡能够继续处理并写入数据帧。

    napi的底半部处理函数为ixgbe_poll,在ixgbe_alloc_q_vector函数中使用netif_napi_add接口注册。ixgbe_poll主要调用ixgbe_clean_rx_irq和ixgbe_clean_tx_irq来处理网卡收到和发送的报文。这里主要分析ixgbe_clean_rx_irq。clean_rx_irq函数会从缓存队列中获取若干个报文信息,并调用ixgbe_alloc_rx_buffers向队列补充缓存空间资源,最后调用ixgbe_rx_skb函数,这个函数直接调用napi_gro_receive函数,之后的流程就与网卡和网卡驱动无关了。


  7. 对于每个数据帧,驱动根据报文类型调用协议栈注册的处理接口函数进行协议栈解析处理。

    napi_gro_receive函数的逻辑较复杂,一般最终会调用__netif_receive_skb_core函数。该函数调用deliver_skb,最终调用注册的packet_type->func函数对skb数据进行解析处理。例如IPv4协议的packet_type中的func函数就是ip_rcv。



由于内核代码结构复杂,上述流程中仍有一些不明或不确之处,欢迎指正。