网卡研究一:驱动框架剖析

前言

本系列计划以优化网卡侧UDP高速小包抓取需求为着力点, 从网卡驱动分析网卡多队列中断均衡优化网卡侧零拷贝抓取实现三个递进深度详细阐述网卡驱动的工作机制。 作为系列第一作,本文以Intel 82580、Intel 82571/4两款网卡为对象, 详细分析网卡驱动的数据结构管理多队列/单队列收发包处理NAPI调用机制等模块,为后续的优化工作打下基础。 操作系统:RHEL7.2 内核版本:linux-3.10.0-327.el7.x86_64 驱动框架: Intel 82580:igb Intel 82571/4:e1000e

数据结构框图分析

多队列网卡是以后网卡能力的基本配置,也是性能提升的基础。 因此首先分析Intel 82580:igb驱动框架。网卡硬件框架如图1所示,驱动数据结构和数据流关系如图2所示: 图1

图1 网卡硬件框架

图2

图2 驱动数据结构和数据流关系

图2中黑色箭头表RX数据结构关系,红色箭头表示TX数据结构关系。首先从数据流的角度简要分析: 图3

图3 硬件队列指针

RX:网卡硬件MAC层收到数据后,根据驱动初始化配置好的rx_desc接收缓冲描述符,利用DMA将硬件RX FIFO中的数据拷贝到内核缓冲rx_buffer中,接收缓冲是以内存page页为管理对象。随后经过驱动处理,将rx_buffer中对应page页挂接到skb中,skb为整个内核网络数据流管理的对象。内核协议栈收到skb后针对不同协议要求进行处理,最后将净载荷拷贝至用户层完成RX工作。 TX:用户层调用网络send接口,内核将数据拷贝至skb管理的缓冲。随后经过驱动处理,直接将skb->data对应的内核缓冲以流式DMA映射的方式拿到对应的page页,填充到tx_desc描述符中,并在最后一帧的描述符tx_desc中将EOP(End of Packet)置位,同时更新硬件指针如图3所示。网卡通过硬件指针检测到有待发数据,利用DMA将内核缓冲中的数据拷贝至硬件TX FIFO中发送到对端计算机。

数据结构详细分析

下面对驱动中的具体数据结构如图4所示进行分析。由于数据结构多且复杂,转成图4画质较低。 图中黑色箭头及①类数字表示RX相关数据结构,红色箭头及❶类数字表示TX相关数据结构。 图4

图4 驱动数据结构

8核CPU下的82580网卡驱动默认属性如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* set default ring sizes */
adapter->tx_ring_count = IGB_DEFAULT_TXD = 256;
adapter->rx_ring_count = IGB_DEFAULT_RXD = 256;

/* set default ITR values */
adapter->rx_itr_setting = IGB_DEFAULT_ITR = 3;
adapter->tx_itr_setting = IGB_DEFAULT_ITR = 3;

/* set default work limits */
adapter->tx_work_limit = IGB_DEFAULT_TX_WORK = 128;

max_rss_queues = IGB_MAX_RX_QUEUES = 8

adapter->rss_queues = min_t(u32, max_rss_queues, num_online_cpus()) = 8

adapter->num_rx_queues = adapter->rss_queues = 8
adapter->num_tx_queues = adapter->rss_queues = 8

