内核是如何发送数据包

news2024/11/25 10:24:59

1、网络发包总览

网络发包总流程图如下:
在这里插入图片描述
从上图中可以看到用户数据被拷贝到内核态,然后经过协议栈处理后进入RingBuffer。随后网卡驱动真正的将数据发送了出去。当发送完成的时候,是通过硬中断来通知CPU,然后清理RingBuffer。

下面从源码的角度给出一个流程图。
在这里插入图片描述

2、网卡启动准备

现在服务器上的网卡一般都是支持多队列的。每一个队列是由一个RingBuffer表示的,开启了多队列以后的网卡就会对应有多个RingBuffer。网卡启动时的最重要任务之一就是分配和初始化RingBuffer.
在网卡启动时,会调用__igb_open函数,RingBuffer就是在这里分配的。

static int __igb_open(struct net_device *netdev, bool resuming)
{
	struct igb_adapter *adapter = netdev_priv(netdev);
	struct e1000_hw *hw = &adapter->hw;
	struct pci_dev *pdev = adapter->pdev;
	//分配传输描述符数组
	err = igb_setup_all_tx_resources(adapter);
	//分配接收描述符数组
	err = igb_setup_all_rx_resources(adapter);
	//中断注册,igb_msix_ring就是在这里进行注册的
	err = igb_request_irq(adapter);
	//开启全部队列
	netif_tx_start_all_queues(netdev);
	..
}
static int igb_setup_all_tx_resources(struct igb_adapter *adapter)
{
	struct pci_dev *pdev = adapter->pdev;
	int i, err = 0;

	for (i = 0; i < adapter->num_tx_queues; i++) {
		//有几个队列就构造几个RingBuffer
		err = igb_setup_tx_resources(adapter->tx_ring[i]);
		if (err) {
			dev_err(&pdev->dev,
				"Allocation for Tx Queue %u failed\n", i);
			for (i--; i >= 0; i--)
				igb_free_tx_resources(adapter->tx_ring[i]);
			break;
		}
	}

	return err;
}
int igb_setup_tx_resources(struct igb_ring *tx_ring)
{
	//申请igb_tx_buffer数组内存
	size = sizeof(struct igb_tx_buffer) * tx_ring->count;
	tx_ring->tx_buffer_info = vmalloc(size);
	if (!tx_ring->tx_buffer_info)
		goto err;

	//申请e1000_adv_tx_desc DMA数组内存
	tx_ring->size = tx_ring->count * sizeof(union e1000_adv_tx_desc);
	tx_ring->size = ALIGN(tx_ring->size, 4096);
	tx_ring->desc = dma_alloc_coherent(dev, tx_ring->size,
					   &tx_ring->dma, GFP_KERNEL);

	//初始化队列成员
	tx_ring->next_to_use = 0;
	tx_ring->next_to_clean = 0;
	...
}

上面__igb_open调用igb_setup_all_tx_resources分配所有传输的RingBuffer,调用igb_setup_all_rx_resources创建所有的接收RingBuffer。真正的RingBuffer构建是在igb_setup_tx_resources完成的。从上述源码可以看到一个传输RingBuffer的内部包含两个环形数组:igb_tx_buffer数组是内核使用;e1000_adv_tx_desc数组是硬件网卡使用。这两个数组在发送的时候,相同位置的指针都将指向同一个skb。这样内核和硬件就能共同访问同样的数据了,内核往skb写数据,网卡硬件负责发送。
硬中断的处理函数igb_msix_ring也是在__igb_open中注册的。

3、数据从用户进程到网卡的详细过程

3.1 send系统调用实现

send系统调用源码位于net/socket.c中,其内部实际使用的是sendto系统调用。该函数主要干了两件事:在内核中将真正的socket找出来,在这个对象里记录了各种协议栈的函数地址;构造一个struct msghdr对象,把用户传入的数据copy进去。之后就调用inet_sendmsg了。大致流程如下图:
在这里插入图片描述
源码如下:

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
		unsigned int, flags)
{
	return sys_sendto(fd, buff, len, flags, NULL, 0);
}

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
		unsigned int, flags, struct sockaddr __user *, addr,
		int, addr_len)
{
	struct socket *sock;
	struct msghdr msg;

	err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
	if (unlikely(err))
		return err;
	//1.根据fd找到socket
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	
	//2.构造msghdr
	msg.msg_name = NULL;
	msg.msg_control = NULL;
	msg.msg_controllen = 0;
	msg.msg_namelen = 0;
	if (addr) {
		err = move_addr_to_kernel(addr, addr_len, &address);
		if (err < 0)
			goto out_put;
		msg.msg_name = (struct sockaddr *)&address;
		msg.msg_namelen = addr_len;
	}
	if (sock->file->f_flags & O_NONBLOCK)
		flags |= MSG_DONTWAIT;
	msg.msg_flags = flags;
	//3.发送数据
	err = sock_sendmsg(sock, &msg);
}

从源码可以看到,send只是sendto封装出来的。在sendto系统调用里,首先根据用户传进来的句柄号来查找真正的socket对象,接着将用户请求的buf、len、flag等参数打包成一个msghdr对象。接着调用了sock_sendmsg==>sock_sendmsg_nosec,在sock_sendmsg_nosec中进入协议栈。

static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
	//实际调用的是inet_sendmsg
	int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));
	BUG_ON(ret == -EIOCBQUEUED);
	return ret;
}

3.2 传输层处理

传输层发送流程大致如下:
在这里插入图片描述

3.2.1 传输层拷贝

int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
	...
	return sk->sk_prot->sendmsg(sk, msg, size);
}

对于TCP的socket来说,sk->sk_prot->sendmsg实际上是指向tcp_sendmsg(对于UDP的socket来说实际上是udp_sendmsg)。
由于这个函数比较长,下面分开进行理解。

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
	// 开始发送数据
	copied = 0;
restart:
	mss_now = tcp_send_mss(sk, &size_goal, flags); // 获取当前 MSS 和目标大小

	while (msg_data_left(msg)) {
		int copy = 0;
		int max = size_goal;

		skb = tcp_write_queue_tail(sk); // 获取发送队列尾部的 sk_buff
		if (tcp_send_head(sk)) {
			if (skb->ip_summed == CHECKSUM_NONE)
				max = mss_now;
			copy = max - skb->len;
		}

		if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
			bool first_skb;

new_segment:
			// 分配新的 sk_buff
			if (!sk_stream_memory_free(sk))
				goto wait_for_sndbuf;

			if (process_backlog && sk_flush_backlog(sk)) {
				process_backlog = false;
				goto restart;
			}
			first_skb = skb_queue_empty(&sk->sk_write_queue);
			//申请skb
			skb = sk_stream_alloc_skb(sk,
						  select_size(sk, sg, first_skb),
						  sk->sk_allocation,
						  first_skb);
			if (!skb)
				goto wait_for_memory;

			process_backlog = true;
			if (sk_check_csum_caps(sk))
				skb->ip_summed = CHECKSUM_PARTIAL;
			//把skb挂到socket的发送队列末尾
			skb_entail(sk, skb);
			copy = size_goal;
			max = size_goal;

			if (tp->repair)
				TCP_SKB_CB(skb)->sacked |= TCPCB_REPAIRED;
		}

		
		if (copy > msg_data_left(msg))
			copy = msg_data_left(msg);
		
		//如果skb有空余空间,则将msg存储的数据copy到skb中
		if (skb_availroom(skb) > 0) {
			copy = min_t(int, copy, skb_availroom(skb));
			//将用户空间的数据拷贝到内核空间,同时计算教育和
			err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);
			if (err)
				goto do_fault;
		} 
		...
		
		//更新seq
		tp->write_seq += copy;
		TCP_SKB_CB(skb)->end_seq += copy;
		tcp_skb_pcount_set(skb, 0);

wait_for_sndbuf:
		set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
		if (copied)
			tcp_push(sk, flags & ~MSG_MORE, mss_now,
				 TCP_NAGLE_PUSH, size_goal);
		//socket发送缓存不足时,如果是阻塞套接字会陷入等待
		err = sk_stream_wait_memory(sk, &timeo);
		if (err != 0)
			goto do_error;

		mss_now = tcp_send_mss(sk, &size_goal, flags);
	}

out:
	if (copied) {
		tcp_tx_timestamp(sk, sockc.tsflags, tcp_write_queue_tail(sk));
		tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
	}
out_nopush:
	release_sock(sk);
	//返回已复制的长度
	return copied + copied_syn;
}

