Linux收发包流程代码分析(ixgbe)

一、前言

Linux收发包是操作系统内核和驱动常规事务,本文是基于4.19内核和对应的ixgbe驱动进行的收包流程梳理。ixgbe是intel一款标准驱动,其中82599系列万兆网卡就是使用该驱动。

二、总体介绍

NIC (network interface card) 在系统启动过程中会通过ixgbe_open函数向系统注册自己的各种信息,系统会根据queue的size和number等信息分配 Ring Buffer 队列和驱动的内存资源,其中DMA的内存资源就是在该函数中通过ixgbe_setup_all_rx_resources和ixgbe_setup_all_tx_resources分配,再通过ixgbe_request_irq中断给网卡。
Ring Buffer 队列内存放的是一个个 Packet Descriptor ,其有两种状态: ready 和 used 。初始时 Descriptor 是空的,指向一个空的 skb,处在 ready 状态。
当有数据时,DMA 负责从 NIC 取数据放到 Ring Buffer 中,当 DMA 读完数据之后,NIC 会触发一个 IRQ 让 CPU 去处理收到的数据。因为每次触发 IRQ 后 CPU 都要花费时间去处理 Interrupt Handler,如果 NIC 每收到一个 Packet 都触发一个 IRQ 会导致 CPU 花费大量的时间在处理 Interrupt Handler,处理完后又只能从 Ring Buffer 中拿出一个 Packet,虽然 Interrupt Handler 执行时间很短,但这么做也非常低效,并会给 CPU 带去很多负担。所以目前都是采用一个叫做 New API(NAPI) 的机制,去对 IRQ 做合并以减少 IRQ 次数。
IRQ软中断在 Ring Buffer 上按顺序找到下一个 ready 的 Descriptor,**将数据存入该 Descriptor 指向的 sk_buff 中,并标记槽为 used。**因为是按顺序找 ready 的槽,所以 Ring Buffer 是个 FIFO 的队列。

Ring Buffer 相关的收消息过程大致如下:

图片来自参考1,对 raise softirq 的函数名做了修改,改为了 napi_schedule
在这里插入图片描述

三、代码解析

接下来介绍一下 NAPI 是怎么做到 IRQ 合并的。它主要是让 NIC 的 driver 能注册一个 poll 函数,之后 NAPI 的 subsystem 能通过 poll 函数去从 Ring Buffer 中批量拉取收到的数据。主要事件及其顺序如下:
1、NIC driver 初始化时向 Kernel 注册 poll 函数,用于后续从 Ring Buffer 拉取收到的数据
2、driver 注册开启 NAPI,这个机制默认是关闭的,只有支持 NAPI 的 driver 才会去开启
3、收到数据后 NIC 通过 DMA 将数据存到内存
4、NIC 触发一个 IRQ,并触发 CPU 开始执行 driver 注册的 Interrupt Handler
5、driver 的 Interrupt Handler 通过 napi_schedule 函数触发 softirq (NET_RX_SOFTIRQ) 来唤醒 NAPI 6、subsystem,NET_RX_SOFTIRQ 的 handler 是 net_rx_action 会在另一个线程中被执行,在其中会7、调用 driver 注册的 poll 函数获取收到的 Packet
8、driver 会禁用当前 NIC 的 IRQ,从而能在 poll 完所有数据之前不会再有新的 IRQ
当所有事情做完之后,NAPI subsystem 会被禁用,并且会重新启用 NIC 的 IRQ
回到第三步

下面重点介绍pool函数,该函数通过netif_napi_add注册,以回调函数形式被调用。读取buffer中到skb以及buffer内存的循环分配,以及XDP的作用位置,以及交由协议栈处理的动作都在本函数中。