adapter->num_q_vectors = numvecs = 8
针对每个网卡接口,内核会抽象adapter数据结构,根据82580网卡支持收发各8个硬件队列属性,该adapter包含8个struct igb_q_vector数据结构。 RX: ① igb_q_vector包含tx,rx各一个struct igb_ring_container,用以维护ring属性; ② igb_q_vector包含tx,rx各一个struct igb_ring,用以维护ring具体属性,是整个网卡管理核心数据结构; ③ 每个队列对应各自硬件中断,各自igb_q_vector,各自napi_struct数据结构,各自napi poll回调接口以轮询处理网卡数据包; ④ rx igb_ring根据网卡属性配置IGB_DEFAULT_RXD = 256分配256个igb_rx_buffer,用以维护rx buffer物理地址及对应page页,填充rx_desc描述符。同时维护skb数据结构,在rx buffer收到数据后,关联skb送入协议栈进行处理; ⑤ rx igb_ring根据网卡属性配置IGB_DEFAULT_RXD = 256分配256个adv_rx_desc,用以维护硬件描述符,是软件与硬件交互的关键数据结构; ⑥ igb_rx_buffer及adv_rx_desc数据结构通过dma_map_page指向同一page页,为接收硬件DMA数据实体 TX: ❶ 用户态通过拷贝数据,将数据传入内核协议栈; ❷ 协议栈以skb为管理对象,将数据依托skb传入igb_tx_buffer; ❸ 驱动将skb->data以dma_map_single(流式DMA映射 )方式拿到物理地址,填充adv_tx_desc描述符;

函数接口调用分析

在进行下述分析前,先引入几个概念,取自linux-3.10.0-327.el7.txt,这里放上网上找来的部分翻译版本便于快速理解,建议读原文: - RSS: Receive Side Scaling ———————————— 当代的NICs支持多个接收和传输队列,即多队列。接收的时候,一个网卡能够发送不同的包到不同的队列,为了在不同的CPU之间分散处理。NIC针对每一个包,通过一个过滤器来指定这个包属于少数几个流中的一个流。每个流中的数据包被控制在一个单独的接收队列中,而队列轮回的被CPU进行处理。这种机制就叫做RSS。RSS的目标和其他控制技术目的都是为了增加性能。多队列也可以被用于流量优先控制,但那不是这些技术的目的。 RSS中的过滤器是一个基于L3和L4层头部的hash函数,例如,基于IP地址和TCP端口的4元组的hash函数。最常见的RSS硬件实现中,使用了128个间接表,其中每个表存储一个队列号(注,网卡的队列数比较少,比如igb是8个,bnx2是5个)。针对某个包而言,使用这个包计算出的hash值(hash是Toeplitz算法)的低7位先确定间接表,再从间接表中的值访问队列。一些高级的NICs允许使用可编程的过滤器来控制包属于哪个队列。例如,绑定TCP端口80的webserver,数据包能被指向他们自己的队列。

  • RPS: Receive Packet Steering ———————————— RPS,逻辑上是一种以软件的方式来实现RSS。在数据路径上,稍后被调用。介于RSS选择了队列和CPU(这个cpu会处理硬中断),RPS选择CPU来执行硬件中断处理之后的协议处理。通过把数据包放在目标CPU的backlog队列,并唤醒CPU来处理。RPS相比RSS有几个好处: 1) RPS能够被任何NIC使用。 2) 软件过滤器能够轻易的被添加,用来hash新的协议。 3) 它不会增加硬件设备的中断。尽管,引入了IPIs(inter-processor interrupts)。 当一个设备使用 netif_rx() 函数和netif_receive_skb()函数,(从网卡驱动)向网络协议栈传递数据包时,RPS在底半环境(通过软中断来实现的,在硬中断处理函数之后。)中被调用。这2个函数调用get_rps_cpu()函数,来选择应该执行包的队列。决定目标CPU的第一步是基于包的地址和端口(有的协议是2元组,有的协议是4元组)来计算hash值。这个值与这个包的流保持一致。这个hash值要么是由硬件来提供的, 要么是由协议栈来计算的。厉害的硬件能够在包的接收描述符中传递hash值,这个值与RSS计算的值是相等的。这个hash值保存在skb->rx_hash中,并且这个值可以作为流的hash值可以被使用在栈的其他任何地方。每一个接收硬件队列有一个相关的CPU列表,RPS可以将包放到这个队列中进行处理。对于每一个接收到的包,指向这个列表的索引是通过流hash值对列表大小取模来计算的。被指向的CPU是处理 数据包的目标CPU,并且这个包被加到CPU的backlog队列的尾部。最底半处理的最后,IPI被发送到这个包所插到的那个CPU。IPI唤醒远程CPU来处理backlog队列,之后队列中数据包被发送到网络协议栈进行处理。

  • RFS: Receive Flow Steering ————————————
    RPS只依靠hash来控制数据包,提供了好的负载平衡,但是它没有考虑应用程序的位置(注:这个位置是指程序在哪个cpu上执行)。RFS则考虑到了应用程序的位置。RFS的目标是通过指派应用线程正在运行的CPU来进行数据包处理,以此来增加数据缓存的命中率。RFS依靠RPS的机制插入数据包到指定CPU的backlog队列,并唤醒那个CPU来执行。

  • XPS: Transmit Packet Steering ———————————— XPS 是一种机制,用来智能的选择多队列设备的队列来发送数据包。为了达到这个目标,从CPU到硬件队列的映射需要被记录。这个映射的目标是专门地分配队列到一个CPU列表,这些CPU列表中的某个CPU来完成队列中的数据传输。这个有两点优势,第一点,设备队列上的锁竞争会被减少,因为只有很少的CPU对相同的队列进行竞争。(如果每个CPU只有自己的传输队列,锁的竞争就完全没有了。)第二点,传输时的缓存不命中的概率就减少,特别是持有sk_buff的数据缓存图5

    图5 多队列处理框架

