Linux TCP小包合并Nagle算法与TCP_CORK互斥

📅 2026/7/21 18:15:21
Linux TCP小包合并Nagle算法与TCP_CORK互斥
Linux TCP小包合并Nagle算法与TCP_CORK互斥三个标志位定义在include/net/tcp.h的tp-nonagle字段中TCP_NAGLE_OFF(1)关闭NagleTCP_NAGLE_CORK(2)开启软木塞TCP_NAGLE_PUSH(4)强制推送覆盖。Nagle算法与TCP_CORK共享同一个nonagle位域但语义有本质差异。Nagle允许至多一个小包在途Minshall变体而CORK无条件阻塞所有小于MSS的段。c/* include/net/tcp.h */#define TCP_NAGLE_OFF 1#define TCP_NAGLE_CORK 2#define TCP_NAGLE_PUSH 4tcp_nagle_check()是决策入口。partial参数表示skb-len mss_now即当前段为部分段。CORK和Nagle的分叉点在此CORK位一旦置位无论是否有未确认包所有部分段均被阻塞。Nagle只有在nonagle0时检测tp-packets_out和tcp_minshall_check()。cstatic bool tcp_nagle_check(bool partial, const struct tcp_sock *tp,int nonagle){return partial ((nonagle TCP_NAGLE_CORK) ||(!nonagle tp-packets_out tcp_minshall_check(tp)));}这是互斥的第一层语义CORK的判定路径不依赖packets_out而Nagle的判定路径依赖packets_out且要求nonagle0。当CORK被设置时nonagle TCP_NAGLE_CORK为真短路求值使tcp_minshall_check()不被调用。这意味着Corked socket即便拥塞窗口充足且无未确认小包部分段也不会被发送。tcp_minshall_check()实现Minshall对经典Nagle的修正——只追踪小包的确认状态而非全部包的确认状态。tp-snd_sml记录最近发送的小包结束序号通过after()宏检测该序号是否已落入snd_una之前。cstatic bool tcp_minshall_check(const struct tcp_sock *tp){return after(tp-snd_sml, tp-snd_una) !after(tp-snd_sml, tp-snd_nxt);}tp-snd_sml由tcp_minshall_update()在每次发送小包时更新关键边界是skb-len tcp_skb_pcount(skb) * mss_now才记录。GSO分段场景下一个skb可能携带多个MSS段此时skb-len tcp_skb_pcount(skb) * mss_now不会更新snd_sml——这意味着大段不会阻塞后续小段符合Minshall语义。tcp_nagle_test()是调用前的包装层处理三个快速路径TCP_NAGLE_PUSH绕过所有检查、urg数据和FIN包无条件发送。cstatic bool tcp_nagle_test(const struct tcp_sock *tp, const struct sk_buff *skb,unsigned int cur_mss, int nonagle){if (nonagle TCP_NAGLE_PUSH)return true;if (tcp_urg_mode(tp) ||(TCP_SKB_CB(skb)-tcp_flags TCPHDR_FIN))return true;if (!tcp_nagle_check(skb-len cur_mss, tp, nonagle))return true;return false;}nonagle参数的传递路径决定了实时互斥语义。在tcp_write_xmit()中遍历发送队列时对于非队尾的skbnonagle被无条件替换为TCP_NAGLE_PUSH避开Nagle检查。c/* net/ipv4/tcp_output.c, tcp_write_xmit() */if (tso_segs 1) {if (unlikely(!tcp_nagle_test(tp, skb, mss_now,(tcp_skb_is_last(sk, skb) ?nonagle : TCP_NAGLE_PUSH))))break;} else {if (tcp_tso_should_defer(sk, tp, skb, defer_flags, in_softirq))break;}这一设计的逻辑基础是非队尾的skb无法再合并新数据应用Nagle只会无意义地延迟发送不会带来任何小包合并收益。仅队尾skb才有资格参与小包合并因此只有队尾skb才受Nagle/CORK约束。TCP_CORK的设置在do_tcp_setsockopt()中完成。设置时在tp-nonagle上OR TCP_NAGLE_CORK清除时的逻辑包含一个关键互斥场景如果同时开启了TCP_NODELAY即TCP_NAGLE_OFF也被置位则必须设置TCP_NAGLE_PUSH以强制刷新CORK期间积压的数据。ccase TCP_CORK:if (val) {tp-nonagle | TCP_NAGLE_CORK;} else {tp-nonagle ~TCP_NAGLE_CORK;if (tp-nonagle TCP_NAGLE_OFF)tp-nonagle | TCP_NAGLE_PUSH;tcp_push_pending_frames(sk);}break;这里存在一个竞态窗口当TCP_CORK被清除而TCP_NODELAY已经置位时TCP_NAGLE_PUSH标志被设置后立即调用tcp_push_pending_frames()。但如果此时另一个上下文通过tcp_write_xmit()正在遍历发送队列tcp_nagle_test()可能看到TCP_NAGLE_PUSH位也可能看不到——取决于时序。该竞争由socket locklock_sock保护但在softirq路径tcp_write_xmit从tcp_push_pending_frames被调用时运行在进程上下文而从tcp_release_cb或tsq任务调度运行时在softirq中需确保bh_lock_sock的正确使用。tcp_push()是tcp_sendmsg_locked()每个写入循环结束后调用的推送入口。MSG_MORE标志直接映射为TCP_NAGLE_CORK如果用户设置了MSG_MOREnonagle参数被覆盖为TCP_NAGLE_CORK无视tp-nonagle的原始值。cstatic void tcp_push(struct sock *sk, int flags, int mss_now,int nonagle, int size_goal){struct tcp_sock *tp tcp_sk(sk);struct sk_buff *skb tcp_write_queue_tail(sk);if (!skb)return;if (!(flags MSG_MORE) || forced_push(tp))tcp_mark_push(tp, tcp_write_queue_tail(sk));tcp_mark_urg(tp, flags, skb);if (tcp_should_autocork(sk, skb, size_goal)) {NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);set_bit(TSQ_THROTTLED, sk-sk_tsq_flags);return;}__tcp_push_pending_frames(sk, mss_now,(flags MSG_MORE) ? TCP_NAGLE_CORK : nonagle);}tcp_should_autocork()是内核3.18引入的自动软木塞机制与TCP_CORK形成第二层互斥。当TCP Small Queues检测到发送队列中仍有未释放的数据sk_wmem_alloc skb-truesize且sysctl_tcp_autocorking开启时tcp_push()不会调用__tcp_push_pending_frames()而是设置TSQ_THROTTLED标志并直接返回。这意味着即便用户在应用层设置了TCP_NODELAY内核也可能因为autocork的逻辑延迟发送覆盖Nagle的禁用语义。cstatic bool tcp_should_autocork(struct sock *sk, struct sk_buff *skb,int size_goal){return skb-len size_goal sock_net(sk)-ipv4.sysctl_tcp_autocorking !tcp_rtx_queue_empty(sk) refcount_read(sk-sk_wmem_alloc) skb-truesize tcp_skb_can_collapse_to(skb);}autocork的触发条件中!tcp_rtx_queue_empty(sk)是关键边界只有确认有数据在重传队列中即已实际发送出去但未确认才启用自动软木塞。如果tp-packets_out 0但重传队列为空仅限TFO或SYN阶段等罕见场景autocork不会生效。tcp_mark_push()设置skb的TCPCB_FLAG_PSH标志但它不直接参与Nagle/CORK判定。PSH标志只影响接收端的应用层交付时机。发送端内核中PSH的副作用是通过tcp_push_pending_frames()触发一次发送尝试但Nagle/CORK检查仍然在该路径中被执行。TCP_NAGLE_PUSH标志的清除时机在skb_entail()中这是一个容易忽视的边界条件。当新skb入队时无条件清除TCP_NAGLE_PUSHcstatic inline void skb_entail(struct sock *sk, struct tcp_sock *tp,struct sk_buff *skb){skb_set_owner_w(skb, sk);if (!skb-sk)skb-destructor sock_wfree;if (!tcp_skb_is_last(sk, skb))skb-next NULL;tp-nonagle ~TCP_NAGLE_PUSH;}这一清除在2.6.13之前是一个else-if条件分支仅当skb成为新的send_head时才清除存在一个长期bug如果TCP_NAGLE_PUSH被设置后新skb入队但不是send_head即队列非空新skb追加到队尾TCP_NAGLE_PUSH不会被清除导致后续所有数据持续绕过Nagle检查。David S. Miller在commit 89ebd197中将其改为无条件清除。tcp_push_one()是TCP_CORK的逆操作——当corked socket收到外部事件如TCP_CORK被关闭、TCP_NODELAY被设置、或收到ACK释放了窗口时tcp_write_xmit()逐skb遍历发送队列。与此相对tcp_push_one()仅发送当前队首的一个skb且调用方必须保证skb-len mss_now即段已填满否则BUG_ON触发。cvoid tcp_push_one(struct sock *sk, unsigned int mss_now){struct tcp_sock *tp tcp_sk(sk);struct sk_buff *skb tcp_send_head(sk);unsigned int tso_segs, cwnd_quota;int limit;BUG_ON(!skb || skb-len mss_now);tso_segs tcp_init_tso_segs(sk, skb, mss_now);cwnd_quota tcp_snd_test(sk, skb, mss_now, TCP_NAGLE_PUSH);if (likely(cwnd_quota)) {limit tcp_window_allows(tp, skb, mss_now, cwnd_quota);TCP_SKB_CB(skb)-when tcp_time_stamp;if (likely(!tcp_transmit_skb(sk, skb_clone(skb, sk-sk_gfp_mask)))) {update_send_head(sk, tp, skb);tcp_cwnd_validate(sk);}}}tcp_push_one()直接使用TCP_NAGLE_PUSH调用tcp_snd_test()绕过所有Nagle/CORK检查。这在TCP_CORK关闭后的刷新路径中至关重要关闭CORK时需确保积压数据被发出不受Nagle限制。但tcp_push_one()仅在tcp_sendmsg_locked()的中间循环中由tcp_push()路径调用——当有完整MSS段就绪且msg_data_left()仍然有数据时直接推送而非等待tcp_write_xmit()。另一个竞态场景存在于tcp_push_pending_frames()与tcp_data_snd_check()之间。tcp_push_pending_frames()由setsockopt或sendmsg的进程上下文调用而tcp_data_snd_check()由ACK接收的softirq路径触发。两者最终都调用__tcp_push_pending_frames()而该函数调用tcp_write_xmit()。两个路径的nonagle参数来源不同- 进程上下文来自tp-nonagle或TCP_NAGLE_CORK经tcp_push()处理- softirq上下文来自__tcp_data_snd_check()中的tp-nonagle如果用户在进程上下文通过setsockopt修改了tp-nonagle例如清除TCP_CORK而softirq路径正在使用旧的tp-nonagle值读取tcp_nagle_check()——这是典型的READ-ONCE和WRITE-ONCE问题。实际上由于do_tcp_setsockopt()持有lock_sock而tcp_data_snd_check()虽在softirq但通过bh_lock_sock获取锁两者互斥。但tcp_push_pending_frames()在进程上下文持有lock_sock时假设进程softirq不会重入——这仅在RT preempt下存在变数。TCP_CORK和Nagle的互斥还体现在MSS动态变化场景。当PMTU发现更新MSS时tcp_current_mss()的返回值改变tcp_nagle_check()中的skb-len mss_now判定可能翻转。原本小于MSS的段在MSS缩小后可能等于MSS从而自动绕过Nagle/CORK约束。反之MSS增大使原本全尺寸段变为部分段触发送出后被Nagle阻塞的静默期——这种情况通常由tcp_write_xmit()的tcp_init_tso_segs()重新分片处理但分片结果是新skb入队需要重新经过Nagle检查。最后一个rare但影响严重的边界tcp_nagle_check()中nonagle参数的值不恒等于tp-nonagle。在tcp_write_xmit()中队尾skb拿到调用者传入的nonagle可能包含TCP_NAGLE_CORK而非队尾拿到TCP_NAGLE_PUSH。如果发送队列只有一个skb则它是队尾且不是队尾的判定是同一个——tcp_skb_is_last()返回true所以该skb受TCP_NAGLE_CORK约束。但如果发送队列中存在多个skb最末一个受CORK约束前面的被TCP_NAGLE_PUSH跳过。这意味着写入了多个数据块时只有最后一个块可能被CORK住——前面的块只要拥塞窗口允许就会被发出。应用层如果使用多次write()一次TCP_CORK关闭的模式需要注意只有最后一次write()的数据受CORK影响这是tcp_write_xmit()的遍历顺序决定的与用户期望的所有数据都被cork住不同。c/* 场景写队列中有三个skbTCP_CORK已设置* skb0 - skb1 - skb2(tail)** tcp_write_xmit遍历顺序* skb0: tcp_skb_is_last()false - nonagleTCP_NAGLE_PUSH - 跳过Nagle检查* skb1: tcp_skb_is_last()false - nonagleTCP_NAGLE_PUSH - 跳过Nagle检查* skb2: tcp_skb_is_last()true - nonagletp-nonagle(含TCP_NAGLE_CORK) - CORK生效** 结果skb0和skb1被发送skb2被cork住等待下一个Push事件*/这一行为由tcp_push()在调用__tcp_push_pending_frames()前判断MSG_MORE并覆盖nonagle为TCP_NAGLE_CORK来部分缓解。当应用层在write()时使用了MSG_MORE标志或者TCP_CORK始终置位所有tcp_push()调用都将nonagle设为TCP_NAGLE_CORK因此tcp_write_xmit()遍历时的队尾skb受CORK约束。但如果在CORK置位时应用层以非MSG_MORE标志write()——这在具体实现中可能发生比如glibc的缓冲写与时序竞争tcp_push()传入tp-nonagle而非TCP_NAGLE_CORK导致上述部分发送现象。Eric Dumazet引入的TCP autocorking机制在3.18之后进一步复杂化了这一互斥模型。autocork在TSQ回调tcp_wfree - tcp_release_cb - tcp_write_xmit中被释放此时nonagle参数来自tcp_release_cb()中的tp-nonagle。如果用户设置了TCP_NODELAYTCP_NAGLE_OFFautocork期间被throttled的数据在wfree回调中将被tcp_nagle_test()中的TCP_NAGLE_OFF检查无条件放行。这避免了Nagle延迟TSQ释放的问题但也意味着autocork的有效性在TCP_NODELAY socket上被部分削弱——autocork虽然设置了TSQ_THROTTLED但tsq回调中的tcp_write_xmit()不会因Nagle而阻塞仅受拥塞窗口限制。tcp_write_xmit()遍历队列时对队尾和非队尾skb的nonagle差异化处理是理解CORK实际效果的关键线索引申。许多开发者错误假设TCP_CORK会扣留所有数据直到显式关闭但内核的实现语义是仅队尾skb受CORK约束历史更早入队的skb在tcp_write_xmit()的同一轮遍历中已被TCP_NAGLE_PUSH放出。如果应用层需要在单次系统调用中保证CORK的完整语义必须结合MSG_MORE标志使用或在所有write()完成后调用setsockopt关闭CORK依靠tcp_push_pending_frames()中的tcp_write_xmit()完整遍历来确保数据有序发出。