6.4 Nagle算法与CORK算法

来源:互联网 发布:淘宝泄露客户信息处罚 编辑:程序博客网 时间:2024/06/06 01:04

6.4.1 算法目的

        当发送端应用进程产生数据很慢(比如Telnet应用),就会使应用进程间传送的TCP有效载荷很小(可能只有1个字节),而传输开销有40字节(20字节的IP头+20字节的TCP头) 这种现象就叫糊涂窗口综合症(Silly Windw Syndrome)。如果有大量的小TCP报文在网络中传输会导致网络拥塞。Nagle算法是为了解决TCP数据发送端“糊涂窗口综合症”而产生的。

6.4.2 算法原理

        Nagle算法会对发送缓冲区内的一定数量的消息进行自动连接(该处理过程称为Nagling),通过减少必须发送的封包的数量,提高了网络应用程序系统的效率,减缓了网络拥塞。所谓的CORK就是塞子的意思,形象地理解就是用CORK将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。设置该选项后,内核会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去。

       Nagle算法和CORK算法非常类似,但是它们的着眼点不一样,Nagle算法主要避免网络因为太多的小包(协议头的比例非常之大)而拥塞,而CORK算法则是为了提高网络的利用率,使得总体上协议头占用的比例尽可能的小。但这二者都会避免发送小包,在这一点上是一致的。而且在Linux的实现上,Nagle和CORK也是结合在一起的。然而Nagle算法关心的是网络拥塞问题,只要所有的ACK回来则发包,而CORK算法却可以关心内容,在前后数据包发送间隔很短的前提下(否则内核会帮你将分散的包发出),即使你是分散发送多个小数据包,你也可以通过使能CORK算法将这些内容拼接在一个包内,如果此时用Nagle算法的话,则可能做不到这一点。

6.4.3 内核实现

        由前文可知,TCP在发送数据时会调用tcp_write_xmit函数,而Nagle算法在tcp_write_xmit中设置了一道“关卡”:tcp_nagle_test:

