Linux 网络内核:tcp_transmit_skb 与 udp_sendmsg 解析

文章目录

    • [Linux 网络内核:tcp_transmit_skb 与 udp_sendmsg 解析](#Linux 网络内核:tcp_transmit_skb 与 udp_sendmsg 解析)
    • [📖 前言](#📖 前言)
    • [1. 🌐 TCP 发送核心:tcp_transmit_skb](#1. 🌐 TCP 发送核心:tcp_transmit_skb)
      • [1.1 函数主要职责](#1.1 函数主要职责)
      • [1.2 源码核心逻辑分析](#1.2 源码核心逻辑分析)
      • [1.3 关键点解读](#1.3 关键点解读)
    • [2. 📦 UDP 发送入口:udp_sendmsg](#2. 📦 UDP 发送入口:udp_sendmsg)
      • [2.1 函数主要职责](#2.1 函数主要职责)
      • [2.2 源码核心逻辑分析](#2.2 源码核心逻辑分析)
      • [2.3 关键步骤详解](#2.3 关键步骤详解)
    • [3. ⚖️ 总结与对比:TCP vs UDP 发送路径](#3. ⚖️ 总结与对比:TCP vs UDP 发送路径)
      • [🧠 核心思考](#🧠 核心思考)

Linux 网络内核:tcp_transmit_skb 与 udp_sendmsg 解析

摘要 :在 Linux 内核网络协议栈中,数据的发送是一个复杂而精妙的过程。本文将深入剖析 TCP 和 UDP 协议栈中两个至关重要的发送函数:tcp_transmit_skbudp_sendmsg。通过源码级分析,带你理解它们在数据包构建、封装及传递给 IP 层过程中的核心逻辑。

关键词:Linux Kernel, TCP/IP, tcp_transmit_skb, udp_sendmsg, 源码分析


📖 前言

  • TCP 作为一个面向连接的可靠协议,其发送过程涉及复杂的窗口管理、拥塞控制和重传机制,而 tcp_transmit_skb 正是这一系列逻辑最终汇聚的出口。
  • UDP 作为一个无连接的不可靠协议,其发送逻辑相对直接,udp_sendmsg 承担了从用户空间数据拷贝到内核并封装发送的主要职责。

本文将基于 Linux 内核源码(以 v5.x 为例),详细解读这两个函数的实现细节。


1. 🌐 TCP 发送核心:tcp_transmit_skb

tcp_transmit_skb 是 TCP 协议栈中真正将数据包(SKB)向下层传递或者是进行克隆发送的函数。无论是首次发送新数据(tcp_write_xmit),还是重传旧数据(tcp_retransmit_skb),亦或是发送单纯的 ACK 包,最终都会调用到这里。

1.1 函数主要职责

  1. SKB 克隆 :为了保留原始 SKB 以便可能的重传,通常会对 SKB 进行克隆(skb_clone)。
  2. TCP 头部构建:填充 TCP 头部字段,如源/目的端口、序列号、确认号、窗口大小、标志位(SYN, ACK, FIN 等)。
  3. 校验和计算:处理 TCP 校验和。
  4. 传递给 IP 层 :调用 icsk->icsk_af_ops->queue_xmit(通常是 ip_queue_xmit)将数据包交给 IP 层。

1.2 源码核心逻辑分析

c 复制代码
/* net/ipv4/tcp_output.c */

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 tcphdr *th;
	int err;

	/* 1. 克隆数据包
	 * TCP 需要保留原始 SKB 在重传队列中,直到收到 ACK 确认。
	 * 因此发送出去的通常是一个克隆包(共享数据区,独立头部结构)。
	 */
	if (clone_it) {
		struct sk_buff *oskb = skb;
		skb = skb_clone(skb, gfp_mask);
		if (unlikely(!skb))
			return -ENOBUFS;
	}

	inet = inet_sk(sk);
	tp = tcp_sk(sk);
	tcb = TCP_SKB_CB(skb);

	/* 2. 构建 TCP 选项 (Timestamp, MSS, SACK 等) */
	tcp_header_size = tcp_options_size = 0;
	if (likely(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);

	/* 3. 在 SKB 头部预留空间并填充 TCP 头 */
	th = (struct tcphdr *)skb_push(skb, tcp_header_size);
	skb_reset_transport_header(skb);

	memset(th, 0, sizeof(struct tcphdr));
	th->source = inet->inet_sport;
	th->dest   = inet->inet_dport;
	th->seq    = htonl(tcb->seq);
	th->ack_seq = htonl(tp->rcv_nxt);

	/* 设置 TCP Flags (SYN, ACK, PSH, RST 等) */
	*(((__be16 *)th) + 6) = htons(((tcp_header_size >> 2) << 12) |
				      tcb->tcp_flags);

	th->window = htons(tcp_select_window(sk));
	th->check = 0;
	th->urg_ptr = 0;

	/* 4. 写入 TCP 选项数据 */
	if (unlikely(tcp_options_size))
		tcp_options_write((__be32 *)(th + 1), tp, &opts);

	/* 5. 计算校验和 (Checksum)
	 * 此处通常涉及硬件卸载 (Checksum Offload) 的设置
	 */
	icsk->icsk_af_ops->send_check(sk, skb);

	/* 6. 发送给网络层 (IP Layer)
	 * 对于 IPv4,这里调用的是 ip_queue_xmit
	 */
	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);
}

1.3 关键点解读

  • clone_it 参数 :只有在发送比如纯 ACK 包或者 RST 包这种不需要重传的报文时,clone_it 可能为 false。对于携带用户数据的报文,必须克隆,因为原始 skb 挂在 sk_write_queue 上等待确认。
  • 拥塞控制与窗口 :虽然 tcp_transmit_skb 本身主要负责封装,但在它之前的 tcp_write_xmit 中已经完成了拥塞窗口(Cwnd)和发送窗口(Rwnd)的检查。
  • 状态机:该函数不改变 TCP 状态机,它只是忠实地将当前 Socket 状态转化为协议头发送出去。

2. 📦 UDP 发送入口:udp_sendmsg

udp_sendmsg 是用户态调用 sendtosendmsg 发送 UDP 数据时,内核对应的系统调用处理函数。与 TCP 不同,UDP 的发送逻辑主要是"一穿到底",尽量在一次调用中完成数据的封装和发送。

2.1 函数主要职责

  1. 路由查找 :确定目的地址、路由出口设备(ip_route_output_flow)。
  2. Corking (粘包) 处理 :如果设置了 MSG_MORE,会将数据暂时缓存。
  3. 数据拷贝与封装 :将用户空间数据拷贝到内核,并封装成 SKB(ip_make_skb)。
  4. 推送发送 :将封装好的 SKB 发送给 IP 层(udp_send_skb)。

2.2 源码核心逻辑分析

c 复制代码
/* net/ipv4/udp.c */

int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{
	struct inet_sock *inet = inet_sk(sk);
	struct udp_sock *up = udp_sk(sk);
	struct flowi4 fl4_stack;
	struct flowi4 *fl4;
	struct rtable *rt = NULL;
	struct sk_buff *skb;
	int err = 0;

	/* 1. 检查是否有 pending 的数据 (Corking 机制) */
	if (up->pending) {
		/* ... lockless fast path or lock processing ... */
        /* 如果已有缓存数据,且本次没带 MSG_MORE,可能触发 flush */
	}

	/* 2. 也是最关键的一步:路由查找
	 * 确定源 IP、目的 IP、出接口等
	 */
	if (connected)
		rt = (struct rtable *)sk_dst_check(sk, 0);

	if (rt == NULL) {
		/* 初始化 flowi4 结构用于路由查找 */
		flowi4_init_output(fl4, ...);

		/* 查找路由表 */
		rt = ip_route_output_flow(net, fl4, sk);
		if (IS_ERR(rt)) {
			err = PTR_ERR(rt);
			goto out;
		}
	}

	/* 3. 数据处理:构建 SKB 或者是追加数据
	 * ip_make_skb 内部会处理数据的拷贝(from user)、分片(Fragmentation)等
	 */
	if (msg->msg_flags & MSG_MORE) {
        /* 如果有 MSG_MORE,只是 append 数据到 pending 队列,不立即发送 */
		err = ip_append_data(sk, fl4, getfrag, msg->msg_iter.iov, ulen, ...);
	} else {
        /* 没有 MSG_MORE,构建完整的 SKB */
		skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iter.iov, ulen, ...);
		if (IS_ERR(skb))
			return PTR_ERR(skb);

		/* 4. 最终发送
		 * 将构建好的 SKB 发送给 IP 层
		 */
		err = udp_send_skb(skb, fl4);
	}

out:
	return err;
}

2.3 关键步骤详解

  • ip_make_skb :这是 UDP 发送路径上的"重型"函数。它负责申请 sk_buff 内存,将用户态数据拷贝进去。如果数据量超过 MTU,它还会负责利用 IP 分片机制(或者 UDP Fragmentation Offload, UFO)来组织数据。
  • udp_send_skb :相比 tcp_transmit_skb,这个函数简单很多。它主要负责填充 UDP 头部(源端口、目的端口、长度、校验和),然后直接调用 ip_send_skb 进入 IP 层。

3. ⚖️ 总结与对比:TCP vs UDP 发送路径

为了更直观地理解两者的区别,我们进行以下对比:

特性 tcp_transmit_skb (TCP) udp_sendmsg (UDP)
层级位置 这里的输入已经是构建好的 sk_buff(通常在 sk_write_queue 中)。 这是系统调用的入口,输入是用户态的 buffer。
数据来源 从 Socket 的发送队列中取出待发送的 SKB。 直接从用户空间拷贝 (copy_from_user)。
内存管理 极其依赖 Clone (克隆) 技术,因为原包必须保留以备重传。 构建全新的 SKB,发送完即释放(除非被驱动层持有)。
状态依赖 强依赖 TCP 状态机(Seq, Ack, Window)。 无状态,仅依赖路由表信息。
IP 层接口 调用 icsk_af_ops->queue_xmit (如 ip_queue_xmit)。 调用 udp_send_skb -> ip_send_skb
主要开销 头部封装、校验和、队列管理、克隆开销。 内存拷贝、路由查找、内存分配。

🧠 核心思考

  • TCP 的复杂性在于**"不仅要发,还要记得发了什么"**。tcp_transmit_skb 只是冰山一角,背后是庞大的拥塞控制和重传逻辑在驱动它。
  • UDP 的简洁性在于**"发后即忘"**。udp_sendmsg 承担了更多的"杂活"(路由、拷贝、分片),一旦交给 IP 层,它的任务就完成了。

希望这两篇文章能对你的学习和工作有所帮助! 如果觉得有用,请点赞收藏支持一下! 👍

相关推荐
John Song1 小时前
miniconda是否初始化?
linux·运维·服务器·python
草草_2 小时前
【内核驱动基础】内核模块的两种编译方式(in-tree vs out-of-tree)
linux·驱动开发·内核
那就回到过去2 小时前
PIM-SM(稀疏模式)
网络·网络协议·tcp/ip·智能路由器·pim·ensp
科技块儿2 小时前
如何高效查询海量IP归属地?大数据分析中的IP查询应用
网络·tcp/ip·数据分析
一执念2 小时前
【路由器-AP、DHCP、ARP、广播帧、交换机、信道】-初级知识串联(五)之路由器与交换机的关系
网络·智能路由器
Xxtaoaooo2 小时前
React Native 跨平台鸿蒙开发实战:网络请求与鸿蒙分布式能力集成
网络·react native·harmonyos
犀思云2 小时前
出海SaaS全球分布式部署:流量调度的六大核心挑战与破局思考
运维·网络·人工智能·系统架构·机器人
j_xxx404_2 小时前
Linux:进程
linux·运维·服务器
Remember_9932 小时前
网络编程套接字深度解析:从理论到实践的完整指南
网络·算法·http·https·udp·哈希算法·p2p