Linux 内核网络协议栈 ------ tcp重传数据包 tcp_retransmit_skb 函数

本文深入探讨了TCP协议中重传机制的实现细节,包括如何判断重传时机、 skb(socket buffer)的处理流程及内存管理策略。文章还介绍了如何避免无效重传,并通过实例解释了重传过程中的关键函数作用。


/* This retransmits one SKB.  Policy decisions and retransmit queue
 * state updates are done by the caller.  Returns non-zero if an
 * error occurred which prevented the send.
 */
int tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb)
{
         struct tcp_sock *tp = tcp_sk(sk);
         struct inet_connection_sock *icsk = inet_csk(sk);
         unsigned int cur_mss = tcp_current_mss(sk, 0);
         int err;
 
         /* Inconslusive MTU probe */
         if (icsk->icsk_mtup.probe_size) {
                 icsk->icsk_mtup.probe_size = 0;
         }
 
         /* Do not sent more than we queued. 1/4 is reserved for possible  
          * copying overhead: fragmentation, tunneling, mangling etc.
          */ // 如果消耗很多的内存做其他事,那么就没有多余的来做队列的处理了~
         if (atomic_read(&sk->sk_wmem_alloc) >                                     // sk_wmem_alloc:传输队列大小
             min(sk->sk_wmem_queued + (sk->sk_wmem_queued >> 2), sk->sk_sndbuf))   // sk_wmem_queud:固定的队列大小
                 return -EAGAIN;
 
         if (before(TCP_SKB_CB(skb)->seq, tp->snd_una)) {// 若这样,说明是有一部分数据才需要重传,形如:seq---snd_una---end_seq,前面一半已收到ACK
                 if (before(TCP_SKB_CB(skb)->end_seq, tp->snd_una))  // 若这样,说明全部ACK,无需重传,BUG
                         BUG();
                 if (tcp_trim_head(sk, skb, tp->snd_una - TCP_SKB_CB(skb)->seq))  // 一些控制信息检查
                         return -ENOMEM;
         }
 
         /* If receiver has shrunk his window, and skb is out of
          * new window, do not retransmit it. The exception is the
          * case, when window is shrunk to zero. In this case
          * our retransmit serves as a zero window probe.
          */
         if (!before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))    // 如果数据在窗口后面,不会发送
             && TCP_SKB_CB(skb)->seq != tp->snd_una)   
                return -EAGAIN;
 
         if (skb->len > cur_mss) {   // 如果skb长度 > MSS
                 if (tcp_fragment(sk, skb, cur_mss, cur_mss))   // 先分片。再传送
                         return -ENOMEM; /* We'll try again later. */
         }
 
         /* Collapse two adjacent packets if worthwhile and we can. */  // 我*,这么多条件
         if (!(TCP_SKB_CB(skb)->flags & TCPCB_FLAG_SYN) &&              // SYN包
             (skb->len < (cur_mss >> 1)) &&                             // 长度<半个MSS
             (tcp_write_queue_next(sk, skb) != tcp_send_head(sk)) &&    // 不是结尾
             (!tcp_skb_is_last(sk, skb)) &&                             // 不是最后一个
             (skb_shinfo(skb)->nr_frags == 0 &&                         // 没有分页数据
              skb_shinfo(tcp_write_queue_next(sk, skb))->nr_frags == 0) &&
             (tcp_skb_pcount(skb) == 1 &&                               // gso_segs=1
              tcp_skb_pcount(tcp_write_queue_next(sk, skb)) == 1) &&
             (sysctl_tcp_retrans_collapse != 0))
                 tcp_retrans_try_collapse(sk, skb, cur_mss);            // 这个函数不是很明白,待看~~~~~~~~~~~~~~~~~~~~~~~~~
 
         if (inet_csk(sk)->icsk_af_ops->rebuild_header(sk)) // 根据目的地址等条件获取路由,如果获取路由失败就不能发送
                 return -EHOSTUNREACH; /* Routing failure or similar. */
 
         /* Some Solaris stacks overoptimize and ignore the FIN on a
          * retransmit when old data is attached.  So strip it off
          * since it is cheap to do so and saves bytes on the network.
          *///Solaris系统的协议栈有时候会忽略重传SKB上带有的FIN标志的payload,将payload全部剥离掉,节省网络流量
         if (skb->len > 0 &&
             (TCP_SKB_CB(skb)->flags & TCPCB_FLAG_FIN) &&
             tp->snd_una == (TCP_SKB_CB(skb)->end_seq - 1)) {
                 if (!pskb_trim(skb, 0)) {
                         /* Reuse, even though it does some unnecessary work */
                         tcp_init_nondata_skb(skb, TCP_SKB_CB(skb)->end_seq - 1,
                                              TCP_SKB_CB(skb)->flags);
                         skb->ip_summed = CHECKSUM_NONE;
                 }
         }
 
         /* Make a copy, if the first transmission SKB clone we made
          * is still in somebody's hands, else make a clone.
          */
         TCP_SKB_CB(skb)->when = tcp_time_stamp;
 
         err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC);    // 这个才是正在的传输函数~~~~~~~~~~~~~~~~~~~~后面再说~~~~~~~~~~~~~~~
                                                            // 这个函数就是将数据包发送到下面一层,再慢慢传输出去~~~~~~~~~~~~~~
         if (err == 0) {    // 发送成功,那么就需要更新TCP统计信息
                 /* Update global TCP statistics. */  
                 TCP_INC_STATS(TCP_MIB_RETRANSSEGS);
 
                 tp->total_retrans++;   // 整体重传数量++
 
#if FASTRETRANS_DEBUG > 0
                 if (TCP_SKB_CB(skb)->sacked & TCPCB_SACKED_RETRANS) {
                         if (net_ratelimit())
                                 printk(KERN_DEBUG "retrans_out leaked.\n");
                 }
#endif
                 if (!tp->retrans_out)
                         tp->lost_retrans_low = tp->snd_nxt;
                 TCP_SKB_CB(skb)->sacked |= TCPCB_RETRANS;
                 tp->retrans_out += tcp_skb_pcount(skb);   // 重传出去的数量+=。。。
 
                 /* Save stamp of the first retransmit. */
                 if (!tp->retrans_stamp)
                         tp->retrans_stamp = TCP_SKB_CB(skb)->when;  // 第一次重传时间戳
 
                 tp->undo_retrans++;
 
                 /* snd_nxt is stored to detect loss of retransmitted segment,
                  * see tcp_input.c tcp_sacktag_write_queue().
                  */
                 TCP_SKB_CB(skb)->ack_seq = tp->snd_nxt;
         }
         return err;
}
 