1811 static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,1812                int push_one, gfp_t gfp)1813 {...1854         if (tso_segs == 1) {  //发送一个报文段1855             if (unlikely(!tcp_nagle_test(tp, skb, mss_now,1856                              (tcp_skb_is_last(sk, skb) ?1857                               nonagle : TCP_NAGLE_PUSH))))1858                 break; //Nagle算法不允许发送,则停止...1893         tcp_minshall_update(tp, mss_now, skb); ...
        1893:如果发送的数据长度小于mss_now,则记录最后一个字节的seq到tp->snd_sml中

        tcp_nagle_test就像一个闸门,根据Nagle算法的意愿控制着TCP报文的发送。来看看它是怎么做的:

1442 static inline bool tcp_minshall_check(const struct tcp_sock *tp)1443 {1444     return after(tp->snd_sml, tp->snd_una) &&1445         !after(tp->snd_sml, tp->snd_nxt);  //snd_una < snd_sml <= snd_nxt1446 }1447 1448 /* Return false, if packet can be sent now without violation Nagle's rules:1449  * 1. It is full sized.1450  * 2. Or it contains FIN. (already checked by caller)1451  * 3. Or TCP_CORK is not set, and TCP_NODELAY is set.1452  * 4. Or TCP_CORK is not set, and all sent packets are ACKed.1453  *    With Minshall's modification: all sent small packets are ACKed.1454  */1455 static inline bool tcp_nagle_check(const struct tcp_sock *tp,1456                   const struct sk_buff *skb,1457                   unsigned int mss_now, int nonagle)1458 {1459     return skb->len < mss_now &&  //数据长度小于MSS1460         ((nonagle & TCP_NAGLE_CORK) || //设置了TCP_NAGLE_CORK标记1461          (!nonagle && tp->packets_out && tcp_minshall_check(tp)));1462 }1463 1464 /* Return true if the Nagle test allows this packet to be1465  * sent now.1466  */1467 static inline bool tcp_nagle_test(const struct tcp_sock *tp, const struct sk_buff *skb,1468                   unsigned int cur_mss, int nonagle)1469 {1470     /* Nagle rule does not apply to frames, which sit in the middle of the1471      * write_queue (they have no chances to get new data).1472      *1473      * This is implemented in the callers, where they modify the 'nonagle'1474      * argument based upon the location of SKB in the send queue.1475      */1476     if (nonagle & TCP_NAGLE_PUSH) //有TCP_NAGLE_PUSH标记1477         return true; //允许发送1478 1479     /* Don't use the nagle rule for urgent data (or for the final FIN). */1480     if (tcp_urg_mode(tp) || (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN))1481         return true; //放行1482 1483     if (!tcp_nagle_check(tp, skb, cur_mss, nonagle))1484         return true;  //通过1485 1486     return false; //不许发送1487 }

        1461:tp->packets_out非0意味着有报文发送出去但未被确认;tcp_minshall_check为假意味着snd_msl <= snd_una(snd_msl不可能大于snd_nxt),即发送的最新的小报文段已经被确认。tp->packets_out && tcp_minshall_check(tp)为真的含义是有发送出去但尚未被确认的小报文。nonagle的类型有:

 214 #define TCP_NAGLE_OFF       1   /* Nagle's algo is disabled */ 215 #define TCP_NAGLE_CORK      2   /* Socket is corked     */ 216 #define TCP_NAGLE_PUSH      4   /* Cork is overridden for already queued data */

        在这里nonagel的值一定不是TCP_NAGLE_PUSH(否则早已在1477行返回),也不会是TCP_NAGLE_CORK,否则1460行为真则1461行无法走到;所以nonagel要么是0,要么是TCP_NAGLE_OFF

        从代码上看,Nagle算法允许发送数据的条件为满足下列标准中的任意一个即可:

(1)nonagle设置了TCP_NAGLE_PUSH标记

(2)TCP处于紧急模式(有紧急数据要处理)

(3)报文中有FIN标记

(4)tcp_nagle_check为假

        先来看(4)。tcp_nagle_check为假即1459-1461行的逻辑为假成立的条件为下列标准中的任意一个:

1)数据长度大于等于MSS

2)nonagle没有设置TCP_NAGLE_CORK标记并且nonagle有TCP_NAGLE_OFF标记(即nonagel == TCP_NAGLE_OFF为真)

3)nonagle == 0为真且没有未确认的小报文

        用这3个条件替换(4),我们得到了新的Nagle算法允许发送数据的条件:

(1)nonagle设置了TCP_NAGLE_PUSH标记

(2)TCP处于紧急模式(有紧急数据要处理)

(3)报文中有FIN标记

(4)数据长度大于等于MSS

(5)nonagle == TCP_NAGLE_OFF为真

(6)nonagle == 0为真且没有未确认的小报文

        再来看(1)满足的情况。nonagle是由tcp_write_xmit函数的nonagle参数传入。tcp_sendmsg函数中对tcp_write_xmit函数的调用是用过tcp_push系列函数完成的:

