TCP数据发送接口

1.1 tcp_sendmsg

使用 TCP 发送数据的大部分工作都是在tcp_sendmsg函数中实现的。

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;
    struct sockcm_cookie sockc;
    int flags, err, copied = 0;
    int mss_now = 0, size_goal, copied_syn = 0;
    bool process_backlog = false;
    bool sg;
    long timeo;

    /*
     * 在发送和接收TCP数据前都要对传输控制块上锁,以免
     * 应用程序主动发送接收和传输控制块被动接收而导致
     * 控制块中的发送或接收队列混乱。
     */
    lock_sock(sk);

    flags = msg->msg_flags;
    if (flags & MSG_FASTOPEN) {
        err = tcp_sendmsg_fastopen(sk, msg, &copied_syn, size);
        if (err == -EINPROGRESS && copied_syn > 0)
            goto out;
        else if (err)
            goto out_err;
    }
/*
     * 获取发送数据是否进行阻塞标识,如果阻塞,则通过
     * sock_sndtimeo()获取阻塞超时时间。发送阻塞超时时间保存
     * 在sock结构的sk_sndtimeo成员中。
     */
    timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);skb_shinfo

    /* Wait for a connection to finish. One exception is TCP Fast Open
     * (passive side) where data is allowed to be sent before a connection
     * is fully established.
     */
      /* 
        * sk_state的值在tcp_states.h中定义,使用的是上面的
        * TCP_ESTABLISHED所在的枚举中的值,而不是TCPF_ESTABLISHED
        * 所在的枚举。上下两个枚举的关系是:
        * TCPF_xxx = 1<<TCP_xxx。TCPF_ESTABLISHED所在的枚举
        * 只是用来验证sk->sk_state中的状态是什么,通过
        * 位运算可以同时验证多个,减少比较的次数。
        * 这里或许是为了兼容以前的作法,或许是
        * 协议规定,否则可以将TCP_xxx直接使用TCPF_xxx的形式即可
        */
    /*
     * TCP只在ESTABLISHED或CLOSE_WAIT这两种状态下,接收窗口
     * 是打开的,才能接收数据。因此如果不处于这两种
     * 状态,则调用sk_stream_wait_connect()等待建立起连接,一旦
     * 超时则跳转到out_err处做出错处理。
     */
    if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) &&
        !tcp_passive_fastopen(sk)) {
        err = sk_stream_wait_connect(sk, &timeo);kmem_cache_alloc_node
        if (err != 0)
            goto do_error;
    }

    if (unlikely(tp->repair)) {
        if (tp->repair_queue == TCP_RECV_QUEUE) {
            copied = tcp_send_rcvq(sk, msg, size);
            goto out_nopush;
        }

        err = -EINVAL;
        if (tp->repair_queue == TCP_NO_QUEUE)
            goto out_err;

        /* 'common' sending to sendq */
    }

    sockc.tsflags = sk->sk_tsflags;
    if (msg->msg_controllen) {
        err = sock_cmsg_send(sk, msg, &sockc);
        if (unlikely(err)) {
            err = -EINVAL;
            goto out_err;
        }
    }

    /* This should be in poll */
    sk_clear_bit(SOCKWQ_ASYNC_NOSPACE, sk);

    /* Ok commence sending. */
    copied = 0;

restart:
    /*
     * 调用tcp_send_mss()获取当前有效MSS,tcp_current_mss()中MSG_OOB是判断是否支持GSO的条件之一,而
     * 带外数据不支持GSO。
     * 这里除了获取当前的MSS外,还会获取目标发送的数据
     * size_goal存储的是TCP分段中数据部分的最大长度,如果网卡不支持TSO,
     * 其值和MSS是相等的;如果网卡支持TSO,其值要综合考虑网卡支持
     * 的最大分段大小及接收方通告的最大窗口等,参见tcp_xmit_size_goal().
     *
     获取 MSS 大小。 size_goal 是数据报到达网络设备时所允许的最大长度。
     * 对于不支持分片的网卡, size_goal 是 MSS 的大小。否则,是 MSS 的整倍数。
     */
    mss_now = tcp_send_mss(sk, &size_goal, flags);

    err = -EPIPE;
    /*在使用shutdown关闭了发送之后,再次调用tcp_sendmsg发送数据
    那么该函数会返回错误;*/
    if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
        goto out_err;

    sg = !!(sk->sk_route_caps & NETIF_F_SG);//是否支持 SG(scatter/gather)聚合分散IO
 /* 遍历发送缓存 */
    while (msg_data_left(msg)) {
        int copy = 0;
        int max = size_goal;
    
  /* 拿到发送队列的尾部skb,因为只有队尾的skb才有空间存储数据 */
        skb = tcp_write_queue_tail(sk);
        if (tcp_send_head(sk)) {//判断发送队列是否为空,如果为空则当前获取skb无效
            if (skb->ip_summed == CHECKSUM_NONE)
                max = mss_now;
            copy = max - skb->len;// 如果skb已经使用空间还没有达到size_goal 表明还可以往skb里面赋值数据
        }
/* 剩余空间为0,或者不能合并,分配一个新的skb */
        if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
            //说明sk发送队列的最后一个skb已经没有多余的空间了,则需要从新开辟skb空间
new_segment:
            /* Allocate new segment. If the interface is SG,
             * allocate skb fitting to single page.
             *//* 空闲内存不足,进入等待 */
            if (!sk_stream_memory_free(sk))
                goto wait_for_sndbuf;

            if (process_backlog && sk_flush_backlog(sk)) {
                process_backlog = false;
                goto restart;
            }//注意这里面掉的是alloc_skb_fclone
            skb = sk_stream_alloc_skb(sk,
                          select_size(sk, sg),
                          sk->sk_allocation,
                          skb_queue_empty(&sk->sk_write_queue));
            if (!skb)//如果支持SG(NETIF_F_SG),则无需线性缓冲区,所有数据直接存到shinfo页中
                goto wait_for_memory;

            process_backlog = true;
            /*
             * Check whether we can use HW checksum.
             *//* 网卡允许计算校验和 */
            if (sk_check_csum_caps(sk))
                skb->ip_summed = CHECKSUM_PARTIAL;
 /* 添加到发送队列 */
            skb_entail(sk, skb);
            /*
                 * 初始化copy变量为发送数据包到网络
                 * 设备时最大数据段长度。copy表示每
                 * 次复制到SKB的数据长度。
                 */
            copy = size_goal;
            max = size_goal;

            /* All packets are restored as if they have
             * already been sent. skb_mstamp isn't set to
             * avoid wrong rtt estimation.
             */
            if (tp->repair)
                TCP_SKB_CB(skb)->sacked |= TCPCB_REPAIRED;
        }

        /* Try to append data to the end of skb. */
        //从应用层发送的数据中拷贝复制数据到skb的时候,最多只能拷贝实际数据大小
        if (copy > msg_data_left(msg))
            copy = msg_data_left(msg);

        /* Where to copy to? *//* 线性区域还有空间 */
        if (skb_availroom(skb) > 0) {//说明线性缓冲区中还有数据
            /* We have some space in skb head. Superb! */
             /* 取要拷贝的数量和线性区域的较小值 */
            copy = min_t(int, copy, skb_availroom(skb));//先把tail skb中的剩余空间填上,但最多只能填剩余的空间大小
 /* 从用户空间拷贝到内核 */
            err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);
            //把应用层发送来的数据线填充一部分到tail skb中
            if (err)
                goto do_fault;
        } else { //如果线性缓冲区已经满了,则需要把数据拷贝到shinfo里面
        /* 线性区域没有空间,则使用分页区 */
            bool merge = true;//判断最后一个分页是否能追加数据  1可以  0不可以
             /* 获取页数量 */
            int i = skb_shinfo(skb)->nr_frags;
            /*获取当前SKB的分片段数,在skb_shared_info中用nr_frags表示。*/
             /* 获取缓存的页 */
            struct page_frag *pfrag = sk_page_frag(sk);//获取最后一个分片的页面page 信息;第一次使用的时候这里一般为NULL
             /*如果SKB线性存储区底部已经没有空间了,那就需要把数据复制到支持分散聚合的分页中*/
                /*merge标识是否在最后一个分页中添加数据,初始化为0*/
                //可以参考http://blog.chinaunix.net/uid-23629988-id-196823.html  Scatter/Gather I/O在L3中的应用 

              /* 检查是否有足够的空间,空间不足则申请新页,失败则等待 */
            if (!sk_page_frag_refill(sk, pfrag))
                goto wait_for_memory;
                /* 判断能否在最后一个分片加数据。 */
            if (!skb_can_coalesce(skb, i, pfrag->page,
                          pfrag->offset)) {//该页还没写满,可以继续忘该也写。如果是后面两种else则需要从新分配page页
                if (i == sysctl_max_skb_frags || !sg) {//这个数和skb_shared_info->的frags[]对应
                /* 无法设置分配,那么就重新分配一个 SKB。
                没设置NETIF_F_SG就只能线性化处理;
                nr_frags的值,该字段确定了分段数,这些分散的片段以关联的方式存储在frags数组中
                */
                    tcp_mark_push(tp, skb);//不支持 sg,则设置pushed_seq 成员表示希望尽快发送出去 
                    goto new_segment;
                }
                merge = false;
            }

            copy = min_t(int, copy, pfrag->size - pfrag->offset);
  /*
                 * 在复制数据之前,还需判断用于输出使用的缓存
                 * 是否达到上限,一旦达到则只能等待,直到有可用
                 * 输出缓存或超时为止.
                 */
            if (!sk_wmem_schedule(sk, copy))
                goto wait_for_memory;
  /*
                 * 这时,SKB 的分页已准备好,无论是原先存在还是刚刚分配,
                 * 接下来就调用skb_copy_to_page_nocache()将数据复制到分页中.
                 */
            err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,
                               pfrag->page,
                               pfrag->offset,
                               copy);
            if (err)
                goto do_error;

            /* Update the skb. */
            /*
                 * 完成复制数据到一个分页,这时需要更新有关分段的
                 * 信息.如果是在最后一个页面分段中追加的,则需更新
                 * 该页面内有效数据的长度.
                 */
            if (merge) {//merge为1表示之前已经有数据存在到该分页中了
                skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], copy);
            } else {
            //为0表示刚创建的page页,第一次写数据到该页中
                    /*
                     * 如果是复制到一个新的页面分段中,则需更新的有关
                     * 分段的信息就会多一些,如分段数据的长度、页内偏移、
                     * 分段数量等,这由skb_fill_page_desc()来完成。如果标识最近
                     * 一次分配页面的sk_sndmsg_page不为空,则增加对该页面的
                     * 引用;否则说明复制了数据的页面时新分配的,且没有
                     * 使用完,在增加对该页面的引用的同时,还需要更新
                     * sk_sndmsg_page的值。如果新分配的页面已使用完,就无需
                     * 更新sk_sndmsg_page的值了,因为如果SKB未超过段上限,那么
                     * 下次必定还会分配新的页面,因此在此就省去了对off+copy==PAGE_SIZE
                     * 这条分支的处理
                     */
                skb_fill_page_desc(skb, i, pfrag->page,
                           pfrag->offset, copy);
                get_page(pfrag->page);
            } /*
                     * 复制了新数据,需更新数据尾端在最后一页
                     * 分片的页内偏移.
                     */
                      //记录下该页已经写了数据的内存的页偏移的地方,下次紧跟后面写
            pfrag->offset += copy;
        }
 /*
             * 如果复制的数据长度为零,则取消TCPCB_FLAG_PSH标志.
             */
        if (!copied)
            TCP_SKB_CB(skb)->tcp_flags &= ~TCPHDR_PSH;
 /*
             * 更新发送队列中的最后一个序号write_seq,以及每个数据包的
             * 最后一个序列end_seq,初始化gso分段数gso_segs.
             */
        tp->write_seq += copy;/* 更新 TCP 的序号 */
        TCP_SKB_CB(skb)->end_seq += copy;
        tcp_skb_pcount_set(skb, 0);

        copied += copy;//已经拷贝的总字节数加上最新拷贝的copy字节。
        if (!msg_data_left(msg)) {
            tcp_tx_timestamp(sk, sockc.tsflags, skb);
            if (unlikely(flags & MSG_EOR))
                TCP_SKB_CB(skb)->eor = 1;
            goto out;
        }

        if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair)){
            //说明这个skb还没达到mss,可以继续向其中拷贝下一个iovec中的数据进来
            continue;
        }
        /* 检查该数据是否必须立即发送。 */
        if (forced_push(tp)) { /* 需要使用push标记 */
             /*
                 * 检查是否必须立即发送,即检查自上次发送后
                 * 产生的数据是否已超过对方曾经通告过的最
                 * 大通告窗口值的一半.如果必须立即发送,则设置
                 * PSH标志后调用__tcp_push_pending_frames()将在发送队列
                 * 上的SKB从sk_send_head开始发送出去.
                 * __tcp_push_pending_frames()将发送队列上的段发送出去.如果
                 * 发送失败,则会检测是否需要激活持续定时器.实际上,
                 * 很多处理都是在tcp_write_xmit()中进行的,frames()只是在判断
                 * 是否有段需要发送时简单地调用tcp_write_xmit()发送段,如果
                 * 发送失败,再调用tcp_check_probe_timer()复位持续探测定时器.
                 */
            tcp_mark_push(tp, skb); /* 打psh标记 */
            __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);/* 发送队列中的多个skb */
        } else if (skb == tcp_send_head(sk))
            tcp_push_one(sk, mss_now); /* 否则,有数据要发送,则发送一个skb */
        /*
             * 如果没有必要立即发送,且发送队列上只存在这个段,则
             * 调用tcp_push_one()只发送当前段.
             */
        continue;

