内核是如何发送数据包

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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值