上面说过 poll 是个 driver 实现的函数,所以每个 driver 实现可能都不相同。但流程基本是一致的:
1、从 Ring Buffer 中将收到的 sk_buff 读取出来
2、对 sk_buff 做一些基本检查,可能会涉及到将几个 sk_buff 合并因为可能同一个 Frame 被分散放在多个 sk_buff 中
3、将 sk_buff 交付上层网络栈处理
4、清理 sk_buff,清理 Ring Buffer 上的 Descriptor 将其指向新分配的 sk_buff 并将状态设置为 ready
5、更新一些统计数据,比如收到了多少 packet,一共多少字节等

以ixgbe_poll为例,其中处理TX的函数是ixgbe_clean_tx_irq,处理首包RX的函数是ixgbe_clean_tx_irq。发消息先不管,先看收消息,收消息走的是ixgbe_clean_rx_irq。收完消息后执行 napi_complete_done 退出 polling 模式,并开启 NIC 的 IRQ。从而我们知道大部分工作是在 ixgbe_clean_rx_irq 中完成的,其实现大致上还是比较清晰的,就是上面描述的几步。里面有个 while 循环通过 buget 控制,从而在 Packet 特别多的时候不要让 CPU 在这里无穷循环下去,要让别的事情也能够被执行。循环内做的事情如下:
1、先清理已经读取过的rx_buffer,并以此分配一批新的的rx_buffer ,从而避免每次读一个 sk_buff 就清理一个,很低效。
2、通过next_to_clean在IXGBE_RX_DESC宏中找到 Ring Buffer 中下一个需要被读取的 Descriptor ,并检查描述符状态是否正常
3、根据 Descriptor 找到rx_buffer读出来
4、根据bfp携带的act,通过xdp对数据进行处理
5、根据不同情况,通过ixgbe_build_skb或者ixgbe_construct_skb创建skb,保存rx_buffer->page内的数据。前者的创建函数build_skb,后者的创建函数是
napi_alloc_skb

(skb)向上层提交数据以后,这段内存将始终被这个skb占用(直到上层处理完以后才会
调用__kfree_skb释放,但已经跟这里没有关系了)。
6、通过ixgbe_put_rx_buffer函数将本次处理的buffer内存释放
7、检查是否是 End of packet,是的话说明skb内有 Frame 的全部内容,不是的话说明 Frame 数据比skb大,将该skb放入到下一次待处理的的buffer中进行处理
8、通过 Frame 的 Header 检查 Frame 数据完整性,是否正确之类的
9、记录skb的长度,读了多少数据
10、设置 Hash、checksum、timestamp、VLAN id 等信息,这些信息是硬件提供的。
11、通过ixgbe_rx_skb函数中的napi_gro_receive将 skb 交付上层网络栈
12、更新一堆统计数据
13、回到 1,如果没数据或者 budget 不够就退出循环
budget 会影响到 CPU 执行 poll 的时间,budget 越大当数据包特别多的时候,可以提高 CPU 利用率并减少数据包的延迟。但是 CPU 时间都花在这里会影响别的任务的执行。
budget 默认 300,可以调整
sysctl -w net.core.netdev_budget=600

napi_gro_receive会涉及到 GRO 机制,稍后再说,大致上就是会对多个数据包做聚合,napi_gro_receive 最终是将处理好的 sk_buff 通过调用 netif_receive_skb,将数据包送至上层网络栈。执行完 GRO 之后,基本可以认为数据包正式离开 Ring Buffer,进入下一个阶段了。在记录下一阶段的处理之前,补充一下收消息阶段 Ring Buffer 相关的更多细节。

下面是我对ixgbe_clean_rx_irq函数的注释。

/**
 * ixgbe_clean_rx_irq - Clean completed descriptors from Rx ring - bounce buf
 * @q_vector: structure containing interrupt and ring information
 * @rx_ring: rx descriptor ring to transact packets on
 * @budget: Total limit on number of packets to process
 *
 * This function provides a "bounce buffer" approach to Rx interrupt
 * processing.  The advantage to this is that on systems that have
 * expensive overhead for IOMMU access this provides a means of avoiding
 * it by maintaining the mapping of the page to the syste.
 *
 * Returns amount of work completed
 **/
