linux tcp Nagle算法,TCP_NODELAY和TCP_CORK 转载 糊涂窗口综合症(Silly Windw Syndrome) Nagle和Cork setsockopt 数据发送 tcp_should_autocork tcp_write_xmit tcp_nagle_test tcp_tso_should_defer 应用程序Tips

linux tcp Nagle算法,TCP_NODELAY和TCP_CORK  转载
糊涂窗口综合症(Silly Windw Syndrome)
Nagle和Cork
setsockopt
数据发送
tcp_should_autocork
tcp_write_xmit
tcp_nagle_test
tcp_tso_should_defer
应用程序Tips

转载自:

http://www.cnhalo.net/2016/08/13/linux-tcp-nagle-cork/

http://abcdxyzk.github.io/blog/2018/07/08/kernel-nodelay_cork/

  • 发送方: 应用程序产生数据的速度很慢
    发送1字节需要40B(TCP头和IP头), 发送大量的小包会造成网络拥塞,发送窗口抖动,网络利用率低等特性。
    当年OTT(over the top)类应用(如微信), 由于3G/4G没有大规模普及,因为常用的心跳机制,通常发送小的心跳包,造成了信令风暴,影响了运营商网络的稳定。
    解决: nagle和cork算法,尝试延迟发送,积累成大包后再发送。当然交互类应用需要实时性,不能推迟发送。

  • 接收方: 应用程序消耗数据的速度很慢
    接收窗口满了,发送rwnd=0, 再消耗一字节,rwnd=1,消耗并发送反复的情况。 发送方nagle因为推迟发送,可能忽略这部分通告
    解决:

    • clark方法:只要数据到达就发送ACK,但在缓存中有足够大的空间放入最大长度的报文之前,都宣布rwnd=0
    • 推迟确认:优点:减少ACK数量。缺点:可能导致重传

Nagle和Cork

  • Nagle算法的目的:避免发送大量的小包,网络上每次只能一个小包存在,在小包被确认之前,只能积累发送大包,如果包长度达到MSS,则允许发送;如果该包含有FIN,则允许发送;但发生了超时(一般为200ms),则立即发送, 启动TCP_NODELAY,就意味着禁用了Nagle算法
  • Cork算法的目的: CORK就是塞子的意思,形象地理解就是用CORK将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。 cork是完全避免小包的发送,只发送MSS大小的包及不得不发的小包

setsockopt

TCP_CORK的开关,只会影响TCP_NAGLE_CORK选项,当nagle测试关闭(通过TCP_NODELAY设置了TCP_NAGLE_OFF)的情况下,才会设置TCP_NAGLE_PUSH
而TCP_NODELAY则通过设置TCP_NAGLE_OFF来开关nagle。
TCP_NAGLE_PUSH是个一次性的选项值,每次创建新的skb并放入发送队列的时候,TCP_NAGLE_PUSH都会被清除(skb_entail函数)

#define TCP_NAGLE_OFF        1    /* Nagle's algo is disabled */
#define TCP_NAGLE_CORK        2    /* Socket is corked        */
#define TCP_NAGLE_PUSH        4    /* Cork is overridden for already queued data */
case TCP_CORK:
        /* When set indicates to always queue non-full frames.
         * Later the user clears this option and we transmit
         * any pending partial frames in the queue.  This is
         * meant to be used alongside sendfile() to get properly
         * filled frames when the user (for example) must write
         * out headers with a write() call first and then use
         * sendfile to send out the data parts.
         *
         * TCP_CORK can be set together with TCP_NODELAY and it is
         * stronger than TCP_NODELAY.
         */
        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;
case TCP_NODELAY:
        if (val) {
            /* TCP_NODELAY is weaker than TCP_CORK, so that
             * this option on corked socket is remembered, but
             * it is not activated until cork is cleared.
             *
             * However, when TCP_NODELAY is set we make
             * an explicit push, which overrides even TCP_CORK
             * for currently queued segments.
             */
            tp->nonagle |= TCP_NAGLE_OFF|TCP_NAGLE_PUSH;
            tcp_push_pending_frames(sk);
        } else {
            tp->nonagle &= ~TCP_NAGLE_OFF;
        }

数据发送

tcp_sendmsg在这里我们忽略很多细节,只需要知道根据GSO的大小来copy到skb中,按照合适的时机push各个skb, copy所有数据后(或者内存不足),则调用tcp_push执行发送

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
    //size_goal表示GSO支持的大小,为mss_now的整数倍,不支持GSO时则相等
    mss_now = tcp_send_mss(sk, &size_goal, flags);
    // 把msg的用户态数据,按照GSO支持的最大大小,尽量copy到一个skb中
    //skb_entail(sk,skb)到发送队列
    //还有数据没copy,但是当前skb已经满了,可以发送了
    if (forced_push(tp)) {    //超过最大窗口的一半没有设置push了
        tcp_mark_push(tp, skb);    //设置push标记,更新pushed_seq
        __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);    //调用tcp_write_xmit马上发送
    } else if (skb == tcp_send_head(sk))    //第一个包,直接发送
        tcp_push_one(sk, mss_now);
    else{
        //说明发送队列前面还有skb等待发送,且距离之前push的包还不是非常久, 则只是继续放到队列中,继续开始创建下一个skb copy
        continue
    }
out:    
    //最后的包调用tcp_push发送
    tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
    ...
}
static void skb_entail(struct sock *sk, struct sk_buff *skb)
{
    ...
    tcp_add_write_queue_tail(sk, skb);
    
    if (tp->nonagle & TCP_NAGLE_PUSH)
        tp->nonagle &= ~TCP_NAGLE_PUSH;    //创建新的skb放入发送队列,立刻清楚push选项
}
static 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;
    if (!tcp_send_head(sk))
        return;
    skb = tcp_write_queue_tail(sk);
    if (!(flags & MSG_MORE) || forced_push(tp))
        tcp_mark_push(tp, skb);
    tcp_mark_urg(tp, flags);
    if (tcp_should_autocork(sk, skb, size_goal)) {
        //利用tsq机制延后发送
        /* avoid atomic op if TSQ_THROTTLED bit is already set */
        if (!test_bit(TSQ_THROTTLED, &tp->tsq_flags)) {
            NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
            set_bit(TSQ_THROTTLED, &tp->tsq_flags);
        }
        /* It is possible TX completion already happened
         * before we set TSQ_THROTTLED.
         */
        if (atomic_read(&sk->sk_wmem_alloc) > skb->truesize)
            return;
    }
    if (flags & MSG_MORE)    //应用程序标记了很快有新的数据到来,则标记cork,不发送小包
        nonagle = TCP_NAGLE_CORK;
    __tcp_push_pending_frames(sk, mss_now, nonagle);    //最终调用tcp_write_xmit
}

tcp_should_autocork

net.ipv4.tcp_autocorking = 1 默认开启
当tcp_autocorking开启后,如果当前skb还没有达到GSO最大值,并且前面还有数据等待发送,也就是不急着发,
返回true后, 利用tsq机制,在网卡发送完成一个包并释放该skb的时候,设置tasklet,在下一个softirq中再次尝试发送

/* If a not yet filled skb is pushed, do not send it if
 * we have data packets in Qdisc or NIC queues :
 * Because TX completion will happen shortly, it gives a chance
 * to coalesce future sendmsg() payload into this skb, without
 * need for a timer, and with no latency trade off.
 * As packets containing data payload have a bigger truesize
 * than pure acks (dataless) packets, the last checks prevent
 * autocorking if we only have an ACK in Qdisc/NIC queues,
 * or if TX completion was delayed after we processed ACK packet.
 */
