文章目录
-
- [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_skb和udp_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 函数主要职责
- SKB 克隆 :为了保留原始 SKB 以便可能的重传,通常会对 SKB 进行克隆(
skb_clone)。 - TCP 头部构建:填充 TCP 头部字段,如源/目的端口、序列号、确认号、窗口大小、标志位(SYN, ACK, FIN 等)。
- 校验和计算:处理 TCP 校验和。
- 传递给 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 是用户态调用 sendto 或 sendmsg 发送 UDP 数据时,内核对应的系统调用处理函数。与 TCP 不同,UDP 的发送逻辑主要是"一穿到底",尽量在一次调用中完成数据的封装和发送。
2.1 函数主要职责
- 路由查找 :确定目的地址、路由出口设备(
ip_route_output_flow)。 - Corking (粘包) 处理 :如果设置了
MSG_MORE,会将数据暂时缓存。 - 数据拷贝与封装 :将用户空间数据拷贝到内核,并封装成 SKB(
ip_make_skb)。 - 推送发送 :将封装好的 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 层,它的任务就完成了。
希望这两篇文章能对你的学习和工作有所帮助! 如果觉得有用,请点赞收藏支持一下! 👍