wait_for_sndbuf:/* 设置空间不足标记 */
    /* 设置当前的状态为无空间状态,并等待内存空间。 */
        set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
        if (copied)/* 已经有拷贝数据到发送队列,则发送之 */
            tcp_push(sk, flags & ~MSG_MORE, mss_now,
                 TCP_NAGLE_PUSH, size_goal);

        err = sk_stream_wait_memory(sk, &timeo);
        if (err != 0)
            goto do_error;

        mss_now = tcp_send_mss(sk, &size_goal, flags);
    }
//拷贝完成或者空间不足从do_error走到这里
out:
    if (copied)//应用层数据拷贝完成或者发送队列中的buffer空间达到sk_sndbuf时,返回拷贝成功的字节数。
    //所以这里也说明了应用层send或者write数据的时候并不会一次write或者send完,如果数据包过大需要多次发送
        tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
out_nopush:
    release_sock(sk);/* 将 sk 释放,并返回已经发出的数据量。 */
    return copied + copied_syn;

do_fault:
    if (!skb->len) {
        /* 当拷贝数据发送异常是,会进入这个分支。如果当前的 SKB 是新分配的,
        * 那么,将该 SKB 从发送队列中去除,并释放该 SKB。*/
        tcp_unlink_write_queue(skb, sk);
        /* It is the one place in all of TCP, except connection
         * reset, where we can be unlinking the send_head.
         */
        tcp_check_send_head(sk, skb);
        sk_wmem_free_skb(sk, skb);
    }

do_error:
    if (copied + copied_syn)/* 如果已经拷贝了数据,那么,就将其发出。 */
        goto out;
out_err:
    err = sk_stream_error(sk, flags, err);/* 获取并返回错误码,释放锁。 */
    /* make sure we wake any epoll edge trigger waiter */
    if (unlikely(skb_queue_len(&sk->sk_write_queue) == 0 && err == -EAGAIN))
        sk->sk_write_space(sk);
    release_sock(sk);
    return err;
}

TCP Push 操作

  TCP 协议提供了 PUSH 功能,只要添加了该标志位, TCP 层会尽快地将数据发送出去。以往多用于传输程序的控制命令。在目前的多数 TCP 实现中,用户往往不会自行指定 PUSHTCP 的实现会根据情况自行指定 PUSH 位。 既然目前的 TCP 实现多数都会自行指定 PUSH 标志位,那么究竟在什么情况下, 数据包会被设置 PUSH 标志位呢? forced_push函数给出了必须设置 PUSH 标志位的 一种情况。

static inline bool forced_push(const struct tcp_sock *tp)
2 {
3 /* 当上一次被 PUSH 出去的包的序号和当前的序号
4 * 相差超过窗口的一半时,会强行被 PUSH。
5 */
6 return after(tp->write_seq, tp->pushed_seq + (tp->max_window >> 1));
7 }

 

  从这里可以看出,在 Linux 中,如果缓存的数据量超过了窗口大小的一半以上,就会被尽快发送出去

tcp_sendmsg_fastopen

/*
tcp_sendmsg_fastopen创建tp->fastopen_req, 请求上下文
调用__inet_stream_connect()->tcp_v4_connect()->tcp_connect()进入正常的connect逻辑, 先生成一个不带数据的syn包放入sk_write_queue重传队列
调用tcp_send_syn_data发送syn+data的TFO包
先从tcp metric中查找目标地址的fast open cookie, 如果找到也就是fo->cookie.len>0, 则会放倒tcp选项中发送; fo->cookie.len=0则会发送fastopen请求选项; <0则使用原来的方式,不带数据
在tcp_send_syn_data中,分配一个新的skb,并把用户态数据copy到这个新的skb中,最多只能发送一个MSS大小,最终会返回发送的数据量给调用者
通过tcp_transmit_skb发送这个带数据的skb, 调用tcp_syn_options和tcp_options_write设置选项
再把这个新的包含数据的skb放入重传队列sk_write_queue, 实其seq+1,并去掉syn选项, 这样重传队列中就包含了syn包和data包两个
*/
static int tcp_sendmsg_fastopen(struct sock *sk, struct msghdr *msg,
                int *copied, size_t size)
{
    struct tcp_sock *tp = tcp_sk(sk);
    int err, flags;
        /* 如果没有开启该功能,返回错误值 */
    if (!(sysctl_tcp_fastopen & TFO_CLIENT_ENABLE))
        return -EOPNOTSUPP;
    if (tp->fastopen_req)/* 如果已经有要发送的数据了,返回错误值 */
        return -EALREADY; /* Another Fast Open is in progress */

    tp->fastopen_req = kzalloc(sizeof(struct tcp_fastopen_request),
                   sk->sk_allocation);/* 分配空间并将用户数据块赋值给相应字段 */
    if (unlikely(!tp->fastopen_req))
        return -ENOBUFS;
    tp->fastopen_req->data = msg;
    tp->fastopen_req->size = size;

    flags = (msg->msg_flags & MSG_DONTWAIT) ? O_NONBLOCK : 0;
    /* 由于 fast open 时,连接还未建立,因此,这里直接调用了下面的
    * 函数建立连接。这样数据就可以在连接建立的过程中被发送出去了。
     */
    err = __inet_stream_connect(sk->sk_socket, msg->msg_name,
                    msg->msg_namelen, flags);
    *copied = tp->fastopen_req->copied;
    tcp_free_fastopen_req(tp);
    return err;
}
 

输出到 IP 层 ---->tcp_write_xmit

/* This routine writes packets to the network.  It advances the
 * send_head.  This happens as incoming acks open up the remote
 * window for us.
 *
 * LARGESEND note: !tcp_urg_mode is overkill, only frames between
 * snd_up-64k-mss .. snd_up cannot be large. However, taking into
 * account rare use of URG, this is not a big flaw.
 *
 * Send at most one packet when push_one > 0. Temporarily ignore
 * cwnd limit to force at most one packet out when push_one == 2.

 * Returns true, if no segments are in flight and we have queued segments,
 * but cannot send anything now because of SWS or another problem.
 */
static bool 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;
    bool is_cwnd_limited = false;
    u32 max_segs;
    sent_pkts = 0;

    if (!push_one) {
        /* Do MTU probing. *//* 进行 MTU 探测 */
        result = tcp_mtu_probe(sk);
        if (!result) {
            return false;
        } else if (result > 0) {
            sent_pkts = 1;
        }
    }
        /* 获取最大的段数 */
    max_segs = tcp_tso_autosize(sk, mss_now);
    while ((skb = tcp_send_head(sk))) {/* 不断循环发送队列,进行发送 */
        unsigned int limit;
/*
         * 设置有关tso的信息,包括GSO类型、GSO分段的大小等。这些
         * 信息是准备给软件TSO分段使用的。如果网络设备不支持TSO,
         * 但又使用了TSO功能,则段在提交给网络设备之前,需进行
         * 软分段,即由代码实现TSO分段。用MSS初始化skb中的gso字段,返回本skb将会被分割成几个TSO段传输
         */
        tso_segs = tcp_init_tso_segs(skb, mss_now);
        BUG_ON(!tso_segs);
 /* 
               * 检查目前是否可以发送数据,
               * 确认当前发送窗口的大小
               */
        /*
         * 检测拥塞窗口的大小,如果为0,则说明
         * 拥塞窗口已满,目前不能发送。
         */
        if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
            /* "skb_mstamp" is used as a start point for the retransmit timer */
            skb_mstamp_get(&skb->skb_mstamp);
            goto repair; /* Skip network transmission */
        }

        cwnd_quota = tcp_cwnd_test(tp, skb);
        if (!cwnd_quota) {
            if (push_one == 2)
                /* Force out a loss probe pkt. */
                cwnd_quota = 1;/* 强制发送一个包进行丢包探测。 */
            else
                break;/* 如果窗口大小为 0,则无法发送任何东西。 */
        }
/*
         * 检测当前段(包括线性区和分散聚合I/O区shinfo)是否完全处在发送窗口内,如果是
         * 则可以发送,否则目前不能发送。--->>检测发送窗口是否至少允许发送skb中的一个的段。如果不允许,结束发送过程
         */
        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
            break;

        if (tso_segs == 1) {
            /*//tso_segs为1,说明skb只有一个段,而且长度可能小于MSS,即是一个小数据包;所以需要检测nagle算法是否允许发送该skb
             * 如果无需TSO分段,则检测是否使用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个时钟
             * 滴答,或者拥塞窗口和发送窗口的最小值大于64KB
             * 或三倍的当前有效MSS,在这些情况下会立即发送,
             * 而其他情况下会延时发送,这样主要为了减少软GSO
             * 分段的次数提高性能。
             */
            if (!push_one &&
                tcp_tso_should_defer(sk, skb, &is_cwnd_limited,
                         max_segs))
                break;
        }

        limit = mss_now;/* 通过上面的拥塞窗口和发送窗口的检测后,我们知道,目前至少是可以发送一个TCP段的。
当然也有可能还可以发送更多,所以需要根据条件调整limit
根据分段对于包进行分段处理。 如果skb有多个段,需要检查到底可以发送多少数据
*/ if (tso_segs > 1 && !tcp_urg_mode(tp))
//返回的是发送窗口和拥塞窗口允许发送的最大字节数 limit
= tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle); /* skb中数据段长度>分段长度限制,则进行分段,会申请新的skb
skb的数据量超过了限定值,需要分段。这种情况只可能发生在TSO情形,因为非TSO场景,skb的长度是不可能超过MSS的。此外,这种分段完全是因为拥塞控制和流量控制算法限制了发包大小
所以才需要分割,和TSO本身没有任何关系
*/ if (skb->len > limit &&/* 如果长度超过了分段限制,那么调用 tso_fragment 进行分段。 */ unlikely(tso_fragment(sk, skb, limit, mss_now, gfp))) break; /* TCP Small Queues : * Control number of packets in qdisc/devices to two packets / or ~1 ms. * This allows for : 控制进入到 qdisc/devices 中的包的数目 * 该机制带来了以下的好处 : * - 更好的 RTT 估算和 ACK 调度 * - 更快地恢复 * - 高数据率 * - better RTT estimation and ACK scheduling * - faster recovery * - high rates * Alas, some drivers / subsystems require a fair amount * of queued bytes to ensure line rate. * One example is wifi aggregation (802.11 AMPDU) */ /* * 根据条件,可能需要对SKB中的段进行分段处理,分段的 * 段包括两种:一种是普通的用MSS分段的段,另一种则是 * TSO分段的段。能否发送段主要取决于两个条件:一是段 * 需完全在发送窗口中,二是拥塞窗口未满。第一种段, * 应该不会再分段了,因为在tcp_sendmsg()中创建段的SKB时已经 * 根据MSS处理了。而第二种段,则一般情况下都会大于MSS, * 因此通过TSO分段的段有可能大于拥塞窗口剩余空间,如果 * 是这样,就需以发送窗口和拥塞窗口的最小值作为段长对 * 数据包再次分段。 */ /* * limit为再次分段的段长,初始化为当前MSS。 */ limit = max(2 * skb->truesize, sk->sk_pacing_rate >> 10); limit = min_t(u32, limit, sysctl_tcp_limit_output_bytes); if (atomic_read(&sk->sk_wmem_alloc) > limit) { set_bit(TSQ_THROTTLED, &tp->tsq_flags); /* It is possible TX completion already happened * before we set TSQ_THROTTLED, so we must * test again the condition. */ smp_mb__after_atomic(); if (atomic_read(&sk->sk_wmem_alloc) > limit) break; } /* * 使用地址族相关的af_sepcific->queue_xmit函数, * 将数据转发到网络层。IPv4使用的是 * ip_queue_xmit */ if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) break; repair: /* Advance the send_head. This one is sent out. * This call will increment packets_out. */ /* * 处理对统计量的更新。更重要的是, * 它会初始化所发送TCP信息段的重传 * 定时器。不必对每个TCP分组都这样做, * 该机制只用于已经确认的数据区之后的 * 第一个分组 */ tcp_event_new_data_sent(sk, skb); /* * 如果发送的段小于MSS,则更新最近发送的小包的 * 最后一个字节序号。 *//* * 更新在函数中已发送的总段数。 */ tcp_minshall_update(tp, mss_now, skb); sent_pkts += tcp_skb_pcount(skb); if (push_one) break; } if (likely(sent_pkts)) {/* 如果发送了数据,那么就更新相关的统计。 */ /* * 如果本次有数据发送,则对TCP拥塞窗口进行确认, * 最后返回成功。 */ if (tcp_in_cwnd_reduction(sk)) tp->prr_out += sent_pkts; /* Send one loss probe per tail loss episode. */ if (push_one != 2) tcp_schedule_loss_probe(sk); is_cwnd_limited |= (tcp_packets_in_flight(tp) >= tp->snd_cwnd); tcp_cwnd_validate(sk, is_cwnd_limited); return false; } /* * 如果本次没有数据发送,则根据已发送但未确认的段数packets_out和 * sk_send_head返回,packets_out不为零或sk_send_head为空都被视为有数据发出, * 因此返回成功。 */ return !tp->packets_out && tcp_send_head(sk); }
/* Initialize TSO state of a skb.
 * This must be invoked the first time we consider transmitting
 * SKB onto the wire.
 */
static int tcp_init_tso_segs(struct sk_buff *skb, unsigned int mss_now)
{
    int tso_segs = tcp_skb_pcount(skb);
    //cond1: tso_segs为0表示该skb的GSO信息还没有被初始化过
        //cond2: MSS发生了变化,需要重新计算GSO信息

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

 tcp_init_tso_segs

/* Initialize TSO segments for a packet. */
static void tcp_set_skb_tso_segs(struct sk_buff *skb, unsigned int mss_now)
{
    if (skb->len <= mss_now || skb->ip_summed == CHECKSUM_NONE) {
        /* Avoid the costly divide in the normal
         * non-TSO case.
         *///如果该skb数据量不足一个MSS,或者根本就不支持GSO,那么就是一个段
        tcp_skb_pcount_set(skb, 1);
        TCP_SKB_CB(skb)->tcp_gso_size = 0;
    } else {
        //计算要切割的段数,就是skb->len除以MSS,结果向上取整
        tcp_skb_pcount_set(skb, DIV_ROUND_UP(skb->len, mss_now));
        TCP_SKB_CB(skb)->tcp_gso_size = mss_now;
    }
}

 

/* Due to TSO, an SKB can be composed of multiple actual
 * packets.  To keep these tracked properly, we use this.
 */
static inline int tcp_skb_pcount(const struct sk_buff *skb)
{
    return TCP_SKB_CB(skb)->tcp_gso_segs;
    //gso_segs记录了网卡在传输当前skb时应该将其分割成多少个包进行
}

 

/* This is valid iff skb is in write queue and tcp_skb_pcount() > 1. */
static inline int tcp_skb_mss(const struct sk_buff *skb)
{
    return TCP_SKB_CB(skb)->tcp_gso_size;
    //gso_size记录了该skb应该按照多大的段被切割,即上次的MSS
}

 tcp_cwnd_test

/* Can at least one segment of SKB be sent right now, according to the
 * congestion window rules?  If so, return how many segments are allowed.
 */
static inline unsigned int tcp_cwnd_test(const struct tcp_sock *tp,
                     const struct sk_buff *skb)
{
    u32 in_flight, cwnd, halfcwnd;

    /* Don't be strict about the congestion window for the final FIN. 
    //如果是FIN段,并且只有一个段(FIN有可能会携带很多数据),
    那么总是可以发送,不会被拥塞窗口限制*/
    if ((TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN) &&
        tcp_skb_pcount(skb) == 1)
        return 1;
        //估算当前还在网络中传输的TCP段的数目
    in_flight = tcp_packets_in_flight(tp);
    cwnd = tp->snd_cwnd;//snd_cwnd就是当前拥塞窗口的大小,以TCP段为单位
    if (in_flight >= cwnd)
        return 0;//拥塞窗口已经好耗尽,返回0表示不允许发送数据

    /* For better scheduling, ensure we have at least
     * 2 GSO packets in flight.
     */
    halfcwnd = max(cwnd >> 1, 1U);
    //比较拥塞窗口大小和飞行报文数目,余量就是拥塞控制还允许发送的段数
    //但是为了更好的调度,需要保证至少两个2GSO
    return min(halfcwnd, cwnd - in_flight);
}
/* left_out = sacked_out + lost_out 
    sacked_out:Packets, which arrived to receiver out of order and hence not ACKed. With SACK this  number is simply amount of SACKed data. 
Even without SACKs it is easy to give pretty reliable  estimate of this number, counting duplicate ACKs.

lost_out:Packets lost by network. TCP has no explicit loss notification feedback from network
        (for now). It means that this number can be only guessed. Actually, it is the heuristics to predict  lossage that distinguishes different algorithms.
        F.e. after RTO, when all the queue is considered as lost, lost_out = packets_out and in_flight = retrans_out.
*/
//该函数估算的是那些已经发送出去(初传+重传)并且已经离开
//网络的段的数目,这些段主要是SACK确认的+已经判定为丢失的段
static inline unsigned int tcp_left_out(const struct tcp_sock *tp)
{
    //sacked_out:启用SACK时,表示已经被SACK选项确认的段的数量;
    //            不启用SACK时,记录了收到的重复ACK的次数,因为重复ACK不会自动发送,一定是对端收到了数据包;
    //lost_out:记录发送后在传输过程中丢失的段的数目,因为TCP没有一种机制可以准确的知道
    //          发出去的段是否真的丢了,所以这只是一种算法上的估计值
    //无论如何,这两种段属于已经发送,但是可以确定它们在网络中已经不存在了
    return tp->sacked_out + tp->lost_out;
}
 
/* This determines how many packets are "in the network" to the best
 * of our knowledge.  In many cases it is conservative, but where
 * detailed information is available from the receiver (via SACK
 * blocks etc.) we can make more aggressive calculations.
 *
 * Use this for decisions involving congestion control, use just
 * tp->packets_out to determine if the send queue is empty or not.
 *
 * Read this equation as:
 *
 *    "Packets sent once on transmission queue" MINUS
 *    "Packets left network, but not honestly ACKed yet" PLUS
 *    "Packets fast retransmitted"
 
 packets_out is SND.NXT - SND.UNA counted in packets.
 retrans_out is number of retransmitted segments.
left_out is number of segments left network, but not ACKed yet.

 */
static inline unsigned int tcp_packets_in_flight(const struct tcp_sock *tp)
{
    //packets_out记录的是已经从发送队列发出,但是尚未被确认的段的数目(不包括重传)
    //retrans_out表示的是因为重传才发送出去,但是还没有被确认的段的数目
    //tcp_left_out():发出去了但是已经离开了网络的数据包数目
    return tp->packets_out - tcp_left_out(tp) + tp->retrans_out;
}

 tcp_snd_wnd_test

/* Does at least the first segment of SKB fit into the send window? 
判断当前发送窗口是否至少允许发送一个段,如果允许,返回1,否则返回0*/
static bool tcp_snd_wnd_test(const struct tcp_sock *tp,
                 const struct sk_buff *skb,
                 unsigned int cur_mss)
{
    u32 end_seq = TCP_SKB_CB(skb)->end_seq;

    if (skb->len > cur_mss)//如果skb中数据超过了一个段大小,则调整end_seq为一个段大小的序号
        end_seq = TCP_SKB_CB(skb)->seq + cur_mss;
        //检查一个段的末尾序号是否超过了发送窗口的右边界
    return !after(end_seq, tcp_wnd_end(tp));
}
/* Returns end sequence number of the receiver's advertised window 
//返回发送窗口的右边界*/
static inline u32 tcp_wnd_end(const struct tcp_sock *tp)
{
    //snd_una:已经发送但是还没有被确认的最小序号
    //snd_wnd:当前发送窗口大小,即接收方剩余的接收缓冲区
    return tp->snd_una + tp->snd_wnd;
}

 

tcp_mss_split_point

/* Returns the portion of skb which can be sent right away 
*/
static unsigned int tcp_mss_split_point(const struct sock *sk,
                    const struct sk_buff *skb,
                    unsigned int mss_now,
                    unsigned int max_segs,
                    int nonagle)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    u32 partial, needed, window, max_len;
        //window为发送窗口允许当前skb发送的最大字节数(可能会超过skb->len)
    window = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
    max_len = mss_now * max_segs;//目前tso 允许发送最大数据

    if (likely(max_len <= window && skb != tcp_write_queue_tail(sk)))
        return max_len;
        //needed为经过发送窗口矫正后的实际要发送的数据量
    needed = min(skb->len, window);

    if (max_len <= needed)
        return max_len;

    partial = needed % mss_now;
    /* If last segment is not a full MSS, check if Nagle rules allow us
     * to include this last segment in this skb.
     * Otherwise, we'll split the skb at last MSS boundary
     *///最终返回值是MSS的整数倍,当然单位依然是字节
    if (tcp_nagle_check(partial != 0, tp, nonagle))
        return needed - partial;

    return needed;
}

 

tso_fragment:主要作用为:如果skb中数据量过大,超过了发送窗口和拥塞窗口的限定,只允许发送skb的一部分,那么就需要将skb拆分成两段,前半段长度为len,本次可以发送,后半段保存在新分配的skb中,在发送队列sk_write_queue中将后半段插入到前半段的后面,这样可以保证数据的顺序发送。

tcp_transmit_skb  

  真正的发送操作是在tcp_transmit_skb中完成的。该函数最主要的工作是构建 TCP的首部,并将包交付给 IP 层。由于数据报需要等到 ACK 后才能释放,所以需要在发送队列中长期保留一份 SKB 的备份。

 

/* This routine actually transmits TCP packets queued in by
 * tcp_do_sendmsg().  This is used by both the initial
 * transmission and possible later retransmissions.
 * All SKB's seen here are completely headerless.  It is our
 * job to build the TCP header, and pass the packet down to
 * IP so it can do the same plus pass the packet off to the
 * device.
 *
 * We are working here with either a clone of the original
 * SKB, or a fresh unique copy made by the retransmit engine.
 */
 /*
 * 通常要发送一个TCP段都是通过tcp_transmit_skb()的。该函数会给
 * 待发送的段构造TCP首部,然后调用网络层接口到IP层,最终
 * 抵达网络设备。由于在成功发送到网络设备后会释放该
 * SKB,而TCP必须要接到对应的ACK后才能真正释放数据,因此
 * 在发送前会根据参数确定是克隆还是复制一份SKB用于发送。
 */ //最终的tcp发送都会调用这个  clone_it表示发送发送队列的第一个SKB的时候,采用克隆skb还是直接使用skb,如果是发送应用层数据则使用克隆的,等待对方应答ack回来才把数据删除。如果是会送ack信息,则无需克隆
//如果不支持TSO或者GSO这里的SKB->len为mss,否则如果支持TSO并且有数据再shinfo中,则这里的SKB长度为shinfo或者拥塞窗口的最小值
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 tcp_md5sig_key *md5;
    struct tcphdr *th;
    int err;

    BUG_ON(!skb || !tcp_skb_pcount(skb));

    if (clone_it) {//根据参数clone_it确定是否克隆待发送的数据包。
        skb_mstamp_get(&skb->skb_mstamp);
        /* 这里收到的 SKB 可能是原始 SKB 的克隆,也可能是
         * 来自重传引擎的一份拷贝。
         */
        if (unlikely(skb_cloned(skb)))
            skb = pskb_copy(skb, gfp_mask);
        else
            skb = skb_clone(skb, gfp_mask);
        if (unlikely(!skb))
            return -ENOBUFS;
    }

    /*首先判断该 TCP 包是否是一个 SYN 包。如果是,则
调用tcp_syn_options构建相应的选项。否则,调用 tcp_established_options来构建
相应的选项。注意,这里仅仅是计算出来具体的选项及其大小,并没有形成最终 TCP 包中选项的格式。
     * 获取INET层和TCP层的传输控制块、SKB中的TCP私有控制块
     * 以及当前TCP首部长度。
     */
    inet = inet_sk(sk);
    tp = tcp_sk(sk);
    tcb = TCP_SKB_CB(skb);
    memset(&opts, 0, sizeof(opts));
 /*
     * 判断当前TCP段是不是SYN段,因为有些选项只能出现在SYN段中,需作
     * 特别处理。
     */
    if (unlikely(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);

    /* if no packet is in qdisc/device queue, then allow XPS to select
     * another queue. We can be called from tcp_tsq_handler()
     * which holds one reference to sk_wmem_alloc.
     *
     * TODO: Ideally, in-flight pure ACK packets should not matter here.
     * One way to get this would be to set skb->truesize = 2 on them.
     根据选项的大小,可以进一步推算出 TCP 头部的大小。之后,调用相关函数在 skb 头
部为 TCP 头部流出空间。
     */
    skb->ooo_okay = sk_wmem_alloc_get(sk) < SKB_TRUESIZE(1);
/*
     * 到此为止,SKB已添加到发送队列中。但从SKB角度去看
     * 还不知道它属于哪个传输控制块,因此此时需调用
     * skb_set_owner_w()设置该SKB的宿主。
     */
     /*在sk_alloc的时候初始化设置为1
    ,然后在skb_set_owner_w加上SKB长度,当SKB发送出去后
    ,在减去该SKB的长度,所以这个值当数据发送后其值始终是1
    ,不会执行sock_wfree
    */
    skb_push(skb, tcp_header_size);
    skb_reset_transport_header(skb);

    skb_orphan(skb);
    skb->sk = sk;
    skb->destructor = skb_is_tcp_pure_ack(skb) ? __sock_wfree : tcp_wfree;
    skb_set_hash_from_sk(skb, sk);
    atomic_add(skb->truesize, &sk->sk_wmem_alloc);

    /* Build TCP header and checksum it. 构建 TCP 头部并计算校验和*/
    th = (struct tcphdr *)skb->data;
    th->source        = inet->inet_sport;
    th->dest        = inet->inet_dport;
    th->seq            = htonl(tcb->seq);
    th->ack_seq        = htonl(tp->rcv_nxt);
    *(((__be16 *)th) + 6)    = htons(((tcp_header_size >> 2) << 12) |
                    tcb->tcp_flags);

    th->check        = 0;
    th->urg_ptr        = 0;
 /*
     * 判断是否需要设置紧急指针和带外数据标志。判断条件有两个,
     * 一是发送时是否设置了紧急方式,二是紧急指针是否在以该报文
     * 数据序号为起始的65535范围之内,其中第二个条件主要是判断紧急
     * 指针的合法性。
     */
    /* The urg_mode check is necessary during a below snd_una win probe */
    if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {
        if (before(tp->snd_up, tcb->seq + 0x10000)) {
            th->urg_ptr = htons(tp->snd_up - tcb->seq);
            th->urg = 1;
        } else if (after(tcb->seq + 0xFFFF, tp->snd_nxt)) {
            th->urg_ptr = htons(0xFFFF);
            th->urg = 1;
        }
    }
    /*在写完首部后,开始构建TCP首部选项。
     */
    tcp_options_write((__be32 *)(th + 1), tp, &opts);
    skb_shinfo(skb)->gso_type = sk->sk_gso_type;
    if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
        th->window      = htons(tcp_select_window(sk));
        tcp_ecn_send(sk, skb, th, tcp_header_size);
    } else {
        /* RFC1323: The window in SYN & SYN/ACK segments
         * is never scaled.
         */
        th->window    = htons(min(tp->rcv_wnd, 65535U));
    }
#ifdef CONFIG_TCP_MD5SIG
    /* Calculate the MD5 hash, as we have all we need now */
    if (md5) {
        sk_nocaps_add(sk, NETIF_F_GSO_MASK);
        tp->af_specific->calc_md5_hash(opts.hash_location,
                           md5, sk, skb);
    }
#endif
  /*
     * 调用IPv4执行校验和接口send_check计算校验和,并设置到TCP首部中。
     * 在TCP中send_check接口被初始化为tcp_v4_send_check。
     */
    icsk->icsk_af_ops->send_check(sk, skb);

  /* 触发相关的 TCP 事件,这些会被用于拥塞控制算法。 */

  /*
     * 如果发送出去的段有ACK标志,则需要通知延时确认模块,递减
     * 快速发送ACK段的数量,同时停止延时确认定时器。
     */
    if (likely(tcb->tcp_flags & TCPHDR_ACK))
        tcp_event_ack_sent(sk, tcp_skb_pcount(skb));
  /*
     * 如果发送出去的TCP段有负载,则检测拥塞窗口闲置是否超时,
     * 并使其失效。同时记录发送TCP的时间,根据最近接受段的时间
     * 确定本端延时确认是否进入pingpong模式。
     */
    if (skb->len != tcp_header_size) {
        tcp_event_data_sent(tp, sk);
        tp->data_segs_out += tcp_skb_pcount(skb);
    }

    if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
        TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
                  tcp_skb_pcount(skb));

    tp->segs_out += tcp_skb_pcount(skb);
    /* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */
    skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);
    skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);

    /* Our usage of tstamp should remain private */
    skb->tstamp.tv64 = 0;

    /* Cleanup our debris for IP stacks */
    memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),
                   sizeof(struct inet6_skb_parm)));

    /* 在这里,我们将包加入到 IP 层的发送队列 
     * 调用发送接口queue_xmit发送报文,如果失败则返回
     * 错误码。在TCP中该接口实现函数为ip_queue_xmit()。
     */
    err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);//ip_queue_xmit
    if (likely(err <= 0))
        return err;

    /*
     * 当发送失败时,类似接收到显式拥塞通知,使拥塞
     * 控制进入CWR状态。最后,根据错误信息,返回发送
     * 是否成功。
     ----->* 如果发送了丢包(如被主动队列管理丢弃),那么进入到拥塞控制状态。 */
    tcp_enter_cwr(sk);

    return net_xmit_eval(err);
}

 

 

窗口的选择

/* Chose a new window to advertise, update state in tcp_sock for the
 * socket, and return result with RFC1323 scaling applied.  The return
 * value can be stuffed directly into th->window for an outgoing
 * frame.这个函数的作用是选择一个新的窗口大小以用于更新tcp_sock。返回的结果根据
RFC1323进行了缩放
 */
static u16 tcp_select_window(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    u32 old_win = tp->rcv_wnd;
    u32 cur_win = tcp_receive_window(tp);
    u32 new_win = __tcp_select_window(sk);
    /* old_win 是接收方窗口的大小。
     * cur_win 当前的接收窗口剩余  剩余 剩余 大小。
      * new_win 是新选择出来的窗口大小。根据剩余缓存计算 新的接收窗口大小
      */
        /* 当新窗口的大小小于当前窗口的大小时,不能缩减窗口大小。
   * 这是 IEEE 强烈不建议的一种行为。
   */
    /* Never shrink the offered window */
    if (new_win < cur_win) {
        /* Danger Will Robinson!
         * Don't update rcv_wup/rcv_wnd here or else
         * we will not be able to advertise a zero
         * window in time.  --DaveM
         *
         * Relax Will Robinson.
         */
        if (new_win == 0)
            NET_INC_STATS(sock_net(sk),
                      LINUX_MIB_TCPWANTZEROWINDOWADV);
        new_win = ALIGN(cur_win, 1 << tp->rx_opt.rcv_wscale);
    }
    tp->rcv_wnd = new_win;/* 将当前的接收窗口设置为新的窗口大小。 */
    tp->rcv_wup = tp->rcv_nxt;

    /* Make sure we do not exceed the maximum possible
     * scaled window.
     *//* 判断当前窗口未越界。 */
    if (!tp->rx_opt.rcv_wscale && sysctl_tcp_workaround_signed_windows)
        new_win = min(new_win, MAX_TCP_WINDOW);
    else
        new_win = min(new_win, (65535U << tp->rx_opt.rcv_wscale));

    /* RFC1323 scaling applied *//* RFC1323 缩放窗口大小。这里之所以是右移,是因为此时的 new_win 是
    * 窗口的真正大小。所以返回时需要返回正常的可以放在 16 位整型中的窗口大小。
    * 所以需要右移。*/
    new_win >>= tp->rx_opt.rcv_wscale;

    /* If we advertise zero window, disable fast path. */
    if (new_win == 0) {
        tp->pred_flags = 0;
        if (old_win)
            NET_INC_STATS(sock_net(sk),
                      LINUX_MIB_TCPTOZEROWINDOWADV);
    } else if (old_win == 0) {
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFROMZEROWINDOWADV);
    }

    return new_win;
}

 每次发送一个TCP数据段,都要构建TCP首部,这时会调用tcp_select_window选择接收窗

窗口大小选择的基本算法:

1. 计算当前接收窗口的剩余大小cur_win。

2. 计算新的接收窗口大小new_win,这个值为剩余接收缓存的3/4,且不能超过rcv_ssthresh。

3. 取cur_win和new_win中值较大者作为接收窗口大小

/* Compute the actual receive window we are currently advertising.
 * Rcv_nxt can be after the window if our peer push more data
 * than the offered window.
 */
static inline u32 tcp_receive_window(const struct tcp_sock *tp)
{
    s32 win = tp->rcv_wup + tp->rcv_wnd - tp->rcv_nxt;

    if (win < 0)
        win = 0;
    return (u32) win;
}

 

This is calculated as the last advertised window minus unacknowledged data length:

tp->rcv_wnd - (tp->rcv_nxt - tp->rcv_wup)

tp->rcv_wup is synced with next byte to be received (tp->rcv_nxt) only when we are sending ACK in

tcp_select_window(). If there is no unacknowledged bytes, the routine returns the exact receive

window advertised last.

