本篇文档主要描述了网络数据包在二层的接收流程,主要包括以下三个部分:
1)、82599网卡和数据包接收相关的内容;
2)、ixgbe网卡驱动数据包接收相关的配置;
3)、ixgbe网卡驱动napi接口的处理。
82599网卡和数据包接收相关的内容
这一部分要介绍的是82599网卡中和数据包接收相关的内容。网络报文接收流程所涉及的内容很多,如报文过滤、mac层卸载、报文接收描述符、校验和卸载以及分离报文有效载荷和头部等,由于篇幅原因,这里只介绍了报文接收描述符相关的内容,其他内容会在后续描述中进行穿插。
说到网卡报文接收,就必须得说到报文接收描述符,因为报文接收描述符承载了报文从网卡流入到主存的过程。对于网卡硬件而言,当网卡收到网络报文的时候,会往报文接收描述符中指定的地址写入报文数据,而网卡驱动则会从报文接收描述符中指定的地址读取报文,并送往上层协议栈处理。
除了上面说到的存放报文的内存地址,报文接收描述符中还有用于存储报文信息的域。对于82599网卡而言,其支持两种格式的报文接收描述符,即传统格式和高级格式。虽然有两种不同格式的报文接收描述符,但是两种格式的报文接收描述符所占用的内存大小是一样的(目前为16字节),只是对这块内存使用有所不同。对于两种不同格式的报文接收描述符,可以在网卡驱动初始化的时候进行配置,通过设置网卡的SRRCTL寄存器的DRSCTYPE域进而选择使用某种格式的报文接收描述符。在初始化阶段,网卡驱动会申请报文描述符,并填充描述符中相关的域,然后告诉网卡该描述符可用,后续网卡接收到报文就可以用报文描述符来存储报文相关的信息,然后网卡将报文描述符回写给网卡驱动,网卡驱动从中获取所需要信息,并交由上层进行处理。
传统格式报文接收描述符
先来看下82599网卡中对传统格式报文接收描述符的定义,如下:
1 | union ixgbe_adv_rx_desc { |
报文接收描述符环形队列是用做网络报文接收的,而在网卡中接收报文的最小单位是一个队列,即RX队列。所以一般来说就是一个RX队列对应一个报文接收描述符环形队列。
从ixgbe驱动的实现可以知道,ixgbe使用一个叫做中断向量的对象来管理队列,其定义如下:
1 | struct ixgbe_q_vector { |
在上面的定义中,struct ixgbe_q_vector对象最后一个类型为struct ixgbe_ring的柔性数组成员就是由该中断向量所管理的队列,这里包括了RX队列和TX队列。报文接收流程只需要关注其中的RX队列即可。一般来说一个中断向量会关联一个硬件中断。当网卡往中断向量中的某个RX队列的描述符中写入报文信息时,就会触发对应的硬件中断,然后中断子系统就会调用我们注册的中断处理函数来处理这个中断,在ixgbe驱动中对应的就是ixgbe_intr()(在msi-x中断模式下对应的是ixgbe_msix_clean_rings())。这里需要做一个说明,就是在legacy或者msi中断模式下,只会使用一个中断向量,对应的使用一个中断号;而在msi-x中断模式下,可能会有多个中断向量,对应的会有多个中断号,一般来说会把一个中断向量对应的中断号进行绑核处理,这样可以提高报文处理效率。而具体到某一个RX队列是如何同一个中断号进行关联的,这里还涉及到另外一个网卡寄存器,即Interrupt Vector Alloction(IVAR),这里不再详细介绍,可以参考ixgbe驱动的ixgbe_configure_msi_and_legacy()和ixgbe_configure_msix()函数,以及网卡中断部分的配置。
在ixgbe网卡驱动的实现中,我们可以看到驱动是以一个叫做struct ixgbe_ring的对象来管理报文描述符环形队列(不管是接收还是发送),其定义如下:
1 | struct ixgbe_ring { |
struct ixgbe_ring对象中最重要的几个成员都已经做了注解,其中的desc成员就是报文描述符队列,从这里的实现也可以看出,报文描述符队列实际上是线性的,其逻辑上的环形操作是通过struct ixgbe_ring对象中的成员,如next_to_clean、next_to_alloc和next_to_use等来实现的。另外,struct ixgbe_ring对象中还有一个类型为dma_addr_t的dma成员,该成员就是desc成员对应的物理地址,有desc成员的内核虚拟地址进行一致性dma映射得到。这样ixgbe驱动可以通过desc来操作描述符环形队列,而网卡可以通过dma成员来操作描述符环形队列。
下面一起来看下ixgbe驱动是如何建立一个描述符环形队列管理对象的。其实现如下:
1 | int ixgbe_setup_rx_resources(struct ixgbe_ring *rx_ring) |
函数ixgbe_setup_rx_resources()处理流程很清晰:
1)、根据之前配置好的环形队列中报文接收描述符个数申请报文描述符数组所需要的内存,以及对应的用来管理报文缓冲区地址信息的缓冲区对象,这个时候缓冲区对象中用来存放报文内容的地址仍然是无效的,因为还没有申请内存,在函数ixgbe_alloc_rx_buffers()处理完成之后,缓冲区对象中存放报文内容的地址就是有效的,可以提供给网卡用来存放报文数据。此外,对报文接收描述符数组内存进行一致性dma映射,获取对应的物理地址,网卡需要使用物理地址,而不是虚拟地址。
2)、初始化描述符环形队列操作所涉及到的索引成员,包括next_to_use和next_to_clean。
经过ixgbe_setup_rx_resources()函数的处理,就已经成功创建了一个描述符环形的管理对象。接下来就需要告诉网卡这个描述符环形队列的信息,这个就是函数ixgbe_configure_rx_ring()所要做的事情了,其实现如下:
1 | void ixgbe_configure_rx_ring(struct ixgbe_adapter *adapter, |
从该函数的实现就可以看到,网卡驱动就是通过将接收报文描述符数组对应的物理地址写入到RDBA寄存器,并初始化RDH和RDT寄存器。通过写RDBA、RDH和RDT寄存器,网卡就知道了当前的描述符环形队列的信息。接着调用函数ixgbe_alloc_rx_buffers()申请用来存放报文数据的内存,并将对应的物理地址保存到接收描述符中,然后设置RDT寄存器,这样网卡就可以使用RDH和RDT之间的描述符进行接收报文处理了,ixgbe_alloc_rx_buffers()函数的实现如下:
1 | void ixgbe_alloc_rx_buffers(struct ixgbe_ring *rx_ring, u16 cleaned_count) |
补充说明:RDT寄存器由网卡驱动在提供报文接收描述符给网卡之后更新,而RDH寄存器由网卡在回写一个报文接收描述符给驱动之后更新。
ixgbe网卡驱动napi接口的处理
NAPI是Linux中综合了中断和轮询方式的网卡数据处理API。下面描述下ixgbe中是如何使用NAPI方式来进行收包处理的。
NAPI对象
在Linux中,NAPI接口提供了一个NAPI对象,这个是设备使用NAPI接口进行数据包处理的必要条件,先来看下其定义:
1 | struct napi_struct { |
一般来说,如果某个设备要使用NAPI接口进行数据包处理,那么该设备会在自己的设备对象中定义一个struct napi_struct类型的对象成员。在第二部分讲到过,ixgbe驱动中每个中断向量会关联一个中断号,从而在硬中断处理函数能获取到中断向量,而如果利用NAPI进行数据包处理的话,也就必须要获取到对应的struct napi_struct类型的对象,所以自然而然地ixgbe驱动将struct napi_struct类型的对象定义在了中断向量中。
下面对其中的部分重要成员进行简单的介绍:
1)、 poll_list。用于将本设备加入到cpu私有数据中类型为struct softnet_data的对象的待轮询设备链表中。
2)、state。设备的状态,有如下几种:
1 | enum { |
3)、weight。设备每次轮询所能处理的包的最大数量。
4)、poll。设备注册的轮询回调,在该回调中一般会遍历设备的所有rx队列,取出报文,送往上层处理。
NAPI初始化
从驱动实现我们知道,ixgbe驱动在中断向量中定义了一个类型为struct napi_struct的NAPI实例。在ixgbe驱动初始化的时候,会在创建中断向量的时候初始化其对应NAPI实例,实现如下:
1 | static int ixgbe_alloc_q_vector(struct ixgbe_adapter *adapter, |
从函数ixgbe_alloc_q_vector()调用netif_napi_add()初始化NAPI对象可以看到,ixgbe驱动注册的poll回调钩子是ixgbe_poll(),而每次轮询最大可处理的数据包为64个。
NAPI调度
在ixgbe驱动中因为使用了NAPI接口进行数据包处理,所以对应的上半部实现就变成了当硬中断触发后,在硬中断处理函数中调用NAPI的调度接口napi_schedule_irqoff()将设备加入到cpu私有数据中类型为struct softnet_data的对象的待轮询设备链表中,并触发软中断。以msi-x中断模式为例,其对应的具体实现如下:
1 | static irqreturn_t ixgbe_msix_clean_rings(int irq, void *data) |
而下半部的处理就是在网络子系统的软中断处理函数net_rx_action()中遍历cpu私有数据中类型为struct softnet_data的对象中的待轮询设备链表,依次调用每个设备注册的poll回调钩子进行报文接收处理,其对应的具体实现如下:
1 | static __latent_entropy void net_rx_action(struct softirq_action *h) |
上面说到过,在下半部的软中断处理函数中会调用设备注册的回调函数poll进行收包处理,而ixgbe驱动中对应的轮询回调函数就是ixgbe_poll()。在这个函数中会遍历NAPI对象关联的中断向量中的所有RX队列,将收到的每一个报文通过调用函数__netif_receive_skb()送往上层协议栈进行处理,具体处理细节可以参考驱动实现。
通过上面对ixgbe驱动中使用NAPI接口的描述,我们可以总结出NAPI接口的数据包接收流程如下: