TCP发送源码学习(2)--tcp_write_xmit

本文详细介绍了TCP协议中数据发送的整个流程,包括拥塞控制、窗口管理等关键环节,并深入探讨了tcp_write_xmit()、tcp_init_tso_segs()等核心函数的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、tcp_write_xmit()将发送队列上的SBK发送出去,返回值为0表示发送成功。函数执行过程如下:
1、检测拥塞窗口的大小。
2、检测当前报文是否完全处在发送窗口内。
3、检测报文是否使用nagle算法进行发送。
4、通过以上检测后将该SKB发送出去。

5、循环检测发送队列上所有未发送的SKB。

static int 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);
    struct sk_buff *skb;
    unsigned int tso_segs, sent_pkts;
    int cwnd_quota;
    int result;

    /*sent_pkts用来统计函数中已发送报文总数。*/
    sent_pkts = 0;

    if (!push_one) {
        /* Do MTU probing. */
        result = tcp_mtu_probe(sk);
        if (!result) {
            return 0;
        } else if (result > 0) {
            sent_pkts = 1;
        }
    }
    /*13~21首先初始化为0,接着发送一个路径MTU探测报文,如果成功则发送报文数加1。*/

    /*如果发送队列不为空,则准备开始发送报文*/
    while ((skb = tcp_send_head(sk))) {
        unsigned int limit;

      /*设置有关TSO的信息,包括GSO类型,GSO分段的大小等等。这些信息是准备给软件TSO分段使用的。
        如果网络设备不支持TSO,但又使用了TSO功能,则报文在提交给网络设备之前,需进行软分段,即由代码实现TSO分段。*/
        tso_segs = tcp_init_tso_segs(sk, skb, mss_now);
        BUG_ON(!tso_segs);

      /*检测拥塞窗口的大小,如果为0,则说明拥塞窗口已满,目前不能发送。
        拿拥塞窗口和正在网络上传输的包数目相比,如果拥塞窗口还大,则返回拥塞窗口减掉正在网络上传输的包数目剩下的大小。
        该函数目的是判断正在网络上传输的包数目是否超过拥塞窗口,如果超过了,则不发送。
        tcp_cwnd_test()源代码见段二*/
        cwnd_quota = tcp_cwnd_test(tp, skb);
        if (!cwnd_quota)
            break;
       
        /*检测当前报文是否完全处于发送窗口内,如果是则可以发送,否则不能发送*/
        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
            break;

        /*tso_segs=1表示无需tso分段*/
        if (tso_segs == 1) {
            /*根据nagle算法,计算是否需要发送数据*/
            if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
                         (tcp_skb_is_last(sk, skb) ?
                         nonagle : TCP_NAGLE_PUSH))))
                break;
        } else {
          /*如果需要TSO分段,则检测该报文是否应该延时发送。tcp_tso_should_defer()用来检测GSO段是否需要延时发送。
            在段中有FIN标志,或者不处于open拥塞状态,或者TSO段延时超过2个时钟滴答,或者拥塞窗口和发送窗口的最小值大于64K或三倍的当前有效MSS,
            在这些情况下会立即发送,而其他情况下会延时发送,这样主要是为了减少软GSO分段的次数,以提高性能。*/
            if (!push_one && tcp_tso_should_defer(sk, skb))
                break;
        }

        /*limit为再次分段的段长,初始化为当前MSS*/
        limit = mss_now; 
      /*在TSO分片大于1并且不是URG模式下,通过mss_now计算limit的值
        以发送窗口和拥塞窗口的最小值作为分段段长*/
        if (tso_segs > 1 && !tcp_urg_mode(tp))
            limit = tcp_mss_split_point(sk, skb, mss_now,
                         cwnd_quota);

        /*如果SKB中的数据长度大于分段段长,则调用tso_fragment()根据该段长进行分段,如果分段失败则暂不发送*/
        if (skb->len > limit &&
         unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))
            break;
      /*line61~71:根据条件,可能需要对SKB中的报文进行分段处理,分段的报文包括两种:一种是普通的用MSS分段的报文,另一种则是TSO分段的报文。
        能否发送报文主要取决于两个条件:一是报文需完全在发送窗口中,而是拥塞窗口未满。第一种报文,应该不会再分段了,因为在tcp_sendmsg()中创建报文的SKB时已经根据MSS处理了,
        而第二种报文,则一般情况下都会大于MSS,因为通过TSO分段的段有可能大于拥塞窗口的剩余空间,如果是这样,就需要以发送窗口和拥塞窗口的最小值作为段长对报文再次分段。*/

      /*更新TCP时间戳,记录此报文发送的时间,
        #define tcp_time_stamp	 ((__u32)(jiffies))*/
        TCP_SKB_CB(skb)->when = tcp_time_stamp;

        /*调用tcp_transmit_skb()发送TCP段,其中第三个参数1表示是否需要克隆被发送的报文,详见后续对此函数的分析*/
        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
            break;

        /* Advance the send_head. This one is sent out.
         * This call will increment packets_out.
         */
      /*调用tcp_event_new_data_sent()-->tcp_advance_send_head()更新sk_send_head,即取发送队列中的下一个SKB。
        同时更新snd_nxt,即等待发送的下一个TCP段的序号,然后统计发出但未得到确认的数据报个数。
        最后如果发送该报文前没有需要确认的报文,则复位重传定时器,对本次发送的报文做重传超时计时。*/
        tcp_event_new_data_sent(sk, skb);

      /*更新struct tcp_sock中的snd_sml字段。snd_sml表示最近发送的小包(小于MSS的段)的最后一个字节序号,
        在发送成功后,如果报文小于MSS,即更新该字段,主要用来判断是否启动nagle算法*/
        tcp_minshall_update(tp, mss_now, skb);
        sent_pkts++;//更新已发送报文总数

        if (push_one)
            break;
    }

    /*如果本次有数据发送,则对TCP拥塞窗口进行检查确认。*/
    if (likely(sent_pkts)) {
        tcp_cwnd_validate(sk);
        return 0;
    }

    /*如果本次没有数据发送,则根据已发送但未确认的报文数packets_out和sk_send_head返回,packets_out不为零或sk_send_head为空都视为有数据发出,因此返回成功。*/
    return !tp->packets_out && tcp_send_head(sk);
}