static bool tcp_should_autocork(struct sock *sk, struct sk_buff *skb,
                int size_goal)
{
    return skb->len < size_goal &&    //不到最大GSO size
           sysctl_tcp_autocorking &&    //默认开启
           skb != tcp_write_queue_head(sk) &&    //发送队列前面还有其他skb
           atomic_read(&sk->sk_wmem_alloc) > skb->truesize;    //qdisc中有数据, 说明网卡发送后完成中断释放内存,会很快有新的数据到来
}

tcp_write_xmit

tcp_push/tcp_push_one/__tcp_push_pending_frames最终都调用tcp_write_xmit()
执行到tcp_write_xmit说明已经尽最大可能在当前send()系统调用中作GSO,
在tcp_write_xmit()中,则使用nagle来判断是否要等待下一个应用程序传递更多的数据再发送
如果决定发送则调用tcp_transmit_skb()执行最终的发送

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
               int push_one, gfp_t gfp)
{
    
    max_segs = tcp_tso_segs(sk, mss_now);    //当前tso支持的最大segs数量
    while ((skb = tcp_send_head(sk))) {    //遍历发送队列
        tso_segs = tcp_init_tso_segs(skb, mss_now);    //skb->len/mss,重新设置tcp_gso_segs,因为在tcp_sendmsg中被清零了    
        ...
        if (tso_segs == 1) {//tso_segs=1表示无需tso分段
            /* 根据nagle算法,计算是否需要推迟发送数据 */
            if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
                             (tcp_skb_is_last(sk, skb) ?
                              nonagle : TCP_NAGLE_PUSH))))    //last skb就直接发送
                break;    //推迟发送
        } else {    //tso分段
            if (!push_one &&    //不只一个skb
                tcp_tso_should_defer(sk, skb, &is_cwnd_limited, //如果发送窗口剩余不多,并且预计下一个ack将很快到来(意味着可用窗口会增加),则推迟发送
                         max_segs))
                break;    //可以推迟
        }
        //不用推迟发送,马上发送
        limit = mss_now;
        ...
        if (tcp_small_queue_check(sk, skb, 0))    //tsq检查,qdisc是否达到限制
            break;
        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))    //发送,如果包被qdisc丢了,则退出循环,不继续发送了
            break;
        tcp_event_new_data_sent(sk, skb);//更新sk_send_head和packets_out
        /* 更新struct tcp_sock中的snd_sml字段。记录非全尺寸发送的最后一个字节序号,主要用来做nagle测试
         */
        tcp_minshall_update(tp, mss_now, skb);
        sent_pkts += tcp_skb_pcount(skb);
        if (push_one)    //只发一个skb的则退出循环
            break;
    }
    ...
    //没有数据包inflight,并且有数据等待发送,则准备尝试0窗口探测
    return !tp->packets_out && tcp_send_head(sk);
}

tcp_nagle_test

在GSO没有开启,或者在当前send()中的数据不够一个mss的时候,则会调用tcp_nagle_test,来判断是否推迟发送.
以下情况将直接发送

  • 设置了TCP_NAGLE_PUSH。 比如应用程序设置了TCP_NODELAY选项;或是当前包是在发送队列中的最后一个;或者当前SKB达到GSO的最大值了,并超过最大窗口的一半没有设置push了
  • 紧急数据或者fin包
  • 当前包达到了MSS大小
  • 没有设置TCP_NAGLE_CORK,并且上一个发送的小包已经被确认

也就是说对于设置了CORK的小包就不发;或者没设置CORK但是上一个发送的小包还未被确认都延迟发送

/* Return true if the Nagle test allows this packet to be
 * sent now.
 */
