一、delay ack机制
就是延时发送ACK机制,起初设计这种机制是为了提高效率
http://tools.ietf.org/html/rfc1122#section-4.2.3.2提出了这样做的理由:平均每个段回复的ack个数小于1,可以提高主机和网络的效率。【按1】
给应用层更新窗口的时机并能给出及时回复【注1】
特殊应用场景下,比如字符界面远程登录,采用delay ack能减少服务端需要发送的数据段。
我们常用的xshell、ssh等都是这样的应用。发送端通常要发送ack、window更新、返回字符。
将这三者可以混合到一个包中,节省发送段数,提高效率。
此外,在多用户共享的大型主机中,delay ack能显著减少主机的处理负担,因为要处理的数据段减少了。
但是,过多的延迟会对RTT计算和packet clocking alg造成影响。
二、delay ack实现
2.1 _tcp_ack_snd_check()——tcp_input.c,关于数据段的判断
说了这么多废话,其实delay ack在实现中,一般体现为2个段回复一个ack。
TCP接收处理流程tcp_input.c中,有两条处理逻辑:快速路径和慢速路径。
这两条路径的区分和首部预测有关,后续博文会做想写解释。
无论是快速路径还是慢速路径,都会调用 _tcp_ack_snd_check() 函数判断ack的发送时机。
上一段代码摘录自linux-2.6.32.60 net/ipv4/tcp_input.c,有多个判断,我们来逐个解析:
这一句说的意思是接收窗口中有大于一个段没有确认,通常情况下就是两个段一个ack了。
rcv_nxt和rcv_wup两个变量在tcp.h中定义,分别表示期望接收的下一个段、上一个已经确认的段。
u32rcv_nxt;/* What we want to receive next */
u32rcv_wup;/* rcv_nxt on last window update sent*/
两个序号相减,当然就表示有多少段已经接收但没有确认了。
inet_csk(sk)->icsk_ack.rcv_mss这个值表示一个段大小。
&& __tcp_select_window(sk) >= tp->rcv_wnd)
这一句讲的是根据空余空间算出的window大小大于等于接收窗口,即有足够的空间容纳接收的段。
在TCP进行synsent状态处理、发送dupack、接收到窗口之外的数据段、或者收到ECN标志段时,进入快速确认模式。
持续快速确认模式的时间是有限的,大概可以连续快送发送8次ack,之后就要退出。
(ofo_possible && skb_peek(&tp->out_of_order_queue)))
乱序一般由丢包造成,在有丢包的情况下,网络可能已经拥塞,需要立即发送ack,通告对方降低发送速率。
至于ofo_possible和skb_peek这些变量和函数,暂时不需要过多关注。如果有必要,后续博文中会详细说明。
TCP接收处理流程tcp_input.c中,有两条处理逻辑:快速路径和慢速路径。
这两条路径的区分和首部预测有关,后续博文会做想写解释。
无论是快速路径还是慢速路径,都会调用 _tcp_ack_snd_check() 函数判断ack的发送时机。
- static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- /* More than one full frame received... */
- if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss
- /* ... and right edge of window advances far enough.
- * (tcp_recvmsg() will send ACK otherwise). Or...
- */
- && __tcp_select_window(sk) >= tp->rcv_wnd) ||
- /* We ACK each frame or... */
- tcp_in_quickack_mode(sk) ||
- /* We have out of order data. */
- (ofo_possible && skb_peek(&tp->out_of_order_queue))) {
- /* Then ack it now */
- tcp_send_ack(sk);
- } else {
- /* Else, send delayed ack. */
- tcp_send_delayed_ack(sk);
- }
- }
2.1.1 多个段没有确认
if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss这一句说的意思是接收窗口中有大于一个段没有确认,通常情况下就是两个段一个ack了。
rcv_nxt和rcv_wup两个变量在tcp.h中定义,分别表示期望接收的下一个段、上一个已经确认的段。
u32rcv_nxt;/* What we want to receive next */
u32rcv_wup;/* rcv_nxt on last window update sent*/
两个序号相减,当然就表示有多少段已经接收但没有确认了。
inet_csk(sk)->icsk_ack.rcv_mss这个值表示一个段大小。
&& __tcp_select_window(sk) >= tp->rcv_wnd)
这一句讲的是根据空余空间算出的window大小大于等于接收窗口,即有足够的空间容纳接收的段。
2.1.2 快速确认模式
tcp_in_quickack_mode(sk)在TCP进行synsent状态处理、发送dupack、接收到窗口之外的数据段、或者收到ECN标志段时,进入快速确认模式。
持续快速确认模式的时间是有限的,大概可以连续快送发送8次ack,之后就要退出。
2.1.3 有乱序段
/* We have out of order data. */(ofo_possible && skb_peek(&tp->out_of_order_queue)))
乱序一般由丢包造成,在有丢包的情况下,网络可能已经拥塞,需要立即发送ack,通告对方降低发送速率。
至于ofo_possible和skb_peek这些变量和函数,暂时不需要过多关注。如果有必要,后续博文中会详细说明。
2.2 延迟定时器40ms
这部分代码有关于delay ack定时器的判断:如果计时器大于TCP_DELACK_MIN,更改计时器的值,后续代码会发送ack。
点击(此处)折叠或打开
- /* Send out a delayed ack, the caller does the policy checking
- * to see if we should even be here. See tcp_input.c:tcp_ack_snd_check()
- * for details.
- */
- void tcp_send_delayed_ack(struct sock *sk)
- {
- struct inet_connection_sock *icsk = inet_csk(sk);
- int ato = icsk->icsk_ack.ato;
- unsigned long timeout;
- if (ato > TCP_DELACK_MIN) {
- const struct tcp_sock *tp = tcp_sk(sk);
- int max_ato = HZ / 2;
- if (icsk->icsk_ack.pingpong ||
- (icsk->icsk_ack.pending & ICSK_ACK_PUSHED))
- max_ato = TCP_DELACK_MAX;
- /* Slow path, intersegment interval is "high". */
- /* If some rtt estimate is known, use it to bound delayed ack.
- * Do not use inet_csk(sk)->icsk_rto here, use results of rtt measurements
- * directly.
- */
- if (tp->srtt) {
- int rtt = max(tp->srtt >> 3, TCP_DELACK_MIN);
- if (rtt < max_ato)
- max_ato = rtt;
- }
- ato = min(ato, max_ato);
- }
点击(此处)折叠或打开
- struct inet_connection_sock {
- ..........................................
- __u32 ato; /* Predicted tick of soft clock */
点击(此处)折叠或打开
- #define TCP_DELACK_MAX ((unsigned)(HZ/5)) /* maximal time to delay before sending an ACK */
- #if HZ >= 100
- #define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */
- #define TCP_ATO_MIN ((unsigned)(HZ/25))
- #else
- #define TCP_DELACK_MIN 4U
- #define TCP_ATO_MIN 4U
- #endif
这样说来,TCP_DELACK_MIN这个值大约就是40ms了,如果HZ值变动,这个值也会改变。
接着ato那一段代码:
点击(此处)折叠或打开
- /* Stay within the limit we were given */
- timeout = jiffies + ato;
- /* Use new timeout only if there wasn't a older one earlier. */
- if (icsk->icsk_ack.pending & ICSK_ACK_TIMER) {
- /* If delack timer was blocked or is about to expire,
- * send ACK now.
- */
- if (icsk->icsk_ack.blocked ||
- time_before_eq(icsk->icsk_ack.timeout, jiffies + (ato >> 2))) {
- tcp_send_ack(sk);
- return;
- }
- if (!time_before(timeout, icsk->icsk_ack.timeout))
- timeout = icsk->icsk_ack.timeout;
- }
三、关闭delay ack
3.1 暴力方法
关于TCP_QUICKACK在TCP源码中的实现,这里不做赘述,如果必要,后续会讲。
可以在tcp_ack_snd_check这个函数里面改,比如修改代码为任何情况下,都调用 tcp_send_ack(sk);
或者根据自己需要,更改判断条件。
3.2 安全的方法
使用TCP提供的系统调用接口。
例如,在recv系统调用后,设置TCP_QUICKACK【参 2】
点击(此处)折叠或打开
- recv(fd, rcvBuf, 132, 0);
- 10: setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, (int[]){1}, sizeof(int));
四、杂记
延迟确认的判断流程就是上面这些代码了,逻辑比较简单,总结起来就是普通情况下,大约两个TCP段回复一个ack,异常情况下会立即回复ack。除了上面基于数据段的判断,delay ack还有一个定时器,大约40ms,也就是说,在40ms内等不到数据,就立即发送ack,否则会对RTT测量和吞吐率带来负担。
有意思的是windows协议栈 TCP compound经常是多个段回复一个ack,没有这个协议的源码,这一点暂时不讨论了。
4.1 快速ack V.S. 延迟ack,性能问题
在协议性能需要优化时,有丢包的情况下,需要考虑启用快速ack,因为这样可以及时通知发送方丢包,避免滑动窗口停等,提升吞吐率。
ack其实并不会对网络性能有太大的影响,delay ack能减少带宽浪费、快速ack能及时通知,意义也就仅限于此了。
windows下,可以通过修改注册表,调整ack回复方式,以及接收窗口,这对于速度或许有40%的提升。玩网游的比较熟悉这一招。
4.2 NAGLE算法
delay ack和nagle算法一起使用,会引起某些问题,下一篇博文讨论nagle算法和糊涂窗口综合症。
注:
【1】没看懂什么叫“给应用层更新窗口的时机”。
下一篇博客会将“何时更新窗口”,这部分资料还没吃透
按:
【1】这句话应该指的是携带确认,防止浪费带宽。延迟可以等待数据。
比如普通tcp包头部有40Byte。ack的内容一般很少,可能几十字节。
如果立即发送,那有效的数据长度所占比例很小,不划算。可以和数据一起发送
参:
【1】http://blog.csdn.net/bdc995/article/details/4144031 HZ定义
【2】http://blog.csdn.net/sctq8888/article/details/7398967 使用TCP_QUICKACK