二、 tcp_init_tso_segs()函数
该函数根据当前mss的值重新设置数据包中的struct skb_shared_info内的关于GSO的内容项。

static int tcp_init_tso_segs(struct sock *sk, struct sk_buff *skb,
             unsigned int mss_now)
{
    int tso_segs = tcp_skb_pcount(skb);

    if (!tso_segs || (tso_segs > 1 && tcp_skb_mss(skb) != mss_now)) {
        tcp_set_skb_tso_segs(sk, skb, mss_now);
        tso_segs = tcp_skb_pcount(skb);
    }
    return tso_segs;
}

、tcp_cwnd_test()函数

static inline unsigned int tcp_cwnd_test(struct tcp_sock *tp,
                     struct sk_buff *skb)
{
    u32 in_flight, cwnd;

    /* Don't be strict about the congestion window for the final FIN. */
    /*对FIN包不检测,让他通过*/
    if ((TCP_SKB_CB(skb)->flags & TCPHDR_FIN) && tcp_skb_pcount(skb) == 1)
        return 1;

    /*计算正在网络上传输的包数目*/
    in_flight = tcp_packets_in_flight(tp);
    /*获取当前拥塞窗口的大小,snd_cwnd表示当前拥塞窗口的大小*/
    cwnd = tp->snd_cwnd;
    if (in_flight < cwnd)
        return (cwnd - in_flight);

    return 0;
}
static inline unsigned int tcp_packets_in_flight(const struct tcp_sock *tp)
{
    return tp->packets_out - tcp_left_out(tp) + tp->retrans_out;
}
static inline unsigned int tcp_left_out(const struct tcp_sock *tp)
{
    return tp->sacked_out + tp->lost_out;
}
这是通过使用 tcp sock 中的几个计数器运算得出的
packets_out:从发送队列发出而未得到确认的TCP段的数目,该值是动态的,当有新的段发出或有新的确认收到都会增加或减少该值。
retrans_out:重传还未得到确认的TCP段数目。

tcp_left_out:已离开主机在网络中且未确认的TCP段数,包含两种情况, 一是通过SACK确认的段,即sacked_out,而是已丢失的段,即lost_out。
所以left_out = sacked_out + lost_out。
left_out需要与p ackets_out进行区分,packets_out只是离开发送队列(不一定已离开主机 ),而left_out则必定离开了主机。所以packets_out必定大于或等于left_out。

四、tcp_snd_wnd_test()函数

static inline int tcp_snd_wnd_test(struct tcp_sock *tp, struct sk_buff *skb,
                 unsigned int cur_mss)
{
    u32 end_seq = TCP_SKB_CB(skb)->end_seq;

    if (skb->len > cur_mss)
        end_seq = TCP_SKB_CB(skb)->seq + cur_mss;

    return !after(end_seq, tcp_wnd_end(tp));
}