static int ixgbe_clean_rx_irq(struct ixgbe_q_vector *q_vector,
			       struct ixgbe_ring *rx_ring,
			       const int budget)
{
	unsigned int total_rx_bytes = 0, total_rx_packets = 0;
	struct ixgbe_adapter *adapter = q_vector->adapter;
#ifdef IXGBE_FCOE
	int ddp_bytes;
	unsigned int mss = 0;
#endif /* IXGBE_FCOE */

	//ring中剩余的size
	u16 cleaned_count = ixgbe_desc_unused(rx_ring);
	unsigned int xdp_xmit = 0;
	struct xdp_buff xdp;

	xdp.rxq = &rx_ring->xdp_rxq;

	while (likely(total_rx_packets < budget)) {
		union ixgbe_adv_rx_desc *rx_desc;
		struct ixgbe_rx_buffer *rx_buffer;
		struct sk_buff *skb;
		unsigned int size;

		/* return some buffers to hardware, one at a time is too slow */
		if (cleaned_count >= IXGBE_RX_BUFFER_WRITE) {
			//一次性为ring中分配新的rx buffer内存空间,用以接受rx_ring中skb数据,避免每释放1个,就申请1个。
			//结构体是struct ixgbe_rx_buffer,并将地址放在rx_desc中。
			ixgbe_alloc_rx_buffers(rx_ring, cleaned_count);
			cleaned_count = 0;
		}
		
		//找到 Ring Buffer 上下一个需要被读取的 Descriptor ,并检查描述符状态是否正常
		rx_desc = IXGBE_RX_DESC(rx_ring, rx_ring->next_to_clean);
		size = le16_to_cpu(rx_desc->wb.upper.length);
		if (!size)
			break;

		/* This memory barrier is needed to keep us from reading
		 * any other fields out of the rx_desc until we know the
		 * descriptor has been written back
		 */
		dma_rmb();
		
        //获取ring中的rx_buffer,以及buffer中的skb
        //正常情况下,buffer中应该是没有skb的,如果存在,说明该skb不是最后一个packet(eop)或者被分片的数据,导致没有被上一次buffer处理
        //而被放到本次流程中处理。涉及的函数是ixgbe_add_rx_frag和ixgbe_is_non_eop

		rx_buffer = ixgbe_get_rx_buffer(rx_ring, rx_desc, &skb, size);

		/* retrieve a buffer from the ring */
		if (!skb) {
			xdp.data = page_address(rx_buffer->page) +
				   rx_buffer->page_offset;
			xdp.data_meta = xdp.data;
			xdp.data_hard_start = xdp.data -
					      ixgbe_rx_offset(rx_ring);
			xdp.data_end = xdp.data + size;
			
			//根据bfp携带的act,通过xdp对数据进行处理
			skb = ixgbe_run_xdp(adapter, rx_ring, &xdp);
		}

		if (IS_ERR(skb)) {
			unsigned int xdp_res = -PTR_ERR(skb);

			if (xdp_res & (IXGBE_XDP_TX | IXGBE_XDP_REDIR)) {
				xdp_xmit |= xdp_res;
				ixgbe_rx_buffer_flip(rx_ring, rx_buffer, size);
			} else {
				rx_buffer->pagecnt_bias++;
			}
			total_rx_packets++;
			total_rx_bytes += size;
		} else if (skb) {
			
			//该函数是将rx_buffer->page加到skb中,当buffer的size小于skb的头,就将rx_buffer->page拷贝加到skb中。
			//否则仅会将rx_buffer->page作为一个分片frag放到skb中,待下个frag来之后再处理。
			//不过为什么当size大于skb头的时候,会将该page以frag方式加入到skb中?这里应该有分片的逻辑,没有看懂。
			ixgbe_add_rx_frag(rx_ring, rx_buffer, skb, size);
		} else if (ring_uses_build_skb(rx_ring)) {
			//通过ixgbe_build_skb或者ixgbe_construct_skb创建skb,保存rx_buffer->page内的数据。
			//前者的创建函数build_skb,后者的创建函数是napi_alloc_skb
			skb = ixgbe_build_skb(rx_ring, rx_buffer,
					      &xdp, rx_desc);
		} else {
			skb = ixgbe_construct_skb(rx_ring, rx_buffer,
						  &xdp, rx_desc);
		}

		/* exit if we failed to retrieve a buffer */
		if (!skb) {
			rx_ring->rx_stats.alloc_rx_buff_failed++;
			rx_buffer->pagecnt_bias++;
			break;
		}
		
        //rx_buffer回收,在这里清空rx_buffer
		ixgbe_put_rx_buffer(rx_ring, rx_buffer, skb);
		cleaned_count++;


		/* place incomplete frames back on ring for completion */
		//确认当前的buffer是否是是该数据帧的最后一个packet(eop,end of packet)
		//如果不是的话,则继续并将两个skb合并起来,并将该skb放到下一次buffer中进行处理,结构体为rx_buffer_info[ntc].skb。
		if (ixgbe_is_non_eop(rx_ring, rx_desc, skb))
			continue;

		/* verify the packet layout is correct */
		//检查完整性
		if (ixgbe_cleanup_headers(rx_ring, rx_desc, skb))
			continue;

		/* probably a little skewed due to removing CRC */
		total_rx_bytes += skb->len;

		/* populate checksum, timestamp, VLAN, and protocol */
		//设置 Hash、checksum、timestamp、VLAN id 等信息,这些信息是硬件提供的。
		ixgbe_process_skb_fields(rx_ring, rx_desc, skb);

#ifdef IXGBE_FCOE
		/* if ddp, not passing to ULD unless for FCP_RSP or error */
		if (ixgbe_rx_is_fcoe(rx_ring, rx_desc)) {
			ddp_bytes = ixgbe_fcoe_ddp(adapter, rx_desc, skb);
			/* include DDPed FCoE data */
			if (ddp_bytes > 0) {
				if (!mss) {
					mss = rx_ring->netdev->mtu -
						sizeof(struct fcoe_hdr) -
						sizeof(struct fc_frame_header) -
						sizeof(struct fcoe_crc_eof);
					if (mss > 512)
						mss &= ~511;
				}
				total_rx_bytes += ddp_bytes;
				total_rx_packets += DIV_ROUND_UP(ddp_bytes,
								 mss);
			}
			if (!ddp_bytes) {
				dev_kfree_skb_any(skb);
				continue;
			}
		}

#endif /* IXGBE_FCOE */

		//通过该函数将skb交由上层网络协议栈处理
		ixgbe_rx_skb(q_vector, skb);

		/* update budget accounting */
		total_rx_packets++;
	}

	if (xdp_xmit & IXGBE_XDP_REDIR)
		xdp_do_flush_map();

	if (xdp_xmit & IXGBE_XDP_TX) {
		struct ixgbe_ring *ring = adapter->xdp_ring[smp_processor_id()];

		/* Force memory writes to complete before letting h/w
		 * know there are new descriptors to fetch.
		 */
		wmb();
		writel(ring->next_to_use, ring->tail);
	}

	u64_stats_update_begin(&rx_ring->syncp);
	rx_ring->stats.packets += total_rx_packets;
	rx_ring->stats.bytes += total_rx_bytes;
	u64_stats_update_end(&rx_ring->syncp);
	q_vector->rx.total_packets += total_rx_packets;
	q_vector->rx.total_bytes += total_rx_bytes;

	return total_rx_packets;
}

四、参考资料

Linux 网络协议栈收消息过程
skbuff详解及功能分析
skb的分配以及释放
SKB 的分配细节
Linux网络协议栈:NAPI机制与处理流程分析(图解)
linux中断处理-----NAPI机制

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值