简而言之,RSS是为了发挥网卡多队列处理性能,RPS则是软件层将数据包分发到多个CPU上进行处理(同时要保证cache一致性)。对于一个多队列的系统,如果RSS已经配置了,导致一个硬件接收队列已经映射到每一个CPU,那么RPS就是多余的和不必要的;如果只有很少的硬件中断队列(比CPU个数少),每个队列的rps_cpus指向的CPU列表与这个队列的中断CPU共享相同的内存域,那RPS将会是有效的。RFS则是为了增加内核层切换到用户层的cache命中率,XFS同理。网卡多队列处理框架如图5所示。

RX: ① 硬件中断顶半部处理,分队列处理; ② 硬件低半部处理; ③ napi轮询回调; ④ 网卡接收数据处理轮询接口,不同队列接收处理的数据,根据五元组hash值入列到依托RPS(Receive Packet Steering)机制选取的CPU的backlog队列,后续的协议栈处理由该CPU处理; ⑤ 在④对应的CPU轮询处理自己的backlog队列,接入协议栈处理; ⑥ IP层数据处理,如果在该层插入自定义协议,则可从该层将符合自定义协议的数据包截获,PF_RING就是利用了这个机制将包截获处理; ⑦ TCP/UDP/ICMP/IGMP等协议处理。

硬件接收中断