上面的源码主要是将用户层的数据拷贝到socket的发送缓存队列末尾,如果socket缓存空间不够,而socket又是阻塞模式的就会陷入等待直到超时或者条件满足。在这个copy步骤中,如果用户层发送的数据长度超过mss,则会进行多次分割copy。

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
	while (msg_data_left(msg)) {

		//发送判断
		if (forced_push(tp)) {
			tcp_mark_push(tp, skb);
			__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
		} else if (skb == tcp_send_head(sk))
			tcp_push_one(sk, mss_now);
		continue;
	}
	...
}

在发送只有满足forced_push(tp)或skb == tcp_send_head(sk)成立时,内核才会启动发送数据包。其中forced_push(tp)判断的时未发送的数据是否超过最大窗口的一半,skb == tcp_send_head(sk)判断的是队列最末尾的skb是不是待发送的skb。
条件不满足的话只是将用户数据拷贝到socket的发送队列。

3.2.2 传输层发送

tcp_write_xmit 是 Linux 内核 TCP 协议栈中用于处理数据包发送的核心函数。它负责从 TCP 套接字的发送队列中取出数据包,并在满足特定条件时通过 IP 层发送它们。这个函数处理了多种情况,包括 MTU 探测、拥塞窗口测试、发送窗口测试、Nagle 算法、TSO(TCP Segmentation Offload)处理等。

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
			   int push_one, gfp_t gfp)
{
	struct tcp_sock *tp = tcp_sk(sk); // 获取 TCP 特定的 sock 结构
	struct sk_buff *skb; // 指向待发送的 sk_buff 结构
	unsigned int tso_segs, sent_pkts; // TSO 分段数,已发送数据包数
	int cwnd_quota; // 拥塞窗口配额
	int result; // 用于存储函数返回值
	bool is_cwnd_limited = false, is_rwnd_limited = false; // 拥塞窗口和发送窗口限制标志
	u32 max_segs; // 最大分段数

	sent_pkts = 0; // 初始化已发送数据包数

	if (!push_one) {
		// 执行 MTU 探测
		result = tcp_mtu_probe(sk);
		if (!result) {
			return false; // 如果探测失败,返回 false
		} else if (result > 0) {
			sent_pkts = 1; // 如果探测成功,增加已发送数据包数
		}
	}

	max_segs = tcp_tso_segs(sk, mss_now); // 计算 TSO 分段数
	while ((skb = tcp_send_head(sk))) { // 循环处理发送队列头部的数据包
		unsigned int limit; // 发送限制

		tso_segs = tcp_init_tso_segs(skb, mss_now); // 初始化 TSO 分段数
		BUG_ON(!tso_segs); // 确保 TSO 分段数有效

		if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
			// 如果需要修复,跳过网络传输
			skb_mstamp_get(&skb->skb_mstamp);
			goto repair;
		}

		// 测试拥塞窗口是否足够
		cwnd_quota = tcp_cwnd_test(tp, skb);
		if (!cwnd_quota) {
			if (push_one == 2) {
				// 强制发送一个丢包探测数据包
				cwnd_quota = 1;
			} else {
				break; // 如果拥塞窗口不足,退出循环
			}
		}

		// 测试发送窗口是否足够
		if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
			is_rwnd_limited = true; // 标记发送窗口限制
			break;
		}

		// 处理 Nagle 算法
		if (tso_segs == 1) {
			if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
						     (tcp_skb_is_last(sk, skb) ?
						      nonagle : TCP_NAGLE_PUSH)))) {
				break; // 如果 Nagle 算法不允许发送,退出循环
			}
		} else {
			// 如果 TSO 分段数大于 1,检查是否应该推迟发送
			if (!push_one &&
			    tcp_tso_should_defer(sk, skb, &is_cwnd_limited,
						 max_segs)) {
				break; // 如果应该推迟,退出循环
			}
		}

		// 计算最大发送大小
		limit = mss_now;
		if (tso_segs > 1 && !tcp_urg_mode(tp)) {
			limit = tcp_mss_split_point(sk, skb, mss_now,
						    min_t(unsigned int,
							  cwnd_quota,
							  max_segs),
						    nonagle);
		}

		// 如果数据包大小超过限制,尝试分片
		if (skb->len > limit &&
		    unlikely(tso_fragment(sk, skb, limit, mss_now, gfp))) {
			break; // 如果分片失败,退出循环
		}

		// 如果设置了 TCP_TSQ_DEFERRED 标志,清除它
		if (test_bit(TCP_TSQ_DEFERRED, &sk->sk_tsq_flags))
			clear_bit(TCP_TSQ_DEFERRED, &sk->sk_tsq_flags);

		// 检查发送队列大小是否过小
		if (tcp_small_queue_check(sk, skb, 0)) {
			break; // 如果队列太小,退出循环
		}

		// 通过 IP 层发送数据包
		if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) {
			break; // 如果发送失败,退出循环
		}

repair:
		// 更新发送头部,标记数据包已发送
		tcp_event_new_data_sent(sk, skb);

		// 更新最小序列号
		tcp_minshall_update(tp, mss_now, skb);

		// 增加已发送数据包数
		sent_pkts += tcp_skb_pcount(skb);

		if (push_one) {
			break; // 如果设置了 push_one,退出循环
		}
	}

	// 如果发送窗口限制,启动计时器
	if (is_rwnd_limited)
		tcp_chrono_start(sk, TCP_CHRONO_RWND_LIMITED);
	else
		tcp_chrono_stop(sk, TCP_CHRONO_RWND_LIMITED);

	// 如果已发送数据包,更新拥塞窗口和发送窗口
	if (likely(sent_pkts)) {
		if (tcp_in_cwnd_reduction(sk))
			tp->prr_out += sent_pkts;

		// 安排发送丢包探测
		if (push_one != 2)
			tcp_schedule_loss_probe(sk);
		is_cwnd_limited |= (tcp_packets_in_flight(tp) >= tp->snd_cwnd);
		tcp_cwnd_validate(sk, is_cwnd_limited);
		return false; // 返回 false,表示没有更多数据包需要发送
	}
	return !tp->packets_out && tcp_send_head(sk); // 如果没有数据包发送,返回 true
}
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
			    gfp_t gfp_mask)
{
	const struct inet_connection_sock *icsk = inet_csk(sk);
	struct inet_sock *inet;
	struct tcp_sock *tp;
	struct tcp_skb_cb *tcb;
	struct tcp_out_options opts;
	unsigned int tcp_options_size, tcp_header_size;
	struct tcp_md5sig_key *md5;
	struct tcphdr *th;
	int err;

	/* 确保传入的 sk_buff 有效,并且至少有一个 TCP 段被发送 */
	BUG_ON(!skb || !tcp_skb_pcount(skb));
	tp = tcp_sk(sk); /* 获取 TCP 特定的 sock 结构 */

	/* 如果需要克隆 sk_buff,进行时间戳获取和速率计算 */
	if (clone_it) {
		skb_mstamp_get(&skb->skb_mstamp);
		TCP_SKB_CB(skb)->tx.in_flight = TCP_SKB_CB(skb)->end_seq
			- tp->snd_una;
		tcp_rate_skb_sent(sk, skb);

		/* 克隆 sk_buff,为发送做准备 */
		if (unlikely(skb_cloned(skb)))
			skb = pskb_copy(skb, gfp_mask);
		else
			skb = skb_clone(skb, gfp_mask);
		if (unlikely(!skb))
			return -ENOBUFS;
	}

	/* 获取 inet_sock 和 tcp_skb_cb 结构 */
	inet = inet_sk(sk);
	tcb = TCP_SKB_CB(skb);
	memset(&opts, 0, sizeof(opts)); /* 初始化 TCP 选项 */

	/* 根据是否为 SYN 包计算 TCP 选项的大小 */
	if (unlikely(tcb->tcp_flags & TCPHDR_SYN))
		tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
	else
		tcp_options_size = tcp_established_options(sk, skb, &opts,
							   &md5);
	tcp_header_size = tcp_options_size + sizeof(struct tcphdr); /* TCP 头部总大小 */

	/* 如果没有数据包在队列中,允许 XPS 选择另一个队列 */
	skb->ooo_okay = sk_wmem_alloc_get(sk) < SKB_TRUESIZE(1);

	/* 如果使用了内存预留来分配这个 sk_buff,这可能会导致回环时的丢包 */
	skb->pfmemalloc = 0;

	/* 为 TCP 头部腾出空间 */
	skb_push(skb, tcp_header_size);
	skb_reset_transport_header(skb);

	/* 将 sk_buff 标记为孤儿,以便它不会影响原始 socket */
	skb_orphan(skb);
	skb->sk = sk; /* 设置 sk_buff 所属的 socket */
	skb->destructor = skb_is_tcp_pure_ack(skb) ? __sock_wfree : tcp_wfree; /* 设置销毁函数 */
	skb_set_hash_from_sk(skb, sk); /* 设置哈希值 */
	atomic_add(skb->truesize, &sk->sk_wmem_alloc); /* 更新内存分配统计 */

	/* 设置目的确认为挂起状态 */
	skb_set_dst_pending_confirm(skb, sk->sk_dst_pending_confirm);

	//封装TCP头
	th = (struct tcphdr *)skb->data;
	th->source		= inet->inet_sport; /* 设置源端口 */
	th->dest		= inet->inet_dport; /* 设置目的端口 */
	th->seq			= htonl(tcb->seq); /* 设置序列号 */
	th->ack_seq		= htonl(tp->rcv_nxt); /* 设置确认序列号 */
	*(((__be16 *)th) + 6)	= htons(((tcp_header_size >> 2) << 12) |
					tcb->tcp_flags); /* 设置数据偏移和控制标志 */

	th->check		= 0; /* 清除校验和 */
	th->urg_ptr		= 0; /* 清除紧急指针 */

	/* 如果处于紧急模式,设置紧急指针 */
	if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {
		if (before(tp->snd_up, tcb->seq + 0x10000)) {
			th->urg_ptr = htons(tp->snd_up - tcb->seq);
			th->urg = 1;
		} else if (after(tcb->seq + 0xFFFF, tp->snd_nxt)) {
			th->urg_ptr = htons(0xFFFF);
			th->urg = 1;
		}
	}

	/* 写入 TCP 选项 */
	tcp_options_write((__be32 *)(th + 1), tp, &opts);
	skb_shinfo(skb)->gso_type = sk->sk_gso_type; /* 设置 GSO 类型 */

	/* 设置窗口大小并处理 ECN */
	if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
		th->window      = htons(tcp_select_window(sk));
		tcp_ecn_send(sk, skb, th, tcp_header_size);
	} else {
		/* RFC1323: SYN & SYN/ACK 段的窗口不进行缩放 */
		th->window	= htons(min(tp->rcv_wnd, 65535U));
	}
#ifdef CONFIG_TCP_MD5SIG
	/* 如果启用了 MD5 签名,计算 MD5 哈希 */
	if (md5) {
		sk_nocaps_add(sk, NETIF_F_GSO_MASK);
		tp->af_specific->calc_md5_hash(opts.hash_location,
					       md5, sk, skb);
	}
#endif

	/* 调用地址族特定的发送检查函数 */
	icsk->icsk_af_ops->send_check(sk, skb);

	/* 如果发送了 ACK,记录事件 */
	if (likely(tcb->tcp_flags & TCPHDR_ACK))
		tcp_event_ack_sent(sk, tcp_skb_pcount(skb));

	/* 如果发送了数据,记录事件 */
	if (skb->len != tcp_header_size) {
		tcp_event_data_sent(tp, sk);
		tp->data_segs_out += tcp_skb_pcount(skb);
	}

	/* 更新统计信息 */
	if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
		TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
			      tcp_skb_pcount(skb));

	tp->segs_out += tcp_skb_pcount(skb); /* 更新段输出计数 */

	/* 设置 GSO 分段信息 */
	skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);
	skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);

	/* 清除时间戳 */
	skb->tstamp = 0;

	/* 清理 IP 栈的控制块 */
	memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),
			       sizeof(struct inet6_skb_parm)));

	//调用网络层发送接口
	err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);

	/* 处理发送结果 */
	if (likely(err <= 0))
		return err;

	/* 进入拥塞控制恢复模式 */
	tcp_enter_cwr(sk);

	/* 评估网络传输结果 */
	return net_xmit_eval(err);
}

tcp_transmit_skb的第一件事是克隆一个新的skb,这是因为最后到达网卡发送完成的时候,这个skb会被释放掉,而TCP支持丢失重传,所以传给网卡的是skb的一个拷贝,等收到ACK再真正删除。第二件事就是修改skb中的TCP头。这里的skb内部其实包含了网络协议所有的头,在网络协议栈不同层次传输时只需要移动指针即可,避免了频繁的内存申请和拷贝,提高了效率。第三步就是将数据交给网络层了。icsk->icsk_af_ops->queue_xmit实际是调用了ip_queue_xmit函数。

3.3 网络层发送处理

在网络层主要处理路由项查找、IP设置、netfilter过滤、skb切分(大于MTU的话)等工作。处理完这些工作之后交给更下一层的邻居子系统来处理。

int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
	struct inet_sock *inet = inet_sk(sk);
	struct net *net = sock_net(sk);
	struct ip_options_rcu *inet_opt;
	struct flowi4 *fl4;
	struct rtable *rt;
	struct iphdr *iph;
	int res;

	/* 如果数据包已经被路由,跳过路由选择 */
	rcu_read_lock();
	inet_opt = rcu_dereference(inet->inet_opt);
	fl4 = &fl->u.ip4;
	rt = skb_rtable(skb);
	if (rt)
		goto packet_routed;

	//检查socket是否有缓存的路由表
	rt = (struct rtable *)__sk_dst_check(sk, 0);
	if (!rt) {
		__be32 daddr;

		/* 如果我们有选项,使用正确的目的地地址 */
		daddr = inet->inet_daddr;
		if (inet_opt && inet_opt->opt.srr)
			daddr = inet_opt->opt.faddr;

		/* 如果路由选择失败,传输层的重传机制会不断尝试查找路由项直到路由出现或连接超时 */
		rt = ip_route_output_ports(net, fl4, sk,
					   daddr, inet->inet_saddr,
					   inet->inet_dport,
					   inet->inet_sport,
					   sk->sk_protocol,
					   RT_CONN_FLAGS(sk),
					   sk->sk_bound_dev_if);
		if (IS_ERR(rt))
			goto no_route;
		sk_setup_caps(sk, &rt->dst);
	}
	//为skb设置路由表
	skb_dst_set_noref(skb, &rt->dst);

packet_routed:
	if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
		goto no_route;

	/* 确定我们要发送的位置,分配并构建 IP 头部 */
	skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
	skb_reset_network_header(skb);
	iph = ip_hdr(skb);
	*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
	if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)
		iph->frag_off = htons(IP_DF);
	else
		iph->frag_off = 0;
	iph->ttl      = ip_select_ttl(inet, &rt->dst);
	iph->protocol = sk->sk_protocol;
	ip_copy_addrs(iph, fl4);
	...
	//发送
	res = ip_local_out(net, sk, skb);
	...
}

ip_queue_xmit函数进行了路由项查找,在路由表查到某个网络应该通过哪个网卡、哪个网关发送出去之后,就缓存到socket上,避免重复查询。接着把路由表地址也缓存到skb中。然后按照规范填充IP头,再通过ip_finish_output进入下一步处理。

int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	int err;
	//执行netfilter过滤
	err = __ip_local_out(net, sk, skb);
	if (likely(err == 1))
		//开始发送数据
		err = dst_output(net, sk, skb);
	return err;
}

在调用ip_local_out=>__ip_local_out=>nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT的过程中会执行netfilter过滤。

static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	return skb_dst(skb)->output(net, sk, skb);
}

此函数找到skb的路由表(dst条目),然后调用路由表的output方法。指向的是ip_output方法。

int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	struct net_device *dev = skb_dst(skb)->dev;
	//统计
	IP_UPD_PO_STATS(net, IPSTATS_MIB_OUT, skb->len);

	skb->dev = dev;
	skb->protocol = htons(ETH_P_IP);
	//再次交给netfilter,完毕回调ip_finish_output
	return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
			    net, sk, skb, NULL, dev,
			    ip_finish_output,
			    !(IPCB(skb)->flags & IPSKB_REROUTED));
}
static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	unsigned int mtu;
	int ret;
	...

	/* 获取数据包的目的 MTU(最大传输单元) */
	mtu = ip_skb_dst_mtu(sk, skb);
	if (skb_is_gso(skb))
		return ip_finish_output_gso(net, sk, skb, mtu);

	/* 检查数据包长度是否大于 MTU 或者需要分片 */
	if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU))
		return ip_fragment(net, sk, skb, mtu, ip_finish_output2);

	/* 如果不需要分片,直接调用 ip_finish_output2 发送数据包 */
	return ip_finish_output2(net, sk, skb);
}
static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	struct dst_entry *dst = skb_dst(skb); // 获取数据包的路由条目
	struct rtable *rt = (struct rtable *)dst; // 转换路由条目
	struct net_device *dev = dst->dev; // 获取网络设备
	unsigned int hh_len = LL_RESERVED_SPACE(dev); // 计算硬件头部空间
	struct neighbour *neigh; // 邻居发现条目
	u32 nexthop; // 下一跳地址

	// 统计多播或广播数据包
	if (rt->rt_type == RTN_MULTICAST) {
		IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTMCAST, skb->len);
	} else if (rt->rt_type == RTN_BROADCAST)
		IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTBCAST, skb->len);

	// 检查数据包头部空间是否足够
	if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) {
		struct sk_buff *skb2;

		skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));
		if (!skb2) {
			kfree_skb(skb); // 如果分配失败,释放数据包
			return -ENOMEM;
		}
		if (skb->sk)
			skb_set_owner_w(skb2, skb->sk); // 设置数据包所有者
		consume_skb(skb); // 释放旧的数据包
		skb = skb2; // 更新数据包指针
	}

	// 处理隧道重定向
	if (lwtunnel_xmit_redirect(dst->lwtstate)) {
		int res = lwtunnel_xmit(skb);
		if (res < 0 || res == LWTUNNEL_XMIT_DONE)
			return res;
	}

	// 获取下一跳邻居
	rcu_read_lock_bh(); // 锁定 RCU 读锁
	//根据下一跳的IP地址查找邻居项,找不到就创建一个
	nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
	neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
	if (unlikely(!neigh))
		neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
	if (!IS_ERR(neigh)) {
		int res;

		sock_confirm_neigh(skb, neigh); // 确认邻居
		res = neigh_output(neigh, skb); // 发送数据包

		rcu_read_unlock_bh(); // 解锁 RCU 读锁
		return res;
	}
	rcu_read_unlock_bh(); // 解锁 RCU 读锁

	net_dbg_ratelimited("%s: No header cache and no neighbour!\n", __func__);
	kfree_skb(skb); // 释放数据包
	return -EINVAL; // 返回错误码
}

3.4 邻居子系统

static inline int neigh_output(struct neighbour *n, struct sk_buff *skb)
{
	const struct hh_cache *hh = &n->hh;

	if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
		return neigh_hh_output(hh, skb);
	else
		//
		return n->output(n, skb);
}

如果是新创建的邻居表项,此时因为目的MAC还未获取,不具备发送IP报文的能力,调用n->output,实际是调用neigh_resolve_output,在这个函数内部有可能发出Arp网络请求。

int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb) {
    int rc = 0; // 初始化返回值,0 表示成功

    // 尝试发送邻居事件,如果失败则继续处理
    if (!neigh_event_send(neigh, skb)) {
        int err; // 用于存储错误代码的变量
        struct net_device *dev = neigh->dev; // 获取邻居结构体中的网络设备指针
        unsigned int seq; // 用于序列化访问的变量

        // 如果网络设备支持缓存并且邻居的硬件头部长度为0,则初始化硬件头部
        if (dev->header_ops->cache && !neigh->hh.hh_len)
            neigh_hh_init(neigh);

        // 循环直到硬件头部正确填充或序列化访问失败
        do {
            // 调整数据包指针,使其指向网络层头部
            __skb_pull(skb, skb_network_offset(skb));
            seq = read_seqbegin(&neigh->ha_lock); // 开始序列化访问
            // 调用网络设备的硬头部函数来填充数据包的硬件头部
            err = dev_hard_header(skb, dev, ntohs(skb->protocol), neigh->ha, NULL, skb->len);
        } while (read_seqretry(&neigh->ha_lock, seq)); // 如果序列化访问失败则重试

        // 如果硬头部填充成功,则将数据包放入网络设备的发送队列
        if (err >= 0)
            rc = dev_queue_xmit(skb);
    }
	...
}
static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
{
	unsigned int seq;
	unsigned int hh_len;

	do {
		seq = read_seqbegin(&hh->hh_lock); // 开始读取序列,用于乐观并发读取
		hh_len = hh->hh_len; // 获取硬件头部长度
		if (likely(hh_len <= HH_DATA_MOD)) {
			// 如果硬件头部长度小于等于 HH_DATA_MOD,直接复制
			memcpy(skb->data - HH_DATA_MOD, hh->hh_data, HH_DATA_MOD);
		} else {
			unsigned int hh_alen = HH_DATA_ALIGN(hh_len); // 对齐硬件头部长度

			// 复制对齐后的硬件头部
			memcpy(skb->data - hh_alen, hh->hh_data, hh_alen);
		}
	} while (read_seqretry(&hh->hh_lock, seq)); // 检查序列是否重试

	// 将硬件头部推入数据包
	skb_push(skb, hh_len);
	return dev_queue_xmit(skb); // 将数据包加入到设备的发送队列
}

当获取到硬件MAC地址之后,就可以封装skb的MAC头。最后调用dev_queue_xmit将skb传递给Linux网络设备子系统。

3.5 网络设备子系统

int dev_queue_xmit(struct sk_buff *skb)
{
	return __dev_queue_xmit(skb, NULL);
}

static int __dev_queue_xmit(struct sk_buff *skb, void *accel_priv) {
    struct net_device *dev = skb->dev; // 获取数据包关联的网络设备
    struct netdev_queue *txq; // 指向网络设备队列的结构体
    struct Qdisc *q; // 指向队列规则(qdisc)的结构体
    int rc = -ENOMEM; // 初始化返回值为-ENOMEM,表示内存分配失败

    skb_reset_mac_header(skb); // 重置数据包的MAC层头部

    // 如果数据包需要时间戳,则设置时间戳
    if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_SCHED_TSTAMP))
        __skb_tstamp_tx(skb, NULL, skb->sk, SCM_TSTAMP_SCHED);

    // 禁用软中断,获取RCU读锁
    rcu_read_lock_bh();

    skb_update_prio(skb); // 更新数据包的优先级

    qdisc_pkt_len_init(skb); // 初始化队列规则的长度

    // 如果配置了网络分类和动作支持
#ifdef CONFIG_NET_CLS_ACT
    skb->tc_at_ingress = 0;
    // 如果需要处理出口数据
# ifdef CONFIG_NET_EGRESS
    if (static_key_false(&egress_needed)) {
        skb = sch_handle_egress(skb, &rc, dev);
        if (!skb)
            goto out; // 如果处理失败,跳转到out标签
    }
# endif
#endif

    // 如果设备或队列规则不需要数据包的目的地址,则立即释放
    if (dev->priv_flags & IFF_XMIT_DST_RELEASE)
        skb_dst_drop(skb);
    else
        skb_dst_force(skb); // 否则强制设置目的地址

    // 选择一个发送队列
    txq = netdev_pick_tx(dev, skb, accel_priv);
    // 获取此队列关联的排队规则
    q = rcu_dereference_bh(txq->qdisc); 

    trace_net_dev_queue(skb); // 跟踪网络设备队列

    // 如果队列规则有enqueue函数,则调用__dev_xmit_skb继续处理
    if (q->enqueue) {
        rc = __dev_xmit_skb(skb, q, dev, txq);
        goto out; // 跳转到out标签
    }

    // 如果设备没有队列规则,通常是软件设备的情况
    if (dev->flags & IFF_UP) {
        int cpu = smp_processor_id(); // 获取当前CPU编号

        // 如果当前CPU不是队列的锁持有者
        if (txq->xmit_lock_owner != cpu) {
			...
            HARD_TX_LOCK(dev, txq, cpu); // 获取硬件发送锁

            // 如果队列没有停止,则发送数据包
            if (!netif_xmit_stopped(txq)) {
                __this_cpu_inc(xmit_recursion); // 增加递归计数
                skb = dev_hard_start_xmit(skb, dev, txq, &rc); // 调用硬件发送函数
                __this_cpu_dec(xmit_recursion); // 减少递归计数
                if (dev_xmit_complete(rc)) {
                    HARD_TX_UNLOCK(dev, txq); // 释放硬件发送锁
                    goto out; // 跳转到out标签
                }
            }
            HARD_TX_UNLOCK(dev, txq); // 释放硬件发送锁
            net_crit_ratelimited("Virtual device %s asks to queue packet!\n", dev->name);
        } 
        ...
}
static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
                                  struct net_device *dev,
                                  struct netdev_queue *txq) {
    spinlock_t *root_lock = qdisc_lock(q); // 获取队列规则的锁
    struct sk_buff *to_free = NULL; // 用于存储需要释放的数据包
    bool contended; // 标记是否在竞争状态
    int rc; // 用于存储返回值

    qdisc_calculate_pkt_len(skb, q); // 计算数据包的长度

    // 如果队列规则正在运行,尝试获取忙锁以序列化竞争
    contended = qdisc_is_running(q);
    if (unlikely(contended))
        spin_lock(&q->busylock); // 获取忙锁

    spin_lock(root_lock); // 获取队列规则的主锁
    // 如果队列规则被停用,则丢弃数据包
    if (unlikely(test_bit(__QDISC_STATE_DEACTIVATED, &q->state))) {
        __qdisc_drop(skb, &to_free); // 丢弃数据包
        rc = NET_XMIT_DROP; // 设置返回值为丢弃
    } else if ((q->flags & TCQ_F_CAN_BYPASS) && !qdisc_qlen(q) &&
               qdisc_run_begin(q)) { // 如果队列规则允许绕过并且队列为空
        // 直接发送数据包,不经过队列
        qdisc_bstats_update(q, skb); // 更新队列规则的统计信息

        if (sch_direct_xmit(skb, q, dev, txq, root_lock, true)) {
            // 如果直接发送成功
            if (unlikely(contended)) {
                spin_unlock(&q->busylock); // 释放忙锁
                contended = false; // 标记不再竞争
            }
            __qdisc_run(q); // 运行队列规则
        } else {
            qdisc_run_end(q); // 结束队列规则的运行
        }
        rc = NET_XMIT_SUCCESS; // 设置返回值为成功
    } else {
        // 将数据包加入队列
        rc = q->enqueue(skb, q, &to_free) & NET_XMIT_MASK;
        if (qdisc_run_begin(q)) {
            if (unlikely(contended)) {
                spin_unlock(&q->busylock); // 释放忙锁
                contended = false; // 标记不再竞争
            }
            __qdisc_run(q); // 运行队列规则
        }
    }
    spin_unlock(root_lock); // 释放队列规则的主锁
    if (unlikely(to_free)) // 如果有数据包需要释放
        kfree_skb_list(to_free); // 释放数据包
    if (unlikely(contended)) // 如果在竞争状态
        spin_unlock(&q->busylock); // 释放忙锁
    return rc; // 返回结果
}

__dev_xmit_skb函数的主要任务是将数据包发送到网络设备。它首先检查队列规则的状态,如果队列规则允许并且队列为空,它将尝试直接发送数据包。否则,它将数据包加入队列。在处理过程中,它还处理了竞争状态,以确保在多核处理器上正确地序列化访问。

void __qdisc_run(struct Qdisc *q)
{
    int quota = dev_tx_weight; // 从设备传输权重获取配额,这个配额限制了处理数据包的数量
    int packets; // 用于存储本轮处理的数据包数量

    while (qdisc_restart(q, &packets)) { // 循环,直到没有更多数据包可以处理
        // 根据可能的发生顺序排序:如果1.我们超出了数据包配额 2.另一个进程需要CPU,则推迟处理
        quota -= packets; // 从配额中减去本轮处理的数据包数量
        if (quota <= 0 || need_resched()) { // 如果配额用完或者系统需要调度其他进程运行
            __netif_schedule(q); // 调度网络设备,以便其他进程可以运行
            break; // 跳出循环
        }
    }

    qdisc_run_end(q); // 结束队列规则的运行,执行任何清理工作
}

从上述代码可以看到while循环不断的从队列取skb并进行发送。
注意:此时占用的是用户进程的系统态实际(sy)。只有当quota用尽或者需要调度其他进程运行时才触发软中断进行发送。
所以这就是在服务器上查看/proc/softirqs时,一般NET_RX比NET_TX大得多的原因。

static inline int qdisc_restart(struct Qdisc *q, int *packets)
{
	...
	//取出一个要发送的skb
	skb = dequeue_skb(q, &validate, packets);
	if (unlikely(!skb))
		return 0;

	root_lock = qdisc_lock(q);
	dev = qdisc_dev(q);
	txq = skb_get_tx_queue(dev, skb);

	return sch_direct_xmit(skb, q, dev, txq, root_lock, validate);
}

qdisc_restart从队列中取出一个skb,并调用sch_direct_xmit继续发送。

int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
                    struct net_device *dev, struct netdev_queue *txq,
                    spinlock_t *root_lock, bool validate) {
    int ret = NETDEV_TX_BUSY; // 初始化返回值为 NETDEV_TX_BUSY,表示设备忙

    // 释放 qdisc 锁
    spin_unlock(root_lock);

    // 在没有锁的情况下验证 skb(GSO, 校验和, ...)
    if (validate)
        skb = validate_xmit_skb_list(skb, dev); // 验证要发送的 skb

    if (likely(skb)) { // 如果 skb 有效
        HARD_TX_LOCK(dev, txq, smp_processor_id()); // 获取硬件发送锁
        if (!netif_xmit_frozen_or_stopped(txq))
            skb = dev_hard_start_xmit(skb, dev, txq, &ret); // 调用驱动程序尝试直接发送 skb

        HARD_TX_UNLOCK(dev, txq); // 释放硬件发送锁
    } else {
        spin_lock(root_lock); // 如果 skb 无效,重新获取 qdisc 锁
        return qdisc_qlen(q); // 返回队列长度
    }
    spin_lock(root_lock); // 重新获取 qdisc 锁

    if (dev_xmit_complete(ret)) { // 如果发送完成
        // 驱动程序成功发送了 skb 或者 skb 被消耗
        ret = qdisc_qlen(q); // 返回队列长度
    } else {
        // 驱动程序返回 NETDEV_TX_BUSY - 重新排队 skb
        if (unlikely(ret != NETDEV_TX_BUSY))
            net_warn_ratelimited("BUG %s code %d qlen %d\n",
                                 dev->name, ret, q->q.qlen); // 如果返回值不是 NETDEV_TX_BUSY,发出警告

        ret = dev_requeue_skb(skb, q); // 将 skb 重新排队
    }

    if (ret && netif_xmit_frozen_or_stopped(txq))
        ret = 0; // 如果设备被冻结或停止,返回 0

    return ret; // 返回最终的返回值
}

这个函数主要工作是调用dev_hard_start_xmit驱动函数发送skb,如果发送失败将skb重新加入队列。

3.6 软中断调度

在上面我们看到当用户进程配额用尽时会调用__netif_schedule触发一个软中断。该函数会进入__netif_reschedule发出一个NET_TX_SOFTIRQ类型中断。
软中断由内核进程运行,该进程会进入net_tx_action函数,在该函数能获取发送队列,并也最终调用到驱动程序里的入口函数dev_hard_start_xmit,如下图所示:
在这里插入图片描述

static void __netif_reschedule(struct Qdisc *q)
{
    struct softnet_data *sd; // 定义一个指向软网数据结构的指针
    unsigned long flags; // 用于保存当前中断状态的标志

    local_irq_save(flags); // 保存当前中断状态并禁用本地中断
    sd = this_cpu_ptr(&softnet_data); // 获取当前 CPU 的软网数据结构

    q->next_sched = NULL; // 将队列规则的下一个调度指针设置为 NULL
    *sd->output_queue_tailp = q; // 将队列规则添加到输出队列的尾部
    sd->output_queue_tailp = &q->next_sched; // 更新输出队列的尾指针

    raise_softirq_irqoff(NET_TX_SOFTIRQ); // 触发网络传输软中断
    local_irq_restore(flags); // 恢复之前的中断状态
}

在这里设置了将保存了发送队列的q添加到CPU的soft_data的发送队列尾部,然后触发软中断。

static __latent_entropy void net_tx_action(struct softirq_action *h)
{
    struct softnet_data *sd = this_cpu_ptr(&softnet_data); // 获取当前 CPU 的软网数据结构

    if (sd->completion_queue) { // 如果有完成队列需要处理
        struct sk_buff *clist;

        local_irq_disable(); // 禁用本地中断
        clist = sd->completion_queue; // 获取完成队列
        sd->completion_queue = NULL; // 清空完成队列
        local_irq_enable(); // 启用本地中断

        while (clist) { // 遍历完成队列
            struct sk_buff *skb = clist; // 获取当前数据包

            clist = clist->next; // 移动到下一个数据包

            WARN_ON(atomic_read(&skb->users)); // 如果数据包的使用者计数器不为0,则发出警告
            if (likely(get_kfree_skb_cb(skb)->reason == SKB_REASON_CONSUMED))
                trace_consume_skb(skb); // 如果数据包已被消耗,则跟踪
            else
                trace_kfree_skb(skb, net_tx_action); // 否则,跟踪数据包的释放

            if (skb->fclone != SKB_FCLONE_UNAVAILABLE)
                __kfree_skb(skb); // 如果数据包不是克隆的,则立即释放
            else
                __kfree_skb_defer(skb); // 否则,延迟释放

            __kfree_skb_flush(); // 刷新所有延迟释放的数据包
        }
    }

    if (sd->output_queue) { // 如果有输出队列需要处理
        struct Qdisc *head;

        local_irq_disable(); // 禁用本地中断
        head = sd->output_queue; // 获取输出队列
        sd->output_queue = NULL; // 清空输出队列
        sd->output_queue_tailp = &sd->output_queue; // 重置尾指针
        local_irq_enable(); // 启用本地中断

        while (head) { // 遍历输出队列
            struct Qdisc *q = head; // 获取当前队列规则
            spinlock_t *root_lock;

            head = head->next_sched; // 移动到下一个队列规则

            root_lock = qdisc_lock(q); // 获取队列规则的锁
            spin_lock(root_lock); // 上锁
            /* We need to make sure head->next_sched is read
             * before clearing __QDISC_STATE_SCHED
             */
            smp_mb__before_atomic(); // 确保内存操作的顺序
            clear_bit(__QDISC_STATE_SCHED, &q->state); // 清除调度状态位
            qdisc_run(q); // 发送数据
            spin_unlock(root_lock); // 释放锁
        }
    }
}