static inline bool tcp_nagle_test(const struct tcp_sock *tp, const struct sk_buff *skb,
                  unsigned int cur_mss, int nonagle)
{
    /* Nagle rule does not apply to frames, which sit in the middle of the
     * write_queue (they have no chances to get new data).
     *
     * This is implemented in the callers, where they modify the 'nonagle'
     * argument based upon the location of SKB in the send queue.
     */
    if (nonagle & TCP_NAGLE_PUSH)
        return true;
    /* Don't use the nagle rule for urgent data (or for the final FIN). */
    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;
    //skb->len < cur_mss且设置了TCP_NAGLE_CORK, 或者上一个发送的小包还未被确认, 则推迟发送
    return false;
}
static bool tcp_nagle_check(bool partial, const struct tcp_sock *tp,
                int nonagle)
{
    return partial &&    //skb->len < mss, 也就是说>=mss就直接发送
        ((nonagle & TCP_NAGLE_CORK) ||    //设置了cork则使用nagle
         (!nonagle && tp->packets_out && tcp_minshall_check(tp)));    //有inflight数据且上一个发送的小包还没被确认则进入nagle
}
/* Minshall's variant of the Nagle send check. */
static bool tcp_minshall_check(const struct tcp_sock *tp)
{
    return after(tp->snd_sml, tp->snd_una) &&    //上一个发送的小包还没确认
        !after(tp->snd_sml, tp->snd_nxt);    //没有回绕
}
static void tcp_minshall_update(struct tcp_sock *tp, unsigned int mss_now,
                const struct sk_buff *skb)
{
    if (skb->len < tcp_skb_pcount(skb) * mss_now)
        tp->snd_sml = TCP_SKB_CB(skb)->end_seq;
}

tcp_tso_should_defer

对于开启了GSO的情况,并且当前skb不只一个分段,则需要tcp_tso_should_defer来判断是否延迟发送
在剩余发送窗口不足且下一个ack可能很快到来的情况下,则推迟发送

static bool tcp_tso_should_defer(struct sock *sk, struct sk_buff *skb,
                 bool *is_cwnd_limited, u32 max_segs)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    u32 age, send_win, cong_win, limit, in_flight;
    struct tcp_sock *tp = tcp_sk(sk);
    struct skb_mstamp now;
    struct sk_buff *head;
    int win_divisor;
    if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
        goto send_now;
    if (icsk->icsk_ca_state >= TCP_CA_Recovery)
        goto send_now;
    /* Avoid bursty behavior by allowing defer
     * only if the last write was recent.
     */
    if ((s32)(tcp_time_stamp - tp->lsndtime) > 0)
        goto send_now;
    in_flight = tcp_packets_in_flight(tp);
    BUG_ON(tcp_skb_pcount(skb) <= 1 || (tp->snd_cwnd <= in_flight));
    send_win = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;    //发送窗口
    /* From in_flight test above, we know that cwnd > in_flight.  */
    cong_win = (tp->snd_cwnd - in_flight) * tp->mss_cache;    //拥塞窗口
    limit = min(send_win, cong_win);    //最大发送窗口剩余
    /* If a full-sized TSO skb can be sent, do it. */
    if (limit >= max_segs * tp->mss_cache)    //支持最大尺寸的tso发送
        goto send_now;
    /* Middle in queue won't get any more data, full sendable already? */
    if ((skb != tcp_write_queue_tail(sk)) && (limit >= skb->len))    //不是发送队列的最后一个,且满足发送窗口
        goto send_now;    //直接发送,不会有数据被添加到这个skb了
    win_divisor = ACCESS_ONCE(sysctl_tcp_tso_win_divisor);
    if (win_divisor) {
        u32 chunk = min(tp->snd_wnd, tp->snd_cwnd * tp->mss_cache);
        /* If at least some fraction of a window is available,
         * just use it.
         */
        chunk /= win_divisor;
        if (limit >= chunk)    //剩余的窗口大于总窗口的比例, 默认1/3
            goto send_now;
    } else {
        /* Different approach, try not to defer past a single
         * ACK.  Receiver should ACK every other full sized
         * frame, so if we have space for more than 3 frames
         * then send now.
         */
        if (limit > tcp_max_tso_deferred_mss(tp) * tp->mss_cache)
            goto send_now;
    }
    head = tcp_write_queue_head(sk);
    skb_mstamp_get(&now);
    age = skb_mstamp_us_delta(&now, &head->skb_mstamp);    //最早的未确认包的距离现在的时间
    /* If next ACK is likely to come too late (half srtt), do not defer */
    if (age < (tp->srtt_us >> 4))    // 也就是说下一个ack的到来很可能大于1/2的srtt,直接发送
        goto send_now;
    /* Ok, it looks like it is advisable to defer. */
    //当前skb的收到cwnd限制
    if (cong_win < send_win && cong_win <= skb->len)
        *is_cwnd_limited = true;
    //可以推迟发送了
    return true;