/* This function returns the amount that we can raise the
 * usable window based on the following constraints
 *
 * 1. The window can never be shrunk once it is offered (RFC 793)
 * 2. We limit memory per socket
 *
 * RFC 1122:
 * "the suggested [SWS] avoidance algorithm for the receiver is to keep
 *  RECV.NEXT + RCV.WIN fixed until:
 *  RCV.BUFF - RCV.USER - RCV.WINDOW >= min(1/2 RCV.BUFF, MSS)"
 *推荐的用于接收方的糊涂窗口综合症的避免算法是保持 recv.next+rcv.win
不变,直到:RCV.BUFF - RCV.USER - RCV.WINDOW >= min(1/2 RCV.BUFF,
MSS)
换句话说,就是除非缓存的大小多出来至少一个 MSS 那么多字节,否则不要增长
窗口右边界的大小。
 * i.e. don't raise the right edge of the window until you can raise
 * it at least MSS bytes.
 *
 * Unfortunately, the recommended algorithm breaks header prediction,
 * since header prediction assumes th->window stays fixed.
 *
 * Strictly speaking, keeping th->window fixed violates the receiver
 * side SWS prevention criteria. The problem is that under this rule
 * a stream of single byte packets will cause the right side of the
 * window to always advance by a single byte.
 *
 * Of course, if the sender implements sender side SWS prevention
 * then this will not be a problem.
 *
 * BSD seems to make the following compromise:
 *
 *    If the free space is less than the 1/4 of the maximum
 *    space available and the free space is less than 1/2 mss,
 *    then set the window to 0.
 *    [ Actually, bsd uses MSS and 1/4 of maximal _window_ ]
 *    Otherwise, just prevent the window from shrinking
 *    and from being larger than the largest representable value.
 *
 * This prevents incremental opening of the window in the regime
 * where TCP is limited by the speed of the reader side taking
 * data out of the TCP receive queue. It does nothing about
 * those cases where the window is constrained on the sender side
 * because the pipeline is full.
 *
 * BSD also seems to "accidentally" limit itself to windows that are a
 * multiple of MSS, at least until the free space gets quite small.
 * This would appear to be a side effect of the mbuf implementation.
 * Combining these two algorithms results in the observed behavior
 * of having a fixed window size at almost all times.
 *
 * Below we obtain similar behavior by forcing the offered window to
 * a multiple of the mss when it is feasible to do so.
 *
 * Note, we don't "adjust" for TIMESTAMP or SACK option bytes.
 * Regular options like TIMESTAMP are taken into account.
 然而,根据 Linux 注释中的说法,被推荐的这个算法会破坏头预测 (header prediction),
 因为头预测会假定th->window不变。严格地说,保持th->window固定不变会违背
接收方的用于防止糊涂窗口综合症的准则。在这种规则下,一个单字节的包的流会引发
窗口的右边界总是提前一个字节。当然,如果发送方实现了预防糊涂窗口综合症的方法,
那么就不会出现问题。
Linux 的 TCP 部分的作者们参考了 BSD 的实现方法。 BSD 在这方面的做法是是,
如果空闲空间小于最大可用空间的 1
4,且空闲空间小于 mss 的 1 2,那么就把窗口设置为
0。否则,只是单纯地阻止窗口缩小,或者阻止窗口大于最大可表示的范围 (the largest
representable value)。 BSD 的方法似乎“意外地”使得窗口基本上都是 MSS 的整倍数。
且很多情况下窗口大小都是固定不变的。因此, Linux 采用强制窗口为 MSS 的整倍数,
以获得相似的行为
 */
u32 __tcp_select_window(struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    /* MSS for the peer's data.  Previous versions used mss_clamp
     * here.  I don't know if the value based on our guesses
     * of peer's MSS is better for the performance.  It's more correct
     * but may be worse for the performance because of rcv_mss
     * fluctuations.  --SAW  1998/11/1
     */
    int mss = icsk->icsk_ack.rcv_mss;
    int free_space = tcp_space(sk);
    int allowed_space = tcp_full_space(sk);
    int full_space = min_t(int, tp->window_clamp, allowed_space);
    int window;

    if (mss > full_space)
        mss = full_space;/* 如果 mss 超过了总共的空间大小,那么把 mss 限制在允许的空间范围内。 */

    if (free_space < (full_space >> 1)) {
        icsk->icsk_ack.quick = 0;/* 当空闲空间小于允许空间的一半时。 */

        if (tcp_under_memory_pressure(sk))
            tp->rcv_ssthresh = min(tp->rcv_ssthresh,
                           4U * tp->advmss);

        /* free_space might become our new window, make sure we don't
         * increase it due to wscale.
         *//* free_space 有可能成为新的窗口的大小,因此,需要考虑
    * 窗口扩展的影响。 */
        free_space = round_down(free_space, 1 << tp->rx_opt.rcv_wscale);

        /* if free space is less than mss estimate, or is below 1/16th
         * of the maximum allowed, try to move to zero-window, else
         * tcp_clamp_window() will grow rcv buf up to tcp_rmem[2], and
         * new incoming data is dropped due to memory limits.
         * With large window, mss test triggers way too late in order
         * to announce zero window in time before rmem limit kicks in.
         *//* 如果空闲空间小于 mss 的大小,或者低于最大允许空间的的 1/16,那么,
      * 返回 0 窗口。否则, tcp_clamp_window() 会增长接收缓存到 tcp_rmem[2]。
         * 新进入的数据会由于内醋限制而被丢弃。对于较大的窗口,单纯地探测 mss 的
         * 大小以宣告 0 窗口有些太晚了(可能会超过限制)。
        */
        if (free_space < (allowed_space >> 4) || free_space < mss)
            return 0;
    }

    if (free_space > tp->rcv_ssthresh)
        free_space = tp->rcv_ssthresh;

    /* Don't do rounding if we are using window scaling, since the
     * scaled window will not line up with the MSS boundary anyway.
     *//* 这里处理一个例外情况,就是如果开启了窗口缩放,那么就没法对齐 mss 了。
      * 所以就保持窗口是对齐 2 的幂的。 */
    window = tp->rcv_wnd;
    if (tp->rx_opt.rcv_wscale) {
        window = free_space;

        /* Advertise enough space so that it won't get scaled away.
         * Import case: prevent zero window announcement if
         * 1<<rcv_wscale > mss.
         */
        if (((window >> tp->rx_opt.rcv_wscale) << tp->rx_opt.rcv_wscale) != window)
            window = (((window >> tp->rx_opt.rcv_wscale) + 1)
                  << tp->rx_opt.rcv_wscale);
    } else {
        /* Get the largest window that is a nice multiple of mss.
         * Window clamp already applied above.
         * If our current window offering is within 1 mss of the
         * free space we just keep it. This prevents the divide
         * and multiply from happening most of the time.
         * We also don't do any window rounding when the free space
         * is too small.
         *//* 如果内存条件允许,那么就把窗口设置为 mss 的整倍数。 * 或者如果 free_space > 当前窗口大小加上全部允许的空间的一半,
         * 那么,就将窗口大小设置为 free_space */
        if (window <= free_space - mss || window > free_space)
            window = (free_space / mss) * mss;
        else if (mss == full_space &&
             free_space > window + (full_space >> 1))
            window = free_space;
    }

    return window;
}

 关于接收窗口 这些东西 太啰嗦了 就不看了,用到时再看

posted @ 2022-04-21 23:46  codestacklinuxer  阅读(107)  评论(0编辑  收藏  举报