tcp中delay_ack的理解
内核版本,3.10。 首先,我们需要知道,在一个sock中,维护ack的就有很多变量,多种状态:
struct inet_connection_sock {
。。。。
__u8 icsk_ca_state:6,
icsk_ca_setsockopt:1,
icsk_ca_dst_locked:1;
__u8 icsk_retransmits;
__u8 icsk_pending;
__u8 icsk_backoff;
__u8 icsk_syn_retries;
__u8 icsk_probes_out;
__u16 icsk_ext_hdr_len;
struct {
__u8 pending; /* ACK is pending */-----------------------pending有很多标志,如IACK_ACK_TIMER,IACK_ACK_PUSHED
__u8 quick; /* Scheduled number of quick acks */
__u8 pingpong; /* The session is interactive */--------------为1,说明是交互型tcp流,为0则意味着基本是单向流
__u8 blocked; /* Delayed ACK was blocked by socket lock */-------------如果delayed ack在timer中被用户阻塞,则设置为1
__u32 ato; /* Predicted tick of soft clock */----------------用来计算delay_ack超时的中间变量
unsigned long timeout; /* Currently scheduled timeout */---------当前delay_ack的超时定时时长
__u32 lrcvtime; /* timestamp of last received data packet */-----------最新收到的报文的时戳
__u16 last_seg_size; /* Size of last incoming segment */
__u16 rcv_mss; /* MSS used for delayed ACK decisions */
} icsk_ack;
enum inet_csk_ack_state_t { ICSK_ACK_SCHED = 1,---------------说明ack需要被快速发送而没有被发送,但这个标志在设置timer的时候也会设置 ICSK_ACK_TIMER = 2,-----------------说明设置了delay_ack的timer ICSK_ACK_PUSHED = 4,-----------------说明需要将ack快点发送 ICSK_ACK_PUSHED2 = 8-----------------在已经设置了ICSK_ACK_PUSHED的情况下,tcp_mesure_rcv_mss会设置这个标志 };
/* tcp_data could move socket to TIME-WAIT */ if (sk->sk_state != TCP_CLOSE) { tcp_data_snd_check(sk);-----------看是否有数据也需要发送出去 tcp_ack_snd_check(sk);------------看是否需要发送ack }
因为收到数据,有两种选择,要么立刻回复ack,要么进行delay_ack的。
在具体实现中,用pingpong来区分这两种模式:
icsk->icsk_ack.pingpong == 0,表示使用快速确认,因为既然不是pingpong模式,说明ack没必要等,但是如果quick的阈值用完了,那么还是会延迟确认,哪怕pingpong =0.
icsk->icsk_ack.pingpong == 1,表示使用延迟确认。
delay_ack的好处是可以在网络上减少一点小包,比如可以和本端数据一起发送,比如可以收到N个报文,但只回复1个ack。当然任何一个特性,有好处自然也会带来坏处。delay_ack也不例外,毕竟增加了时延。
我们来看正常情况下发送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 &&//我们收到的报文量大于一个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) ||---------------------------------我们处于quickack状态 /* We have out of order data. */ (ofo_possible && skb_peek(&tp->out_of_order_queue))) {------我们收到了乱序报文 /* Then ack it now */ tcp_send_ack(sk);-------------------------------------------立即发送ack,不等待 } else { /* Else, send delayed ack. */ tcp_send_delayed_ack(sk);-----------------------------------否则,我们会发送delay_ack } }
那么是不是tcp_send_delay_ack就一定不会立刻发送ack呢,也不是的,不要被这个函数名称骗了:
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; tcp_ca_event(sk, CA_EVENT_DELAYED_ACK); if (ato > TCP_DELACK_MIN) {----------------------如果大于40ms的话 const struct tcp_sock *tp = tcp_sk(sk); int max_ato = HZ / 2;------------------------500ms if (icsk->icsk_ack.pingpong ||---------------------处于交互模式, (icsk->icsk_ack.pending & ICSK_ACK_PUSHED))-------------ack需要立刻发送,这个地方很奇怪,按道理此时max_ato应该设置小点才对。 max_ato = TCP_DELACK_MAX;----------------------能delay的话尽量delay,所以修改该值为200ms, /* 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_us) {-----------------能利用rtt的话 int rtt = max_t(int, usecs_to_jiffies(tp->srtt_us >> 3),-------这个>>3就是算法里面的计算rtt时的1/8权值,也就是rtt=old_rtt*7/8+new_rtt*1/8 TCP_DELACK_MIN);----------最大也就是40ms if (rtt < max_ato) max_ato = rtt;-----------------------rtt小于max_ato,再次修改max_ato } ato = min(ato, max_ato);---------------------确认最终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) {-----------之前还有一个延迟ack的定时器没到期 /* If delack timer was blocked or is about to expire, * send ACK now. */ if (icsk->icsk_ack.blocked ||---------------------被阻塞过,这个只在延迟确认定时器到期时,如果sock被user给lock住,则会设置会1,表示本该发送的ack没发 time_before_eq(icsk->icsk_ack.timeout, jiffies + (ato >> 2))) {//timer快到期了,也就是小于当前时间+ato/4的时间的话,干脆不等了。 tcp_send_ack(sk);----------------立刻发送ack,别等了,可以看到delay_ack的定时器也没有取消 return; } if (!time_before(timeout, icsk->icsk_ack.timeout)) timeout = icsk->icsk_ack.timeout; } icsk->icsk_ack.pending |= ICSK_ACK_SCHED | ICSK_ACK_TIMER;-----------------设置标志,表明有一个延迟ack的定时器被设置了。 icsk->icsk_ack.timeout = timeout; sk_reset_timer(sk, &icsk->icsk_delack_timer, timeout);------------重新设置延迟ack的定时器,超时时间是每次算出来的, }
从上面的计算可以看出,延迟ack的timeout时间不是简单地设置为40ms拉倒,虽然它默认值在HZ大于100的时候是设置为40ms。所以如果你分析报文的时候,如果抓包发现
delay_ack不是40ms,不要慌,看看上面这个函数计算timeout的方式,它其实是一个40ms ~ min(200ms, RTT)的动态值,不过我抓包看到过delay_ack有时候不到10ms,跟算法
不匹配,不知道为啥。
icsk_ack.ato 初始化为0,然后在第一次收包的时候修改为 TCP_ATO_MIN,也就是40ms,之后每次收包的时候计算,
1. delta <= TCP_ATO_MIN /2时,ato = ato / 2 + TCP_ATO_MIN / 2。
2. TCP_ATO_MIN / 2 < delta < ato时,ato = min(ato / 2 + delta, rto)。
3. delta >= ato时,ato值不变。
可以看出,ato的值,最大也不会超过rto。rto的最小的默认值是1s,所以ato最大不会超过1s。
当然这个ato的值并不是直接作用于delay_ack的timeout,具体可以 参照 tcp_send_delayed_ack 函数。
Q:发送ack的函数为?
A:发送ack的函数是:tcp_send_ack-->tcp_transmit_skb-->icsk->icsk_af_ops->queue_xmit,到ip层就离开了tcp了
设置delay_ack的timer:
负责设置延时ack的timer的函数为tcp_delack_timer_handler:
static inline void inet_csk_reset_xmit_timer(struct sock *sk, const int what, unsigned long when, const unsigned long max_when) { 。。。 } else if (what == ICSK_TIME_DACK) { icsk->icsk_ack.pending |= ICSK_ACK_TIMER; icsk->icsk_ack.timeout = jiffies + when; sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout); } 。。。。 }
由于 tcp_transmit_skb 是一个公共函数,所以在判断是发送ack的时候,用的是这个判断:
if (likely(tcb->tcp_flags & TCPHDR_ACK))//发送的是带ack tcp_event_ack_sent(sk, tcp_skb_pcount(skb));
tcp_event_ack_sent主要做什么?
static inline void tcp_event_ack_sent(struct sock *sk, unsigned int pkts) { tcp_dec_quickack_mode(sk, pkts);//每发送一次ack,会减少quick的值,也就是系统倾向于delayack的。 inet_csk_clear_xmit_timer(sk, ICSK_TIME_DACK); }
主要就是减少quick的计数。在非quickack模式下。除此之外, inet_csk_clear_xmit_timer 函数并不仅仅是删除timer,还需要做一个跟qiuckack相关的东西:
static inline void inet_csk_clear_xmit_timer(struct sock *sk, const int what) {。。。。 else if (what == ICSK_TIME_DACK) { icsk->icsk_ack.blocked = icsk->icsk_ack.pending = 0; #ifdef INET_CSK_CLEAR_TIMERS sk_stop_timer(sk, &icsk->icsk_delack_timer); #endif } 。。。。}
可以看到,会将 icsk->icsk_ack.blocked 和 icsk->icsk_ack.pending 都设置为0。
/* There is something which you must keep in mind when you analyze the * behavior of the tp->ato delayed ack timeout interval. When a * connection starts up, we want to ack as quickly as possible. The * problem is that "good" TCP's do slow start at the beginning of data * transmission. The means that until we send the first few ACK's the * sender will sit on his end and only queue most of his data, because * he can only send snd_cwnd unacked packets at any given time. For * each ACK we send, he increments snd_cwnd and transmits more of his * queue. -DaveM */ static void tcp_event_data_recv(struct sock *sk, struct sk_buff *skb) { struct tcp_sock *tp = tcp_sk(sk); struct inet_connection_sock *icsk = inet_csk(sk); u32 now; inet_csk_schedule_ack(sk);//设置ack状态 tcp_measure_rcv_mss(sk, skb);//计算mss tcp_rcv_rtt_measure(tp);//计算rtt now = tcp_time_stamp; if (!icsk->icsk_ack.ato) { /* The _first_ data packet received, initialize * delayed ACK engine. */ tcp_incr_quickack(sk); icsk->icsk_ack.ato = TCP_ATO_MIN; } else { int m = now - icsk->icsk_ack.lrcvtime; if (m <= TCP_ATO_MIN / 2) { /* The fastest case is the first. */ icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + TCP_ATO_MIN / 2; } else if (m < icsk->icsk_ack.ato) { icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + m; if (icsk->icsk_ack.ato > icsk->icsk_rto) icsk->icsk_ack.ato = icsk->icsk_rto; } else if (m > icsk->icsk_rto) { /* Too long gap. Apparently sender failed to * restart window, so that we send ACKs quickly. */ tcp_incr_quickack(sk);//增加快速ack的计数,前提非常难得,就是收包间隔大于重传定时器才进这个分支 sk_mem_reclaim(sk); } } icsk->icsk_ack.lrcvtime = now;//更新收到报文的最新时间 TCP_ECN_check_ce(tp, skb); if (skb->len >= 128) tcp_grow_window(sk, skb);//更新窗口 }
从这个函数的注释可以看出,当我们收到报文的时候,如果是一个链路的发起阶段,由于很多对端会启动慢启动流程,这样我们的ack需要快速发回,
这样对端可以增加它的snd_cwnd,然后更快地发包,所以说一个tcp连接,在没有明确setsockopt调用关闭quickack的情况下,应该是默认处于quickack的回复状态。
不过这种状态是有一定的阈值的,也就是 icsk->icsk_ack.quick 的值是有一个上限,一般最大为TCP_MAX_QUICKACKS=16,这么做主要就是为了加速slowstart的发包,因为ack回得越快,越能告诉服务器端客户端的最新情况和网络的情况。当然也更用户设置的
,且每发送一个ack,还会减少若干个阈值,,慢慢过渡到delay_ack流程,为了防止避免进入delay_ack,在 tcp_incr_quickack 中,而负责减少quick计数的函数是:
static inline void tcp_dec_quickack_mode(struct sock *sk, const unsigned int pkts) { struct inet_connection_sock *icsk = inet_csk(sk); if (icsk->icsk_ack.quick) {//处于quick模式下,更新quickack的计数,递减, if (pkts >= icsk->icsk_ack.quick) { icsk->icsk_ack.quick = 0;-------------------阈值不够了,进入delay_ack状态 /* Leaving quickack mode we deflate ATO. */ icsk->icsk_ack.ato = TCP_ATO_MIN;---------初始timer设置为40ms } else icsk->icsk_ack.quick -= pkts;//递减 } }
既然quickack是一个动态值,而且是慢慢减少,说明系统是倾向于delay_ack的
Q:如何关闭delay_ack
A:如果用户明确知道这条tcp链路是非pingpong模式,那么可以使用 TCP_QUICKACK 来设置socket属性,
case TCP_QUICKACK: if (!val) { icsk->icsk_ack.pingpong = 1; } else { icsk->icsk_ack.pingpong = 0; if ((1 << sk->sk_state) & (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT) && inet_csk_ack_scheduled(sk)) { icsk->icsk_ack.pending |= ICSK_ACK_PUSHED;--------设置要求推送ack的标志,说明 tcp_cleanup_rbuf(sk, 1); if (!(val & 1)) icsk->icsk_ack.pingpong = 1; } }
但由于delay_ack并不是一个固定值,是不停在计算的,所以在用户态程序需要不断设置TCP_QUICKACK ,当然我也觉得这个不合理,完全可以持久化。
总结一下:
Q:什么时候进行快速确认?
A:总结如下:
1、 接收到数据包,检查是否需要发送ACK时 (__tcp_ack_snd_check):
1. 接收缓冲区中有一个以上的全尺寸数据段仍然是NOT ACKed,并且接收窗口变大了。
所以一般收到了两个数据包后,会发送ACK,而不是对每个数据包都进行确认,但是如果我们收到的是2个小包,尺寸加起来还是小于MSS,也不会继续等,因为收到小包的话,
则会设置ICSK_ACK_PUSHED标志,第二次再收到小包,则设置ICSK_ACK_PUSHED2,这个在 tcp_measure_rcv_mss 函数中实现,则极大概率会立刻发送ack,此处的极大概率是指,
当我们发送ack的时候,如果出现内存不足,skb申请失败,则只能再次设置delay_ack,真tm复杂。还有,这个跟内核版本也有关系,不要混淆了前提,比如2.6的内核这个行为又不同。
2. 接收到数据包时,仍然处于快速确认模式中。也就是icsk_ack.quick配额还没有消耗完。
3. 接收到数据包时,乱序队列不为空,且传给 __tcp_ack_snd_check 的乱序与否的参数为1.
4.当接收队列中有数据复制到用户空间时,会判断是否要立即发送ACK,tcp_cleanup_rbuf 函数,
if (inet_csk_ack_scheduled(sk)) { const struct inet_connection_sock *icsk = inet_csk(sk); /* Delayed ACKs frequently hit locked sockets during bulk * receive. */ if (icsk->icsk_ack.blocked || /* Once-per-two-segments ACK was not sent by tcp_input.c */ tp->rcv_nxt - tp->rcv_wup > icsk->icsk_ack.rcv_mss || /* * If this read emptied read buffer, we send ACK, if * connection is not bidirectional, user drained * receive buffer and there was a small segment * in queue. */ (copied > 0 && ((icsk->icsk_ack.pending & ICSK_ACK_PUSHED2) || ((icsk->icsk_ack.pending & ICSK_ACK_PUSHED) && !icsk->icsk_ack.pingpong)) && !atomic_read(&sk->sk_rmem_alloc))) time_to_ack = true; } /* We send an ACK if we can now advertise a non-zero window * which has been raised "significantly". * * Even if window raised up to infinity, do not send window open ACK * in states, where we will not receive more. It is useless. */ if (copied > 0 && !time_to_ack && !(sk->sk_shutdown & RCV_SHUTDOWN)) { __u32 rcv_window_now = tcp_receive_window(tp); /* Optimize, __tcp_select_window() is not cheap. */ if (2*rcv_window_now <= tp->window_clamp) { __u32 new_window = __tcp_select_window(sk); /* Send ACK now, if this read freed lots of space * in our buffer. Certainly, new_window is new window. * We can advertise it now, if it is not less than current one. * "Lots" means "at least twice" here. */ if (new_window && new_window >= 2 * rcv_window_now) time_to_ack = true; } } if (time_to_ack) tcp_send_ack(sk);
从这几个条件看,由于每发送ack都会进行quick配额的减少,即 tcp_dec_quickack_mode 函数,所以快速确认的几率其实不高的。
Q:什么时候进行delay_ack
1. 快速确认模式中的ACK额度用完了,一般在快速确认了半个接收窗口的数据后,进入延迟确认模式。
2. 发送ACK时,因为内存分配失败,启动延迟确认定时器,希望过一会能申请到内存,我觉得这个应该优化为使用mem_pool,至少让服务器端知道这边内存不够,延迟那么几十毫秒意义不大,因为内存不会变化那么剧烈的。
3. 接收到数据包,检查是否需要发送ACK时(__tcp_ack_snd_check),如果无法进行快速确认。
4. 使用TCP_QUICKACK选项禁用快速确认,设置的值为0。
Q:什么时候进入quickack模式?
A:主要搜索 tcp_enter_quickack_mode 函数,要注意进入quickack模式和quickack的区别。在quickack模式下,不一定能立刻发ack,因为可能申请不到内存,在delay_ack模式下,也有可能不等定时器超时而立刻发送ack,所以我理解立刻发送ack和quickack模式是有关联的,而不是必然的关系。
1、TCP_ECN_check_ce 函数,数据包含有路由器的显式拥塞通知,进入快速确认模式。
2、应用进程显式设置TCP_QUICKACK选项之后:进入快速确认模式,并立即发送一个ACK。
3、在收到重复的带负荷的数据段时,这个需要认为我们的ack服务器没有收到,则立刻dup_ack给发送方,tcp_send_dupack 函数中,并立即发送一个ack。
参考资料:
https://www.rfc-editor.org/rfc/rfc5681.txt
https://blog.csdn.net/zhangskd/article/details/45127565