软中断这里会获取soft_data,软中断主要做了两件事:一清理发送完成队列的skb,二将发送队列的数据发送出去。这里发送数据消耗的CPU都显示在si这里,不会消耗用户进程时间。
这里和用户态一样会调用__qdisc_run发送数据。

static inline void qdisc_run(struct Qdisc *q)
{
	if (qdisc_run_begin(q))
		__qdisc_run(q);
}

然后也是进入qdisc_restart=>sch_direct_xmit,直到进入驱动程序函数dev_hard_start_xmit。

3.7 igb网卡驱动发送

无论是在用户进程的内核态还是对于软中断上下文,都会调用网络设备子系统中的dev_hard_start_xmit函数。在这个函数中会调用到驱动的发送函数igb_xmit_frame。
在驱动函数里,会将skb挂到RingBuffer上,驱动调用完毕,数据包将真正从网卡发送出去。流程图如下:
在这里插入图片描述

struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
                                    struct netdev_queue *txq, int *ret)
{
    struct sk_buff *skb = first; // 初始化 skb 为第一个数据包
    int rc = NETDEV_TX_OK; // 初始化返回码为 NETDEV_TX_OK,表示发送成功

    while (skb) { // 循环处理所有数据包,直到 skb 为 NULL
        struct sk_buff *next = skb->next; // 保存下一个数据包的指针

        skb->next = NULL; // 将当前数据包的下一个指针设置为 NULL,因为硬件发送通常一次处理一个数据包

        rc = xmit_one(skb, dev, txq, next != NULL); // 调用 xmit_one 函数发送当前数据包
        if (unlikely(!dev_xmit_complete(rc))) { // 如果发送不完整(失败)
            skb->next = next; // 恢复下一个数据包的指针
            goto out; // 跳出循环
        }

        skb = next; // 移动到下一个数据包
        if (netif_xmit_stopped(txq) && skb) { // 如果队列已停止并且还有数据包
            rc = NETDEV_TX_BUSY; // 设置返回码为 NETDEV_TX_BUSY,表示设备忙
            break; // 跳出循环
        }
    }

out:
    *ret = rc; // 将最终的返回码存储在 ret 指针指向的变量中
    return skb; // 返回当前处理的数据包(可能是最后一个数据包或因错误而停止的数据包)
}
static int xmit_one(struct sk_buff *skb, struct net_device *dev,
		    struct netdev_queue *txq, bool more)
{
	unsigned int len;
	int rc;
	//tcpdump获取本机发送包工作的地方
	if (!list_empty(&ptype_all) || !list_empty(&dev->ptype_all))
		dev_queue_xmit_nit(skb, dev);
	
	len = skb->len;
	trace_net_dev_start_xmit(skb, dev);
	//发送数据包
	rc = netdev_start_xmit(skb, dev, txq, more);
	trace_net_dev_xmit(skb, rc, dev, len);

	return rc;
}
static inline netdev_tx_t netdev_start_xmit(struct sk_buff *skb, struct net_device *dev,
					    struct netdev_queue *txq, bool more)
{
	//获取设备的回调函数集合
	const struct net_device_ops *ops = dev->netdev_ops;
	int rc;
	//继续发送
	rc = __netdev_start_xmit(ops, skb, dev, more);
	if (rc == NETDEV_TX_OK)
		txq_trans_update(txq);

	return rc;
}
static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops,
					      struct sk_buff *skb, struct net_device *dev,
					      bool more)
{
	skb->xmit_more = more ? 1 : 0;
	return ops->ndo_start_xmit(skb, dev);
}

ndo_start_xmit是网卡驱动要实现的一个函数,是在net_device_ops中定义的。

drivers/net/ethernet/intel/igb/igb_main.c
static const struct net_device_ops igb_netdev_ops = {
	.ndo_open		= igb_open,	//当网络设备被激活(打开)时调用,通常用于初始化硬件。
	.ndo_stop		= igb_close,	//当网络设备被停用(关闭)时调用,用于停止设备的操作。
	.ndo_start_xmit		= igb_xmit_frame,	//用于发送数据包,调用驱动程序的发送函数。
	.ndo_get_stats64	= igb_get_stats64,	//获取网络设备的统计信息,如发送和接收的数据包数量等。
	.ndo_set_rx_mode	= igb_set_rx_mode,	//设置接收模式,如混杂模式、多播模式或单播模式。
	.ndo_set_mac_address	= igb_set_mac,	//设置网络设备的 MAC 地址。
	.ndo_change_mtu		= igb_change_mtu,	//更改网络设备的 MTU(最大传输单元)大小。
	.ndo_do_ioctl		= igb_ioctl,	//处理 IOCTL 命令,用于执行设备特定的操作。
	.ndo_tx_timeout		= igb_tx_timeout,	//处理发送超时,当设备在一定时间内没有发送数据包时调用。
	.ndo_validate_addr	= eth_validate_addr,	//验证硬件地址的有效性,通常由以太网设备通用层实现。
	...
};

对于网络设备层定义的ndo_start_xmit,igb的实现函数是igb_xmit_frame,这个函数是在网卡驱动初始化的时候被赋值的。

static netdev_tx_t igb_xmit_frame(struct sk_buff *skb,
                                  struct net_device *netdev)
{
    struct igb_adapter *adapter = netdev_priv(netdev); // 从网络设备结构体中获取私有数据
	...
    return igb_xmit_frame_ring(skb, igb_tx_queue_mapping(adapter, skb)); // 调用实际的发送函数
}
netdev_tx_t igb_xmit_frame_ring(struct sk_buff *skb,
                                struct igb_ring *tx_ring)
{
    struct igb_tx_buffer *first;
    int tso;
    u32 tx_flags = 0;
    unsigned short f;
    u16 count = TXD_USE_COUNT(skb_headlen(skb)); // 计算所需的描述符数量
    __be16 protocol = vlan_get_protocol(skb); // 获取 VLAN 协议
    u8 hdr_len = 0; // 头部长度

    // 计算所需的描述符数量,包括数据、填充和上下文描述符
    for (f = 0; f < skb_shinfo(skb)->nr_frags; f++)
        count += TXD_USE_COUNT(skb_shinfo(skb)->frags[f].size);

    if (igb_maybe_stop_tx(tx_ring, count + 3)) { // 检查是否需要停止发送
        /* this is a hard error */
        return NETDEV_TX_BUSY; // 如果需要停止,则返回忙状态
    }

    // 获取TX Queue中下一个可用的缓冲区信息
    first = &tx_ring->tx_buffer_info[tx_ring->next_to_use];
    first->skb = skb;
    first->bytecount = skb->len;
    first->gso_segs = 1;