#define TCP_SKB_CB(__skb)    ((struct tcp_skb_cb *)&((__skb)->cb[0]))
TCP层在SKB区块有个私有信息控制块,即skb_buff结构的cb成员,TCP利用这个字段存储了一个tcp_skb_cb结构。在TCP层,用宏TCP_SKB_CB实现访问该信息控制块,以增强代码的可读性。

struct tcp_skb_cb {
    union {
        struct inet_skb_parm    h4;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
        struct inet6_skb_parm    h6;
#endif
    } header;    /* For incoming frames        */
    __u32        seq;        /* Starting sequence number    */
    __u32        end_seq;    /* SEQ + FIN + SYN + datalen    */
    __u32        when;        /* used to compute rtt's    */
    __u8        flags;        /* TCP header flags.        */
    __u8        sacked;        /* State flags for SACK/FACK.    */
#define TCPCB_SACKED_ACKED    0x01    /* SKB ACK'd by a SACK block    */
#define TCPCB_SACKED_RETRANS    0x02    /* SKB retransmitted        */
#define TCPCB_LOST        0x04    /* SKB is lost            */
#define TCPCB_TAGBITS        0x07    /* All tag bits            */

#define TCPCB_EVER_RETRANS    0x80    /* Ever retransmitted frame    */
#define TCPCB_RETRANS        (TCPCB_SACKED_RETRANS|TCPCB_EVER_RETRANS)

    __u32        ack_seq;    /* Sequence number ACK'd    */
}

union {...} header:
   在TCP处理接收到的TCP段之前,下层协议(IPv4或IPv6)会先处理该段,且会利用SKB中的控制块来记录每一个包中的信息,例如IPv4会记录从IP首部中解析出来的IP首部选项。
为了不破坏三层协议层私有数据,在SKB中TCP控制块的前部定义了这个结构,这包括IPv4和IPv6。
__u32 seq
__u32 end_seq
   seq为当前段开始序号,而end_seq为当前段开始序号加上当前段数据长度,如果标志域中存在SYN或FIN标志,则还需加1,因为SYN和FIN标志都会消耗一个序号,利用end_seq、seq和标志,很容易得到数据长度。
__u32 when
   段发送时间及段发送时记录的当前jffies值。必要时,此值也用来计算RTT。
__u8 flags
   记录原始TCP首部标志。发送过程中,tcp_transmit_skb()在发送TCP段之前会根据此标志来填充发送段的TCP首部的标志域;接收过程中,会提取接收段的TCP首部标志到该字段中。
__u8 sacked
   主要用来描述段的重传状态,同时标识包是否包含紧急数据。检查接收到的SACK,根据需要更新TCPCB_TAGBITS,重传引擎会根据该标志位来确定是否需要重传。一旦重传超时发生,所有的SACK状态标志将被清除,因为无须再关心其状态。无论通过哪种方式重传了包,重传超时或快速重传,都会设置TCPCB_EVER_RETRANS标志位。tcp_restransmit_skb()中设置TCPCB_EVER_RETRANS和TCPCB_SACKED_RETRANS标志位,tcp_enter_loss()中则清除TCPCB_SACKED_RETRANS标志位。
sacked的取值如下:
TCPCB_SACKED_ACKED:该段通过SACK被确认。
TCPCB_SACKED_RETRANS:该段已经重传。
TCPCB_LOST:该段在传输过程中已丢失。
__u32 ack_seq
    接收到的TCP段首部中的确认序号。
from: http://sunjiangang.blog.chinaunix.net/uid-9543173-id-3543419.html