send_now:
    return false;
}

应用程序Tips

    • http服务器的response,要发送http头+sendfile()文件,
      可以先设置TCP_CORK, 然后write() http header, 不让header发出去,
      调用sendfile(), 这时候如果没有达到GSO大小,还是不会发出去
      最后设置TCP_NODELAY,这时候设置了TCP_NAGLE_PUSH, 会马上发出去。 如果你只是取消TCP_CORK, 内核还是会继续判断是否需要nagle。

    • send()的flag参数设置为MSG_MORE, 给内核hint,表示马上会有其他数据到来,内核会自动加上CORK标记,你就不需要多调用一次setsockopt系统调用. 但是设置MSG_EOR并不会马上push数据

    • 启动TCP_NODELAY,就意味着禁用了Nagle算法  http server 一般禁用
       
       
https://www.cnblogs.com/wanpengcoder/p/5366156.html

1. Nagle算法:

是为了减少广域网的小分组数目,从而减小网络拥塞的出现;

该算法要求一个tcp连接上最多只能有一个未被确认的未完成的小分组,在该分组ack到达之前不能发送其他的小分组,tcp需要收集这些少量的分组,并在ack到来时以一个分组的方式发送出去;其中小分组的定义是小于MSS的任何分组;

该算法的优越之处在于它是自适应的,确认到达的越快,数据也就发哦送的越快;而在希望减少微小分组数目的低速广域网上,则会发送更少的分组;

2. 延迟ACK:

如果tcp对每个数据包都发送一个ack确认,那么只是一个单独的数据包为了发送一个ack代价比较高,所以tcp会延迟一段时间,如果这段时间内有数据发送到对端,则捎带发送ack,如果在延迟ack定时器触发时候,发现ack尚未发送,则立即单独发送;

延迟ACK好处:

(1) 避免糊涂窗口综合症;

(2) 发送数据的时候将ack捎带发送,不必单独发送ack;

(3) 如果延迟时间内有多个数据段到达,那么允许协议栈发送一个ack确认多个报文段;

3. 当Nagle遇上延迟ACK:

试想如下典型操作,写-写-读,即通过多个写小片数据向对端发送单个逻辑的操作,两次写数据长度小于MSS,当第一次写数据到达对端后,对端延迟ack,不发送ack,而本端因为要发送的数据长度小于MSS,所以nagle算法起作用,数据并不会立即发送,而是等待对端发送的第一次数据确认ack;这样的情况下,需要等待对端超时发送ack,然后本段才能发送第二次写的数据,从而造成延迟;

4. 关闭Nagle算法:

使用TCP套接字选项TCP_NODELAY可以关闭套接字选项;

如下场景考虑关闭Nagle算法:

(1) 对端不向本端发送数据,并且对延时比较敏感的操作;这种操作没法捎带ack;

(2) 如上写-写-读操作;对于此种情况,优先使用其他方式,而不是关闭Nagle算法:

--使用writev,而不是两次调用write,单个writev调用会使tcp输出一次而不是两次,只产生一个tcp分节,这是首选方法;

--把两次写操作的数据复制到单个缓冲区,然后对缓冲区调用一次write;

--关闭Nagle算法,调用write两次;有损于网络,通常不考虑;