1016 int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,1017         size_t size)1018 {...1099             skb = tcp_write_queue_tail(sk); //得到队列尾部的skb1100             if (tcp_send_head(sk)) { //skb尚未发送1101                 if (skb->ip_summed == CHECKSUM_NONE)1102                     max = mss_now;1103                 copy = max - skb->len;1104             }1105 1106             if (copy <= 0) { //空间不足1107 new_segment:1108                 /* Allocate new segment. If the interface is SG,1109                  * allocate skb fitting to single page.1110                  */1111                 if (!sk_stream_memory_free(sk))1112                     goto wait_for_sndbuf;1113 1114                 skb = sk_stream_alloc_skb(sk,1115                               select_size(sk, sg),1116                               sk->sk_allocation); //申请一个新的1117                 if (!skb)1118                     goto wait_for_memory;...1201             if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))1202                 continue;1203             //数据长度小于MSS的skb不会走到这里1204             if (forced_push(tp)) {  //积累一定字节数的报文后forced_push会为真1205                 tcp_mark_push(tp, skb);1206                 __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);//传给tcp_write_xmit函数TCP_NAGLE_PUSH标记1207             } else if (skb == tcp_send_head(sk))  //发送的是新写入内核的第一个包1208                 tcp_push_one(sk, mss_now); //传给tcp_write_xmit函数TCP_NAGLE_PUSH标记1209             continue;1210 1211 wait_for_sndbuf:1212             set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);1213 wait_for_memory:  //无法申请内核内存或接收缓存空间达到限制1214             if (copied)  1215                 tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);//传给tcp_write_xmit函数TCP_NAGLE_PUSH标记...1223 1224 out:1225     if (copied)1226         tcp_push(sk, flags, mss_now, tp->nonagle); //不传递TCP_NAGLE_PUSH标记给tcp_write_xmit函数...
        1099-1116:数据会优先放在有剩余空间且尚未发送的旧skb中,这样就可以将多个小数据段拼成一个大的发送

        1204-1206:forced_push的功能是检查是否允许向外推送报文:

 585 static inline void tcp_mark_push(struct tcp_sock *tp, struct sk_buff *skb) 586 { 587     TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; 588     tp->pushed_seq = tp->write_seq; 589 } 590  591 static inline bool forced_push(const struct tcp_sock *tp) 592 { 593     return after(tp->write_seq, tp->pushed_seq + (tp->max_window >> 1)); 594 }
        可见当有多于tp->max_window >> 1个字节的数据没有被"mark_push"时,forced_push为真。

        1207:当前写入数据的skb为要发送的首包时这个条件为真;tcp_send_head指针的设置是在skb_entail中完成,而必须是新申请的skb才能调用skb_entail来加入发送队列。这就意味着当前skb之前的所有skb已经全部发送完毕。

        1213:内存紧张时,需要发送数据以便收到确认时能够腾出发送缓存的空间

        在tcp_sendmsg的1026、1028、1215、1226行共有4次发送数据,其中3次会有TCP_NAGLE_PUSH标记,但这时发送的都是满MSS的大包。1226行的调用会使用tp->nonagle中的标记,但由于tcp_sendmsg会调用skb_entail函数去除TCP_NAGLE_PUSH标记:

 596 static inline void skb_entail(struct sock *sk, struct sk_buff *skb) 597 { 598     struct tcp_sock *tp = tcp_sk(sk); 599     struct tcp_skb_cb *tcb = TCP_SKB_CB(skb); 600  601     skb->csum    = 0; 602     tcb->seq     = tcb->end_seq = tp->write_seq; 603     tcb->tcp_flags = TCPHDR_ACK; 604     tcb->sacked  = 0; 605     skb_header_release(skb); 606     tcp_add_write_queue_tail(sk, skb); 607     sk->sk_wmem_queued += skb->truesize; 608     sk_mem_charge(sk, skb->truesize); 609     if (tp->nonagle & TCP_NAGLE_PUSH) 610         tp->nonagle &= ~TCP_NAGLE_PUSH; 611 }
故1226行不会传递TCP_NAGLE_PUSH标记。1226行会调用tcp_push函数:

 619 static inline void tcp_push(struct sock *sk, int flags, int mss_now, 620                 int nonagle) 621 { 622     if (tcp_send_head(sk)) { 623         struct tcp_sock *tp = tcp_sk(sk); 624  625         if (!(flags & MSG_MORE) || forced_push(tp)) 626             tcp_mark_push(tp, tcp_write_queue_tail(sk)); 627  628         tcp_mark_urg(tp, flags); 629         __tcp_push_pending_frames(sk, mss_now, 630                       (flags & MSG_MORE) ? TCP_NAGLE_CORK : nonagle); 631     } 632 }
        可见当应用进程使用MSG_MORE(可以通过send系统调用的flags参数设置)属性时,tcp_write_xmit会收到TCP_NAGLE_CORK标记。由上面总结的Nagle算法允许发送数据的条件可知,有TCP_NAGLE_CORK标记时Nagle算法是不允许发送数据的。这时的效果与使用setsockopt系统调用的TCP_CORK选项设置TCP_CORK标记的效果是一样的。

        除了tcp_sendmsg函数外,设置TCP_NAGLE_PUSH标记的代码还有setsockopt系统调用:

2371 static int do_tcp_setsockopt(struct sock *sk, int level,2372         int optname, char __user *optval, unsigned int optlen)2373 {...2423     case TCP_NODELAY:2424         if (val) {2425             /* TCP_NODELAY is weaker than TCP_CORK, so that2426              * this option on corked socket is remembered, but2427              * it is not activated until cork is cleared.2428              *2429              * However, when TCP_NODELAY is set we make2430              * an explicit push, which overrides even TCP_CORK2431              * for currently queued segments.2432              */2433             tp->nonagle |= TCP_NAGLE_OFF|TCP_NAGLE_PUSH;2434             tcp_push_pending_frames(sk);  //传递tp->nonagle给tcp_write_xmit...2503     case TCP_CORK:2504         /* When set indicates to always queue non-full frames.2505          * Later the user clears this option and we transmit2506          * any pending partial frames in the queue.  This is2507          * meant to be used alongside sendfile() to get properly2508          * filled frames when the user (for example) must write2509          * out headers with a write() call first and then use2510          * sendfile to send out the data parts.2511          *2512          * TCP_CORK can be set together with TCP_NODELAY and it is2513          * stronger than TCP_NODELAY.2514          */2515         if (val) {2516             tp->nonagle |= TCP_NAGLE_CORK;2517         } else {2518             tp->nonagle &= ~TCP_NAGLE_CORK;2519             if (tp->nonagle&TCP_NAGLE_OFF)2520                 tp->nonagle |= TCP_NAGLE_PUSH;2521             tcp_push_pending_frames(sk);2522         }
        这里有两种情况会通过tcp_push_pending_frames传递给tcp_write_xmit函数TCP_NAGLE_PUSH标记来允许发送报文:

1)使用TCP_NODELAY socket选项关闭Nagle算法时(这时条件(5)成立)

2)使用TCP_CORK socket选项"拔出塞子"且Nagle算法已经被关闭时

        条件(1)成立等同于上述两个条件任意一个成立。

        使条件(5)成立的还有一处,就是TCP丢失探测定时器超时的时候。这个定时器超时的函数是tcp_send_loss_probe:

1974 void tcp_send_loss_probe(struct sock *sk)1975 {      1976     struct tcp_sock *tp = tcp_sk(sk);1977     struct sk_buff *skb;1978     int pcount;1979     int mss = tcp_current_mss(sk);1980     int err = -1;1981 1982     if (tcp_send_head(sk) != NULL) {1983         err = tcp_write_xmit(sk, mss, TCP_NAGLE_OFF, 2, GFP_ATOMIC);1984         goto rearm_timer;1985     }
        这时会关闭Nagle算法来发送数据。

        再看条件(6)。nonagle == 0为真意味着:

1)没有TCP_NAGLE_OFF标记(即没有关闭Nagle算法)

2)没有TCP_NAGLE_CORK标记(即没有开启TCP_CORK算法)

3)没有TCP_NAGLE_PUSH标记(即条件(1)不成立,而1)成立则条件(1)不会成立)

        这3个条件同时成立。

        下面根据最新的结论更新一下Nagle算法允许发送数据的条件:、

(1)使用TCP_NODELAY socket选项关闭Nagle算法时

(2)TCP处于紧急模式(有紧急数据要处理)

(3)报文中有FIN标记

(4)数据长度大于等于MSS

(5)使用TCP_CORK socket选项"拔出塞子"且Nagle算法已经被关闭时

(6)既没有关闭Nagle算法也没有开启TCP_CORK算法,且没有未确认的小报文

(7)上述条件均未满足但定时器超时

        由上述条件可知,在通常情况下(未关闭Nagle算法、发送大量小报文,定时器未超时),Nagle算法会将小报文积攒的发送队列中并拼成一个大的发送,从而减小了小数据的发送。如果对端ACK回复很快的话(条件(6)成立),Nagle算法会认为没有拥塞,就不会拼接太多的数据包,虽然可能不至于导致网络拥塞,网络总体的利用率依然很低。

0 0
原创粉丝点击