1
-> igb_msix_ring 		 中断服务函数(分队列处理)	
1
2
3
4
5
-> napi_schedule -> napi_schedule_prep		
检测napi->state, NAPI_STATE_SCHED是否置位允许napi调度
-> __napi_schedule -> ____napi_schedule
关闭硬件中断,并将该napi->poll_list添加到全局轮询队列poll_list
-> __raise_softirq_irqoff(NET_RX_SOFTIRQ) 产生napi软件中断
1
2
3
4
-> net_rx_action 							软中断服务函数
只要全局poll_list队列不为空,则一直轮询处理
当轮询完成预设目标任务budget,或者2秒轮询超时后强制退出则结束napi轮询,重新使能中断
-> n->poll -> igb_poll 回调网卡轮询处理接口
1
2
3
4
5
6
7
8
9
10
11
12
-> igb_clean_rx_irq
-> igb_alloc_rx_buffers
判断回收rx_buffer超过IGB_RX_BUFFER_WRITE(16),一次性补充16个buffer
-> igb_fetch_rx_buffer 申请skb并将rx_buffer数据page挂接到skb
-> napi_gro_receive
判断网卡是否支持GRO(Generic Segmentation Offload)
相对应的有TSO(TCP Segmentation Offload)
-> napi_skb_finish -> netif_receive_skb
使能CONFIG_RPS(Receive Packet Steering)时
-> get_rps_cpu 依据skb->hash获取后续传输层协议栈处理target CPU
-> enqueue_to_backlog -> __skb_queue_tail
将各个队列的skb入列到对应处理target CPU的input_pkt_queue
1
2
3
4
5
6
7
8
9
-> ____napi_schedule 
-> __raise_softirq_irqoff(NET_RX_SOFTIRQ) 产生sd->backlog软件中断
-> net_rx_action 软中断服务函数
-> n->poll -> process_backlog 回调backlog的轮询处理函数
-> __skb_dequeue 从队列中出列待处理skb
-> __netif_receive_skb -> __netif_receive_skb_core
开始处理网络层skb
根据注册skb->protocol搜索pt_prev /* Protocol hook */
-> deliver_skb -> pt_prev->func
1
2
3
4
5
6
7
8
9
10
11
12
-> ip_rcv               IP层数据处理,net/ipv4/af_inet.c注册
若注册pf_ring的packet_type,则调用pfring rcv
prot_hook.func = packet_rcv;
prot_hook.type = htons(ETH_P_ALL);
-> ip_rcv_finish -> dst_input /* Input packet from network to transport */
-> skb_dst(skb)->input(skb)
-> ip_local_deliver -> ip_defrag /* Reassemble IP fragments. */
-> ip_local_deliver_finish
根据注册inet_protos搜索ipprot
tcp_protocol, udp_protocol, icmp_protocol, igmp_protocol等
在net/ipv4/af_inet.c,inet_init注册,以tcp为例
-> tcp/udp层 -> ipprot->handler
1
2
3
4
5
-> tcp_v4_rcv(udp_rcv) -> tcp_v4_do_rcv 
-> tcp_rcv_state_process
-> case TCP_ESTABLISHED: tcp_data_queue
-> tcp_queue_rcv
-> __skb_queue_tail /* queue a buffer at the list tail */
1
2
-> napi_complete            结束轮询
-> igb_ring_irq_enable 重新使能硬件中断

由于发送流程相对一致、简单,这里只对接口功能进行注释,不做赘述。 TX:网络发送流程

1
2
3
4
5
6
7
8
9
10
11
应用层send -> copy_from_user 
-> 内核层skb -> 协议栈
-> 分队列
-> ndo_start_xmit -> igb_xmit_frame
-> igb_xmit_frame_ring
-> igb_tx_queue_mapping 根据skb->queue_mapping获取对应发送队列tx_ring
-> igb_xmit_frame_ring skb填充tx_ring->tx_buffer_info
-> igb_tso TSO(TCP Segmentation Offload)
-> igb_tx_map
将skb流式映射dma_map_single填充描述符tx_desc
并在最后一帧的描述符tx_desc中将EOP(End of Packet)置位

硬件发送完成中断

1
2
3
4
5
6
7
8
9
10
11
-> igb_msix_ring                            中断服务函数(分队列处理)
-> napi_schedule -> napi_schedule_prep
检测napi->state, NAPI_STATE_SCHED是否置位允许napi调度
-> __napi_schedule -> ____napi_schedule
关闭硬件中断,并将该napi->poll_list添加到全局轮询队列poll_list
-> __raise_softirq_irqoff(NET_RX_SOFTIRQ) 产生napi软件中断
-> net_rx_action 软中断服务函数
只要全局poll_list队列不为空,则一直轮询处理
当轮询完成预设目标任务budget,或者2秒轮询超时后强制退出则结束napi轮询,重新使能中断
-> n->poll -> igb_poll 回调网卡轮询处理接口
-> igb_clean_tx_irq 释放skb,dma_unmap_single

结束语

本文在对Intel82580网卡驱动分析的基础上,梳理了各部数据结构间的关联关系,总结了网卡收发数据流的处理流程。 那么,多队列和CPU中断之间均衡处理关系到底应该如何优化,中断均衡是否能够带来实质性的性能提升,落实到具体实测数据多队列网卡表现又是如何,请看网卡研究二:中断均衡优化。

PS:在研究之初对网卡队列并没有过多的理解,所以选择了单队列网卡Intel 82574(e1000e)开始分析,其数据结构关系和收发流程与多队列网卡大同小异,在此不做赘述。

-------------The End-------------
🙈坚持原创技术分享,感谢支持🙈
0%