关于延时ACK的思考

在网络上很容易搜到关于延时ACK相关的信息以及触发条件,一般网上有一种说法是神秘的40ms的延时ACK反馈机制,但是在一般网上所说的都是在单独的一来一回的情景下展开的,很少有文章会讲关于客户端多个报文发送的条件下,除了40ms的延时返回外,是否还有什么机制触发延时ACK。

总体上说延时ACK发送的触发条件有两个:

(1) TCP中的延时ACK定时器触发

(2) 接收包时调用tcp_rcv_estalibshed

其中第一种情况在这里先不作讨论(都是定时器指定时间间隔触发的东西),我们讨论下tcp_rcv_estalibshed函数

int tcp_rcv_established(struct sock *sk, struct sk_buff *skb, struct tcphdr *th, unsigned len) {

	struct tcp_opt *tp = &(sk->tp_pinfo.af_tcp);

	tp->saw_tstamp = 0;
	// 触发进入条件:
	// 1、预测标识一致
	// 2、接收包的seq是顺序(乱序一般都是网络原因导致的)
	if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pref_flags &&
		TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
		... ...
		if (len <= tcp_header_len) {
			... ...
		}
		else {
			int eaten = 0;
			// 触发进入条件:
			// 1、当前进程进行的动作
			// 2、将要拷贝的seq为下次接收的seq,即表明为每次都很快就将数据读取完
			// 3、读取的缓存大小大于等于接收到的包的大小
			if (tp->ucopy.task == current && tp->copied_seq == tp->rcv_nxt &&
				len - tcp_header_len <= tp->ucopy.len && sk->lock.users) {
				// 这个eaten设置为重点
				eaten = 1;
				NET_INC_STATS_BH(TCPHPHitsToUser);
				__set_current_state(TASK_RUNNING);
				if (tcp_copy_to_iovec(sk, skb, tcp_header_len))
					goto csum_error;
				__skb_pull(skb, tcp_header_len);
				tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
			}
			else {
				... ...
			}
			// 计算ACK超时(ATO)时长
			tcp_event_data_recv(sk, tp, skb);
			
			if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
				... ...
			}
			
			if (eaten) {
				if (tcp_in_quick_mode(tp))
					tcp_send_ack(sk);
				else
					tcp_send_delayed_ack(sk);
			}
			else {
				__tcp_ack_snd_check(sk, 0);
			}
			
no_ack:
			if (eaten)
				__kfree_skb(skb);
			else
				sk->date_ready(sk, 0);
			return 0;
		}
	}
slow_path:
	... ...
	
	return 0;
}

我们看到eaten为1时都会触发延时ACK的函数流程(除了QUICKACK控制外),可以看到控制eaten为1的条件是这样的:

(1) tp->ucopy.task == current    --> 接收到的包跟当前调用recv对应的进程是一致的

(2) tp->copied_seq == tp->rcv_nxt    --> 当前的进程每次都会把接收缓存的信息拿光(拿的很快)

(3) len - tcp_header_len <= tp->ucopy.len --> 用户层调用recv的缓存大小比实际包大小大

(4) sk->lock.users --> 调用recv就会把sk->lock.users置为1

满足了上述方面的条件后会就触发tcp_send_delayed_ack函数:

void tcp_send_delayed_ack(struct sock *sk) {

	struct tcp_opt *tp = &sk->tp_pinfo.af_tcp;
	int ato = tp->ack.ato;
	unsigned long timeout;
	
	if (ato > TCP_DELACK_MIN) {
		int max_ato = HZ/2;
		
		if (tp->ack.pingpong || (tp->ack.pending & TCP_ACK_PUSHED))
			max_ato = TCP_DELACK_MAX;
			
		if (tp->srtt) {
			int rtt = max(tp->srtt>>3, TCP_DELACK_MIN);
			if (rtt < max_ato)
				max_ato = rtt;
		}
		ato = min(ato, max_ato);
	}
	
	timeout = jiffies + ato;
	
	if (tp->ack.pending & TCP_ACK_TIMER) {
		if (tp->ack.blocked || time_before_eq(tp->ack.timeout, jiffies + (ato>>2))) {
			tcp_send_ack(sk);
			return ;
		}
		if (!time_before(timeout, tp->ack.timeout))
			timeout = tp->ack.timeout;
	}
	tp->ack.pending | TCP_ACK_SCHED | TCP_ACK_TIMER;
	tp->ack.timeout = timeout;
	if (!mod_timer(&tp->delack_timer, timeout))
		sock_hold(sk);
	... ...
}

这个函数重点是在触发tcp_send_ack函数(该函数会立刻发ack)的逻辑,触发条件如下:

(1) tp->ack.blocked --> 该功能会在定时ACK上会置1

(2) time_before_eq(tp->ack.timeout, jiffies + (ato>>2)) --> 已经到达超时时间


如果eaten为0时,则走__tcp_ack_snd_check函数,函数详细信息如下:

void __tcp_ack_snd_check(struct sock *sk, int ofo_possible) {

	struct tcp_opt *tp = &(sk->tp_pinfo.af_tcp);
	
	if (((tp->rcv_nxt - tp->rcv_wup) > tp->ack.rcv_mss && __tcp_select_window(sk) >= tp->rcv_wnd) ||
		tcp_in_quickack_mode(tp) ||
		(ofo_possible && skb_peek(&tp->out_of_order_queue) != NULL)) {
	
		tcp_send_ack(sk);
	}
	else {
		tcp_send_delayed_ack(sk);
	}
}

从中我们可以看到满足下面的几个条件的一个就会立刻触发ACK,条件如下:

(1) tp->rcv_nxt - tp->rcv_wup > tp->ack.rcv_mss --> 收到的seq跟已经确认的seq大于mss

(2) __tcp_select_window(sk) >= tp->rcv_wnd --> 新的接收窗口比旧的大

(3) tcp_in_quickack_mode(sk)

(4) ofo_possible && skb_peek(&tp->out_of_order_queue) != NULL --> seq乱序了,不过这里参数ofo_possible为0,该条件忽略


总结如下:

其实延时ACK的触发有部分是按照书上说的40ms的定时器触发的,但是还有一部分是由于函数__tcp_ack_snd_check触发ACK动作的,主要由触发ACK的点如下:

(1) tp->rcv_nxt - tp->rcv_wup > tp->ack.rcv_mss:

该条件下如果发送端发送数据很多且很频繁,那么累计到mss字节后接收端就会发ACK

(2) __tcp_select_window(sk) >= tp->rcv_wnd:

新的接收窗口大于等于旧的接收窗口,该条件下如果应用层recv接收数据及时一般都会触发的

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值