内核版本,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;
其中,icsk_ack.pending 就有多种状态组合:
enum inet_csk_ack_state_t {
ICSK_ACK_SCHED = ,---------------说明ack需要被快速发送而没有被发送,但这个标志在设置timer的时候也会设置
ICSK_ACK_TIMER = ,-----------------说明设置了delay_ack的timer
ICSK_ACK_PUSHED = ,-----------------说明需要将ack快点发送
ICSK_ACK_PUSHED2 = 8-----------------在已经设置了ICSK_ACK_PUSHED的情况下,tcp_mesure_rcv_mss会设置这个标志
};
tcp_rcv_state_process 在当tcp收到数据的时候,如果正常,会经历两个函数:
    /* 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 / ;------------------------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就是算法里面的计算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 >> ))) {//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,跟算法

不匹配,不知道为啥。

Q:tcp_send_delayed_ack 函数中,delay_ack的timeout 是由ato决定的,那么icsk_ack.ato 的计算方式是?
A:主要的函数在tcp_event_data_recv 中,
icsk_ack.ato 初始化为0,然后在第一次收包的时候修改为 TCP_ATO_MIN,也就是40ms,之后每次收包的时候计算,
假设delta为这次收包到上次收包的间隔,则算法为:

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 = ;
#ifdef INET_CSK_CLEAR_TIMERS
sk_stop_timer(sk, &icsk->icsk_delack_timer);
#endif
}
。。。。}

可以看到,会将  icsk->icsk_ack.blocked 和 icsk->icsk_ack.pending 都设置为0。

tcp_data_snd_check对delayack的影响:
要知道,一个tcp流要满足pingpong模式,显然应该有收有发,而且收发的频率还应该相当才对,就像人们打乒乓球一样。我们来看 tcp_data_snd_check 对delayack相关状态的影响:
/* 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 / ) {
/* The fastest case is the first. */
icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> ) + TCP_ATO_MIN / ;
} else if (m < icsk->icsk_ack.ato) {
icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> ) + 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 >= )
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 = ;-------------------阈值不够了,进入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 = ;
} else {
icsk->icsk_ack.pingpong = ;
if (( << 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, );
if (!(val & ))
icsk->icsk_ack.pingpong = ;
}
}

但由于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 > &&
((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 > && !time_to_ack && !(sk->sk_shutdown & RCV_SHUTDOWN)) {
__u32 rcv_window_now = tcp_receive_window(tp); /* Optimize, __tcp_select_window() is not cheap. */
if (*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 >= * 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

05-08 15:10