    if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP)) { // 检查是否需要硬件时间戳
        struct igb_adapter *adapter = netdev_priv(tx_ring->netdev);

        if (!test_and_set_bit_lock(__IGB_PTP_TX_IN_PROGRESS, &adapter->state)) {
            skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;
            tx_flags |= IGB_TX_FLAGS_TSTAMP;

            adapter->ptp_tx_skb = skb_get(skb);
            adapter->ptp_tx_start = jiffies;
            if (adapter->hw.mac.type == e1000_82576)
                schedule_work(&adapter->ptp_tx_work);
        }
    }

    skb_tx_timestamp(skb); // 记录数据包的时间戳

    if (skb_vlan_tag_present(skb)) { // 检查是否存在 VLAN 标签
        tx_flags |= IGB_TX_FLAGS_VLAN;
        tx_flags |= (skb_vlan_tag_get(skb) << IGB_TX_FLAGS_VLAN_SHIFT);
    }

    // 记录初始标志和协议
    first->tx_flags = tx_flags;
    first->protocol = protocol;

    tso = igb_tso(tx_ring, first, &hdr_len); // 尝试进行 TSO(TCP Segmentation Offload)
    if (tso < 0)
        goto out_drop; // 如果 TSO 失败,则丢弃数据包
    else if (!tso)
        igb_tx_csum(tx_ring, first); // 如果不进行 TSO,则计算校验和
	
	// 准备给设备发送的数据(给Tx Queue建立映射关系)
    igb_tx_map(tx_ring, first, hdr_len); 

	...
}
static void igb_tx_map(struct igb_ring *tx_ring,
                       struct igb_tx_buffer *first,
                       const u8 hdr_len)
{
    struct sk_buff *skb = first->skb; // 获取要发送的数据包
    struct igb_tx_buffer *tx_buffer;
    union e1000_adv_tx_desc *tx_desc; // 传输描述符
    struct skb_frag_struct *frag;
    dma_addr_t dma; // DMA 地址
    unsigned int data_len, size;
    u32 tx_flags = first->tx_flags; // 传输标志
    u32 cmd_type = igb_tx_cmd_type(skb, tx_flags); // 命令类型
    u16 i = tx_ring->next_to_use; // 下一个要使用的描述符索引

    tx_desc = IGB_TX_DESC(tx_ring, i); // 获取当前描述符

    // 设置描述符的 olinfo_status 字段
    igb_tx_olinfo_status(tx_ring, tx_desc, tx_flags, skb->len - hdr_len);

    size = skb_headlen(skb); // 数据包头部长度
    data_len = skb->data_len; // 数据包数据长度

    // 为skb->data构造内存映射,以允许设备通过DMA从RAM中读取数据
    dma = dma_map_single(tx_ring->dev, skb->data, size, DMA_TO_DEVICE);

    tx_buffer = first; // 初始化 tx_buffer 为第一个缓冲区
	
	//遍历该数据包所有分片,为skb的每个分片生成有效
    for (frag = &skb_shinfo(skb)->frags[0];; frag++) { // 遍历所有数据包片段
        if (dma_mapping_error(tx_ring->dev, dma)) // 检查 DMA 映射是否出错
            goto dma_error;

        // 记录长度和 DMA 地址
        dma_unmap_len_set(tx_buffer, len, size);
        dma_unmap_addr_set(tx_buffer, dma, dma);

        tx_desc->read.buffer_addr = cpu_to_le64(dma); // 设置描述符的缓冲区地址

        // 处理每个描述符的最大数据长度限制
        while (unlikely(size > IGB_MAX_DATA_PER_TXD)) {
            tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type ^ IGB_MAX_DATA_PER_TXD);

            i++;
            tx_desc++;
            if (i == tx_ring->count) { // 如果到达描述符环的末尾,回到开始
                tx_desc = IGB_TX_DES
                i = 0;
            }
            tx_desc->read.olinfo_status = 0;

            dma += IGB_MAX_DATA_PER_TXD;
            size -= IGB_MAX_DATA_PER_TXD;

            tx_desc->read.buffer_addr = cpu_to_le64(dma);
        }

        if (likely(!data_len)) // 如果没有更多数据要发送,跳出循环
            break;

        tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type ^ size);

        i++;
        tx_desc++;
        if (i == tx_ring->count) { // 如果到达描述符环的末尾,回到开始
            tx_desc = IGB_TX_DESC(tx_ring, 0);
            i = 0;
        }
        tx_desc->read.olinfo_status = 0;

        size = skb_frag_size(frag); // 获取片段大小
        data_len -= size;

        // 将数据包片段映射到 DMA
        dma = skb_frag_dma_map(tx_ring->dev, frag, 0, size, DMA_TO_DEVICE);

        tx_buffer = &tx_ring->tx_buffer_info[i]; // 更新 tx_buffer 为当前片段
    }

    // 设置最后一个描述符的 RS 和 EOP 位
    cmd_type |= size | IGB_TXD_DCMD;
    tx_desc->read.cmd_type_len = cpu_to_le32(cmd_type);

    // 更新网络设备发送统计信息
    netdev_tx_sent_queue(txring_txq(tx_ring), first->bytecount);
	...
}

当所有需要的描述符都已建好,且skb的所有数据都映射到DMA地址后,驱动就进入最后一步,触发真实的发送。

4、RingBuffer内存回收

当数据发送完成时,内存还没有清理。在发送完成时,网卡设备会触发硬中断来释放内存。