<think>好的,用户想了解Linux内核TCP协议栈在发生超时重传时,重传数据包和新数据包的发送顺序。这个问题涉及到TCP重传机制和队列管理,我需要先回忆一下相关机制。 首先,TCP在检测到数据包丢失时,会触发重传。超时重传通常发生在RTO(Retransmission Timeout)超时的情况下,这时候TCP会进入恢复状态。根据RFC标准,当出现超时重传时,发送方应该优先发送未确认的数据,也就是旧的数据包,以确保可靠性。 接着,Linux内核的实现可能遵循这个原则。在超时发生后,内核会将未确认的数据重新放入发送队列,并且可能会优先处理这些重传数据包。而新数据则可能需要等待重传完成后再发送,以避免拥塞加剧。不过,具体情况可能取决于内核版本和使用的拥塞控制算法。 用户提到的重传数据包和新数据包的发送顺序,需要确认在超时重传期间,内核如何处理新的数据。比如,是否允许新数据在重传之前发送,或者是否必须等待所有重传完成。这可能涉及到TCP的“保守”策略,即在恢复阶段优先处理未确认的数据。 另外,可能需要参考Linux内核中的相关代码或文档,比如tcp_retransmit_skb函数,以及拥塞控制模块的处理逻辑。例如,在超时发生后,拥塞窗口可能会被重置为1,进入慢启动阶段,此时每次只能发送一个报文段,直到确认到达。这种情况下,新数据可能需要排队,直到重传的数据被确认。 不过,用户提供的引用资料中提到了nstat和TcpExtIPReversePathFilter,这可能与反向路径过滤有关,但和重传顺序关系不大。所以主要还是要依赖TCP协议本身的机制和Linux的实现细节。 总结一下,可能的顺序是:当发生超时重传时,内核会优先发送重传数据包,而新生成的数据包需要等待这些重传完成后再发送。这是为了快速修复丢包,避免网络拥塞进一步恶化。同时,拥塞窗口的调整也会影响发送顺序,例如在超时后窗口缩小,限制新数据的发送。 需要验证这一点是否正确。比如,查看Linux内核TCP实现,确认在重传队列处理时,是否将重传数据放在队列头部,优先发送。另外,是否有机制允许新数据在重传期间穿插发送,这可能会影响性能但提高吞吐量。 最后,结合RFC 6298和Linux内核的拥塞控制逻辑,可以得出结论:超时重传时,重传数据包会被优先发送,新数据包重传之后发送,直到所有未确认的数据得到处理,或者拥塞窗口允许新的传输。</think>在Linux内核TCP协议栈中,当发生超时重传时,**重传数据包会优先于新数据包发送**。这种设计基于TCP可靠性传输的核心机制,具体原理如下: --- ### 一、重传队列优先级机制 1. **重传队列独立管理** Linux内核维护独立的$retransmit\_queue$队列存放需要重传的报文段。当RTO(重传超时)触发时,内核会将该报文段从常规发送队列移动到重传队列,并立即进行重传操作[^1]。 2. **发送优先级判定** 在发送新数据前,内核会检查$tcp\_write\_queue$和$retransmit\_queue$的优先级: ```c // Linux内核代码片段(简化) if (!tcp_send_head(sk) || tcp_pacing_check(sk)) { tcp_xmit_retransmit_queue(sk); // 优先处理重传队列 } else { tcp_write_xmit(sk, mss_now, nonagle); // 发送新数据 } ``` --- ### 二、拥塞控制影响 1. **RTO触发拥塞状态重置** 超时重传会使TCP进入$TCP\_CA\_Loss$状态,此时: - 拥塞窗口($cwnd$)被重置为1 MSS - 慢启动阈值($ssthresh$)降为当前窗口的一半 $$ssthresh = \max(cwnd/2, 2)$$ 这种保守策略强制要求**每次只能发送一个报文段**,直到收到ACK确认[^2]。 2. **新数据发送限制** 在$cwnd=1$的情况下,新数据必须等待重传报文被确认后才能发送: ``` 发送顺序示例: 1. 发送重传报文段#3 (cwnd=1) 2. 收到#3的ACK后,cwnd恢复增长 3. 发送新报文段#6 ``` --- ### 三、例外情况处理 1. **部分确认(Partial ACK)** 如果收到新数据的ACK(如确认了报文段#4但未确认#3),Linux会立即重传#3(快速重传机制),但这种情况属于快速恢复流程,与超时重传有本质区别。 2. **尾部丢失(Tail Loss)** 当多个连续报文丢失时,内核会通过$FACK$(Forward Acknowledgment)算法识别并批量重传,但仍遵循**旧数据优先**原则。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值