一、delay ack机制
就是延时发送ACK机制,起初设计这种机制是为了提高效率
提出了这样做的理由:
平均每个段回复的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的发送时机。
-
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);
-
}
-
}
上一段代码摘录自linux-2.6.32.60 net/ipv4/tcp_input.c,有多个判断,我们来逐个解析:
2.1.1 多个段没有确认
if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss
这一句说的意思是接收窗口中有大于一个段没有确认,通常情况下就是两个段一个ack了。
rcv_nxt和rcv_wup两个变量在tcp.h中定义,分别表示期望接收的下一个段、上一个已经确认的段。
u32 rcv_nxt; /* What we want to receive next */
u32 rcv_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
上面的逻辑判断,在不满足立即发送ack的条件下,会进入tcp_send_delayed_ack(sk);
这部分代码有关于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);
-
}
ato的定义在inet_sock_conneciton.h中,是一个时钟计时器。
-
struct inet_connection_sock {
-
..........................................
-
__u32 ato; /* Predicted tick of soft clock */
TCP_DELACK_MIN的定义看这里:
-
#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
关于HZ:用来定义每一秒有几次timer interrupts。HZ通常为1000【参 1】。
这样说来,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;
-
}
如果ack已经准备好了,定时器又快要超时,这种情况下就立即发送ack。
三、关闭delay ack
3.1 暴力方法
可以在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_QUICKACK在TCP源码中的实现,这里不做赘述,如果必要,后续会讲。
四、杂记
延迟确认的判断流程就是上面这些代码了,逻辑比较简单,总结起来就是普通情况下,大约两个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
阅读(1229) | 评论(0) | 转发(0) |