static inline void ____napi_schedule(struct softnet_data *sd,
				     struct napi_struct *napi)
{
	list_add_tail(&napi->poll_list, &sd->poll_list);
	__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

在这里可以发现,无论是接收数据还是发送完成通知,从硬中断触发的软中断都是NET_RX_SOFTIRQ。
接着进入软中断的回调函数igb_poll。

static int igb_poll(struct napi_struct *napi, int budget)
{
	struct igb_q_vector *q_vector = container_of(napi,
						     struct igb_q_vector,
						     napi);
	bool clean_complete = true;
	int work_done = 0;
	...
	if (q_vector->tx.ring)
		clean_complete = igb_clean_tx_irq(q_vector, budget);
	...
}
static bool igb_clean_tx_irq(struct igb_q_vector *q_vector, int napi_budget)
{
    struct igb_adapter *adapter = q_vector->adapter; // 获取适配器实例
    struct igb_ring *tx_ring = q_vector->tx.ring; // 获取发送队列
    struct igb_tx_buffer *tx_buffer; // 指向当前处理的 tx_buffer
    union e1000_adv_tx_desc *tx_desc; // 指向当前处理的 tx_desc
    unsigned int total_bytes = 0, total_packets = 0; // 用于统计发送的总字节和数据包数量
    unsigned int budget = q_vector->tx.work_limit; // NAPI 预算
    unsigned int i = tx_ring->next_to_clean; // 从哪里开始清理

    if (test_bit(__IGB_DOWN, &adapter->state)) // 如果设备已经关闭
        return true; // 返回 true 表示没有更多的工作要做

    tx_buffer = &tx_ring->tx_buffer_info[i]; // 获取当前要清理的 tx_buffer
    tx_desc = IGB_TX_DESC(tx_ring, i); // 获取当前要清理的 tx_desc
    i -= tx_ring->count; // 调整索引

    do {
        union e1000_adv_tx_desc *eop_desc = tx_buffer->next_to_watch; // 获取这个数据包的最后一个描述符

        /* if next_to_watch is not set then there is no work pending */
        if (!eop_desc) // 如果没有工作待处理,则退出循环
            break;

        /* prevent any other reads prior to eop_desc */
        read_barrier_depends(); // 确保 eop_desc 的读取不会乱序

        /* if DD is not set pending work has not been completed */
        if (!(eop_desc->wb.status & cpu_to_le32(E1000_TXD_STAT_DD))) // 如果 DD 位未设置,表示工作未完成
            break;

        /* clear next_to_watch to prevent false hangs */
        tx_buffer->next_to_watch = NULL; // 清除 next_to_watch 以避免假挂起

        /* update the statistics for this packet */
        total_bytes += tx_buffer->bytecount; // 更新发送的总字节数
        total_packets += tx_buffer->gso_segs; // 更新发送的总数据包数

        //释放skb
        napi_consume_skb(tx_buffer->skb, napi_budget); 

        /* unmap skb header data */
        dma_unmap_single(tx_ring->dev, // 取消映射 skb 头部数据
                         dma_unmap_addr(tx_buffer, dma),
                         dma_unmap_len(tx_buffer, len),
                         DMA_TO_DEVICE);

        /* clear tx_buffer data */
        dma_unmap_len_set(tx_buffer, len, 0); // 清除 tx_buffer 数据

        /* clear last DMA location and unmap remaining buffers */
        while (tx_desc != eop_desc) { // 清除剩余的 DMA 映射
            tx_buffer++;
            tx_desc++;
            i++;
            if (unlikely(!i)) {
                i -= tx_ring->count;
                tx_buffer = tx_ring->tx_buffer_info;
                tx_desc = IGB_TX_DESC(tx_ring, 0);
            }

            /* unmap any remaining paged data */
            if (dma_unmap_len(tx_buffer, len)) {
                dma_unmap_page(tx_ring->dev,
                               dma_unmap_addr(tx_buffer, dma),
                               dma_unmap_len(tx_buffer, len),
                               DMA_TO_DEVICE);
                dma_unmap_len_set(tx_buffer, len, 0);
            }
        }

        /* move us one more past the eop_desc for start of next pkt */
        tx_buffer++; // 移动到下一个数据包
        tx_desc++;
        i++;
        if (unlikely(!i)) {
            i -= tx_ring->count;
            tx_buffer = tx_ring->tx_buffer_info;
            tx_desc = IGB_TX_DESC(tx_ring, 0);
        }

        /* issue prefetch for next Tx descriptor */
        prefetch(tx_desc); // 预取下一个 Tx 描述符

        /* update budget accounting */
        budget--; // 更新预算
    } while (likely(budget)); // 如果预算还有剩余,继续处理

    netdev_tx_completed_queue(txring_txq(tx_ring), // 更新网络设备的完成统计
                              total_packets, total_bytes);
    i += tx_ring->count; // 调整索引
    tx_ring->next_to_clean = i; // 设置新的清理位置
    u64_stats_update_begin(&tx_ring->tx_syncp); // 开始更新统计信息
    tx_ring->tx_stats.bytes += total_bytes; // 更新发送的总字节数
    tx_ring->tx_stats.packets += total_packets; // 更新发送的总数据包数
    u64_stats_update_end(&tx_ring->tx_syncp); // 结束更新统计信息
    q_vector->tx.total_bytes += total_bytes; // 更新 q_vector 的总字节数
    q_vector->tx.total_packets += total_packets; // 更新 q_vector 的总数据包数
    ...
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2164863.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024.9.25 作业和思维导图

栈 #include <iostream> #include <stdexcept> using namespace std;class My_stack { private:int * data; //栈空间int capacity;int top; //栈顶元素的下标 protected:public:/******************成员函数*************///构造函数My_stack(int c 10):capac…

JS中的事件和DOM操作

一、事件[重要] 1、 事件介绍 事件: 就是发生在浏览器(页面)上一件事,键盘事件,鼠标事件,表单事件,加载事件等等 2、 事件绑定方式 事件要想发生,就得将事件和标签先绑定(确定哪个标签发生什么事情,又有什么响应) 一个完整的事件有三部分 事件源(标签),哪里发出的事. 什么事(…

【DAY20240925】随机梯度下降:高效优化背后的原理与进阶策略

文章目录 前言随机梯度下降SGDMini-batch 随机梯度下降常见优化算法的改进版本 前言 梯度下降更新的通用形式&#xff1a; 论文中类似的表达形式&#xff0c;都表示根据 损失函数对这些参数的梯度 进行更新参数。梯度值较大时&#xff0c;说明当前控制参数对损失有较大的影响…

排序个人总结

插入排序 思路&#xff1b;定义 i 和 j&#xff0c;默认 i 前面的数都是有序的&#xff0c;j 定义为 i 的前一个数&#xff0c;把 i 的值给tmp&#xff0c;tmp与j对应的值进行比较&#xff0c;如果arr[j] > tmp,将arr[j] (大的数前移一位)&#xff0c;如下图 代码&#xf…

【亲子英语】英语故事有声绘本分享

文章目录 一、视觉与听觉的双重盛宴二、语言学习的最佳伙伴三、亲子共读的温馨时光四、适用人群广泛&#xff0c;随时随地学习五、获取方式 在这个快速发展的时代&#xff0c;英语学习已经不再局限于课本和课堂。特别是对于活泼好动的孩子们来说&#xff0c;一种既有趣又高效的…

open-resty 服务安装jwt插件

作者&#xff1a;程序那点事儿 日期&#xff1a;2023/11/16 22:07 lua-resty-jwt 插件 如果想使用Lua识别用户令牌&#xff0c;我们需要引入lua-resty-jwt模块&#xff0c;是用于 ngx_lua 和 LuaJIT 的 Lua 实现库&#xff0c;在该模块能实现Jwt令牌生成、Jwt令牌校验。 下载…

9.25作业

手动实现队列 代码如下 MyQueue.h #ifndef MYQUEUE_H #define MYQUEUE_H #include <iostream> #include <cstring> using namespace std;class Queue{ private:char* data; //字符串数据int len; //当前数量int size; //最大容量int front; //头索引int …

uboot — uboot命令的使用

uboot的命令繁多&#xff0c;下文只对工作中常用到的命令进行记录&#xff0c;其余命令待用到时再查查资料也不迟 一、环境变量操作命令 1、printenv 打印环境变量 2、setenv 修改环境变量/新建环境变量 3、saveenv 保存环境变量/删除环境变量&#xff08;给环境变量赋空值…

巴特沃斯滤波器的MATLAB实现

一、引言 巴特沃斯滤波器&#xff08;Butterworth Filter&#xff09;&#xff0c;是滤波器的一种&#xff0c;其主要特点是通频带的频率响应曲线最平滑。这种滤波器最先由英国工程师斯蒂芬巴特沃斯(StephenButterworth)在1930年发表在英国《无线电工程》期刊的一篇论文中提出的…

基于vue框架的村务综合服务系统8p0l3(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;居民,村委,支部,公告信息,通讯录,重点户 开题报告内容 基于Vue框架的村务综合服务系统开题报告 一、引言 随着信息化时代的深入发展&#xff0c;农村社会治理模式正经历着深刻的变革。传统村务管理方式往往存在信息不对称、效率低下、…

【SpringBoot整合Redis测试Redis集群案例】

1、第一步&#xff0c;创建springboot项目&#xff0c;并导入依赖 如图&#xff0c;创建项目遇到的第一个问题就是&#xff0c;当type选择maven&#xff0c;jdk选择1.8时&#xff0c;java部分没办法选择1.8的版本&#xff0c;这怎么办呢&#xff1f; 原因&#xff1a;搜了一下…

【C++ Primer Plus习题】17.7

问题: 解答: #include <iostream> #include <vector> #include <string> #include <fstream> #include <algorithm>using namespace std;const int LIMIT 50;void ShowStr(const string& str); void GetStrs(ifstream& fin, vector<…

【多线程】面试高频考点!JUC常见类的详细总结,建议收藏!

&#x1f490;个人主页&#xff1a;初晴~ &#x1f4da;相关专栏&#xff1a;多线程 / javaEE初阶 JUC是“Java Util Concurrency”的缩写&#xff0c;指的是Java并发工具包&#xff0c;它位于java.util.concurrent包及其子包中。JUC包提供了大量用于构建并发应用程序的工具和…

简历信息提取系统源码分享

简历信息提取检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

C++之stack 和 queue

目录 前言 1.stack的介绍和使用 1.1 stack的介绍 1.2 stack的使用 1.3 stack 的模拟 2. queue的介绍和使用 2.1 queue的介绍 2.2 queue的使用 2.3 queue的模拟 3.适配器 3.1 什么是适配器 3.2 STL标准库中stack和queue的底层结构 3.3 deque 的介绍&#xff08;了解&…

每日OJ题_牛客_ 腐烂的苹果_多源BFS_C++_Java

目录 牛客_腐烂的苹果&#xff08;多源 BFS&#xff09; 题目解析 C代码 Java代码 牛客_腐烂的苹果&#xff08;多源 BFS&#xff09; 腐烂的苹果_牛客题霸_牛客网 题目解析 多源 BFS 问题&#xff0c;固定套路&#xff0c;BFS学习 &#xff1a;Offer必备算法28_多源BFS_…

【C++算法】哈希表

哈希表介绍&#xff1a; 1.哈希表是什么&#xff1f; 存储数据的容器 2.哈希表有什么用&#xff1f; “快速”查找某个元素——O(N) 3.什么时候使用哈希表&#xff1f; 频繁的查找某一个数的时候&#xff0c;频繁也可以使用二分&#xff08;有序&#xff09; 4.怎么用哈希表&…

cadence多版本启动问题

一、问题描述 电脑上安装了 17.4 和16.6两个版本打开16.6时会弹出 **原因&#xff1a;**使用Allegro设计PCB时&#xff0c;当关闭软件后&#xff0c;再次打开Allegro软件&#xff0c;打开的文件为上一次操作过的.brd文件&#xff0c;这是Allegro软件安装的默认设置。 二、解…

单体项目中定时任务的实现-详细教程

单体项目中定时任务的实现 在企业开发中&#xff0c;遇到的项目无非就两种&#xff0c;单体项目和分布式项目 单体项目中实现定时任务有以下几种方式 1. 使用Timer实现定时任务&#xff08;不常用&#xff09; 1.1、JDK1.3推出的定时任务实现工具类java.util.Timer 1.2、API…

学习MRI处理过程中搜到的宝藏网站

今天浏览网页查到了一些宝藏网站&#xff0c;正好记录一下&#xff0c;后面搜到好东东再接着填充&#xff0c;方便查阅~ &#xff08;1&#xff09;牛人网站 这个网站是在搜集seed关键词时发现的&#xff0c;用pdf文档记录&#xff0c;可下载查阅&#xff0c;条理清晰&#xf…