<think>我们正在讨论Linux内核TCP协议栈中的函数。根据引用[1]和[2],我们知道TCP发送数据有两个主要函数:tcp_sendmsg(用于应用层数据)和tcp_transmit_skb(用于实际传输数据包)。用户询问的是tcp_send_head()函数的作用。 在Linux内核中,tcp_send_head()函数通常用于获取发送队列中尚未发送的第一个skb(socket buffer)。这个函数在内核源码中定义,其作用是从TCP控制块(tcp_sock)的发送队列中返回第一个未发送的skb。 具体来说,tcp_send_head()函数的作用是: 1. 获取发送队列(sk_write_queue)中尚未发送的第一个数据包(skb)。发送队列是一个链表,按照顺序存放待发送的数据包。 2. 在发送数据的过程中,比如在tcp_write_xmit()函数(引用[2]中提到的发送函数)中,会使用tcp_send_head()来获取下一个要发送的数据包。 因此,tcp_send_head()函数在TCP数据传输中的作用是提供发送队列的头部skb,以便协议栈可以继续发送数据。 下面我们详细解释: 在TCP协议中,发送的数据被分成多个数据包(skb),并按照顺序放入发送队列(sk_write_queue)中。当需要发送数据时(例如,由tcp_write_xmit函数处理),协议栈会从发送队列中取出数据包发送tcp_send_head()函数的典型实现如下(以Linux内核为例): static inline struct sk_buff *tcp_send_head(const struct sock *sk) { return skb_peek(&sk->sk_write_queue); } 它使用skb_peek()函数来获取发送队列的第一个skb,而不将它从队列中移除。 在发送过程中,tcp_write_xmit()函数会循环发送发送队列中的skb,直到遇到拥塞窗口限制或发送窗口限制等。在循环中,它会调用tcp_send_head()来获取下一个要发送的skb。 此外,当需要重传数据时,也会用到tcp_send_head()来获取需要重传的第一个数据包(在快速重传等机制中)。 总结:tcp_send_head()函数是TCP协议栈内部用于获取发送队列中第一个未发送数据包(skb)的辅助函数。它在数据发送和重传过程中起着关键作用,确保协议栈能够按顺序处理发送队列中的数据。 注意:由于我们讨论的是内核函数,具体实现可能因内核版本而异,但基本功能一致。 根据要求,我们生成相关问题: §§相关问题§§ 1. TCP发送队列(sk_write_queue)是如何管理的? 2. 在TCP重传机制中,tcp_send_head()函数有什么作用? 3. 除了tcp_send_head(),还有哪些函数用于操作TCP发送队列?</think>### `tcp_send_head()` 函数在 TCP 协议栈中的作用 `tcp_send_head()` 是 Linux 内核 TCP 协议栈中的关键辅助函数,其主要作用是从套接字的发送队列中**获取下一个待发送的 SKB(Socket Buffer)**。它在 TCP 数据发送流程(如 `tcp_write_xmit()` 函数)中扮演核心角色,具体功能如下: #### 1. **定位发送队列头部** - 发送队列 `sk_write_queue` 按顺序存储所有待发送的 SKB(包括新数据和待重传数据)。 - `tcp_send_head()` 通过 `skb_peek(&sk->sk_write_queue)` 获取队列中**第一个未发送/待重传的 SKB**,而不将其移出队列[^1][^2]。 #### 2. **支持发送流程的核心循环** 在发送函数 `tcp_write_xmit()` 中: ```c while ((skb = tcp_send_head(sk))) { // 检查拥塞窗口、接收窗口等条件 if (发送条件满足) tcp_transmit_skb(sk, skb); // 发送数据包 else break; } ``` - 该函数驱动循环逐包处理发送队列,确保按顺序发送数据[^2]。 #### 3. **动态适应网络状态** - 在拥塞控制中,当拥塞窗口(`cwnd`)变化时,`tcp_send_head()` 帮助协议栈快速定位可发送的新数据包。 - 在接收窗口(`snd_wnd`)扩大时,协助及时填充新增的可用带宽[^2]。 #### 4. **重传机制的关键支持** - 当需要重传丢失数据包时(如触发快速重传),`tcp_send_head()` 能准确返回待重传队列头部的 SKB,确保重传从正确位置开始[^1]。 #### 典型调用场景示例 1. **初始数据发送** 应用层数据通过 `tcp_sendmsg()` 放入发送队列后,`tcp_write_xmit()` 调用 `tcp_send_head()` 获取首个 SKB 并发送2. **拥塞窗口更新后** 收到 ACK 时,若拥塞窗口扩大,协议栈通过 `tcp_send_head()` 立即获取新可发送的 SKB。 3. **重传超时处理** 在 RTO(Retransmission Timeout)事件中,重传逻辑使用 `tcp_send_head()` 定位需重传的起始 SKB。 --- ### 总结 `tcp_send_head()` 的核心功能是**高效、安全地访问 TCP 发送队列的头部 SKB**,确保协议栈能: 1. 按顺序处理待发送数据包 2. 动态响应网络条件变化(拥塞窗口、接收窗口) 3. 精准触发重传机制 其设计显著提升了 TCP 协议栈的发送效率和健壮性[^1][^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值