TCP数据接收之ACK的处理

本文详细介绍了TCP协议在接收到ACK时的处理过程,包括发送窗口的更新、ACK参数解析以及tcp_ack()函数的核心操作。重点讨论了在收到ACK后如何更新发送窗口,并分析了快速路径和慢速路径下的不同处理策略。

在TCP输入数据段的处理过程中,如果发现输入段携带了ACK信息,则会调用tcp_ack()进行ACK相关的处理。实际中,ACK信息总是会携带的,因为携带ACK不需要任何的附加开销,所以对于输入的每一个段(输了RST等特殊段),这个过程总是要执行的,这篇笔记就来看看TCP对ACK确认的处理过程。

1. 发送窗口

收到确认后,一项重要的工作就是更新发送窗口,所以这里再来认识一下发送窗口相关的几个关键字段,如下图所示:

在这里插入图片描述
注意,图中的发送窗口,指的是接收方通知给发送方的接收窗口大小,即流量控制窗口,并非拥塞窗口。

2. ACK处理

2.1 参数flag

tcp_ack()有个非常重要的参数flag,其贯穿整个ACK的处理过程,它记录了从输入段中能够获取到的任何信息(比如是否携带了数据、是否重复ACK、是否是SACK等),供后面的拥塞控制、RTT采样等操作参考。flag可能是如下值的组合。

flag描述
FLAG_DATA0x01ACK段中携带了数据
FLAG_WIN_UPDATE0x02收到ACK段后更新了发送窗口,可能更新了左边界,也有可能更新了右边界(通告窗口变大)
FLAG_DATA_ACKED0x04ACK段确认了新数据
FLAG_RETRANS_DATA_ACKED0x08ACK段携带的数据已经收到过了
FLAG_SYN_ACKED0x10ACK段确认了SYN段
FLAG_DATA_SACKED0x20ACK段确认了新的数据
FLAG_ECE0x40该ACK段携带了ECE标志
FLAG_DATA_LOST0x80SACK检测到了数据丢失
FLAG_SLOWPATH0x100该ACK段是由慢速路径处理的
FLAG_ONLY_ORIG_SACKED0x200
FLAG_SND_UNA_ADVANCED0x400ACK段更新了snd_una,即收到ACK后,发送窗口左边界可以右移
FLAG_DSACKING_ACK0x800ACK段中包含有DSACK信息
FLAG_NONHEAD_RETRANS_ACKED0x1000
FLAG_SACK_RENEGING0x2000检测到之前SACK确认过的数据段被对端丢弃了(这是协议允许的)

此外,还定义了一些基本flag的组合:

#define FLAG_ACKED		(FLAG_DATA_ACKED|FLAG_SYN_ACKED)
//用于判断输入的数据段是否为重复段
#define FLAG_NOT_DUP		(FLAG_DATA|FLAG_WIN_UPDATE|FLAG_ACKED)
#define FLAG_CA_ALERT		(FLAG_DATA_SACKED|FLAG_ECE)
#define FLAG_FORWARD_PROGRESS	(FLAG_ACKED|FLAG_DATA_SACKED)
#define FLAG_ANY_PROGRESS	(FLAG_FORWARD_PROGRESS|FLAG_SND_UNA_ADVANCED)

2.2 tcp_ack()

/* This routine deals with incoming acks, but not outgoing ones. */
static int tcp_ack(struct sock *sk, struct sk_buff *skb, int flag)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	//TCB中尚未被确认的最小序号
	u32 prior_snd_una = tp->snd_una;
	//ACK段中的序号
	u32 ack_seq = TCP_SKB_CB(skb)->seq;
	//ACK段中的确认号
	u32 ack = TCP_SKB_CB(skb)->ack_seq;
	u32 prior_in_flight;
	u32 prior_fackets;
	int prior_packets;
	int frto_cwnd = 0;

	/* If the ack is newer than sent or older than previous acks
	 * then we can probably ignore it.
	 */
	//确认的是还没有发送的数据,这是无意义的确认,直接返回
	if (after(ack, tp->snd_nxt))
		goto uninteresting_ack;
	//该确认号已经收到过了。这种可能是重复ACK,也有可能是正常的,比如该AC段有延时。
	//这种ACK有可能还携带了有效的SACK信息
	if (before(ack, prior_snd_una))
		goto old_ack;

	//到这里,说明确认号在期望的范围内[snd_una, snd_nxt],

	//确认号确认了新数据,设置FLAG_SND_UNA_ADVANCED标记。
	//if判断是为了排除ack==prior_snd_una的情况
	if (after(ack, prior_snd_una))
		flag |= FLAG_SND_UNA_ADVANCED;

	//tcp_abc特性相关
	if (sysctl_tcp_abc) {
		if (icsk->icsk_ca_state < TCP_CA_CWR)
			tp->bytes_acked += ack - prior_snd_una;
		else if (icsk->icsk_ca_state == TCP_CA_Loss)
			/* we assume just one segment left network */
			tp->bytes_acked += min(ack - prior_snd_una, tp->mss_cache);
	}

	prior_fackets = tp->fackets_out;
	prior_in_flight = tcp_packets_in_flight(tp);

	//下面是更新发送窗口,按照快速路径和慢速路径分别处理
	if (!(flag & FLAG_SLOWPATH) && after(ack, prior_snd_una)) {
		/* Window is constant, pure forward advance.
		 * No more checks are required.
		 * Note, we use the fact that SND.UNA>=SND.WL2.
		 */
		//记录最近一次导致发送窗口更新的ACK段的序号,即tp->snd_wl1=ack_seq
		tcp_update_wl(tp, ack, ack_seq);
		//更新发送窗口左边界
		tp->snd_una = ack;
		//设置发送窗口更新标记
		flag |= FLAG_WIN_UPDATE;
		//通知拥塞控制算法,发生了CA_EVENT_FAST_ACK事件
		tcp_ca_event(sk, CA_EVENT_FAST_ACK);

		NET_INC_STATS_BH(LINUX_MIB_TCPHPACKS);
	} else {
		//慢速路径处理

		//ACK段还携带了数据,设置FLAG_DATA标记
		if (ack_seq != TCP_SKB_CB(skb)->end_seq)
			flag |= FLAG_DATA;
		else
			NET_INC_STATS_BH(LINUX_MIB_TCPPUREACKS);
		//更新发送窗口
		flag |= tcp_ack_update_window(sk, skb, ack, ack_seq);

		//SACK相关处理
		if (TCP_SKB_CB(skb)->sacked)
			flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una);
		//ECN相关处理
		if (TCP_ECN_rcv_ecn_echo(tp, tcp_hdr(skb)))
			flag |= FLAG_ECE;
		//通知拥塞控制算法,发生了CA_EVENT_SLOW_ACK事件
		tcp_ca_event(sk, CA_EVENT_SLOW_ACK);
	}

	//清除软件错误
	sk->sk_err_soft = 0;
	//更新最近一次接收到ACK段的时间戳
	tp->rcv_tstamp = tcp_time_stamp;
	//如果之前根本就没有待确认的段,那么无需后续的重传队列以及拥塞控制处理;
	//这种情况下需要做和持续定时器相关的操作,因为可能之前传送过探测报文
	prior_packets = tp->packets_out;
	if (!prior_packets)
		goto no_queue;

	//删除重传队列中已经确认的数据段,并进行时延采样
	flag |= tcp_clean_rtx_queue(sk, prior_fackets);

	//F-RTO算法相关内容
	if (tp->frto_counter)
		frto_cwnd = tcp_process_frto(sk, flag);
	/* Guarantee sacktag reordering detection against wrap-arounds */
	if (before(tp->frto_highmark, tp->snd_una))
		tp->frto_highmark = 0;

	//拥塞控制相关
	if (tcp_ack_is_dubious(sk, flag)) {
		/* Advance CWND, if state allows this. */
		if ((flag & FLAG_DATA_ACKED) && !frto_cwnd &&
		    tcp_may_raise_cwnd(sk, flag))
			tcp_cong_avoid(sk, ack, prior_in_flight);
		tcp_fastretrans_alert(sk, prior_packets - tp->packets_out,
				      flag);
	} else {
		if ((flag & FLAG_DATA_ACKED) && !frto_cwnd)
			tcp_cong_avoid(sk, ack, prior_in_flight);
	}

	if ((flag & FLAG_FORWARD_PROGRESS) || !(flag & FLAG_NOT_DUP))
		dst_confirm(sk->sk_dst_cache);

	return 1;

no_queue:
	//之前没有未被确认的段,收到了ACK,进行持续定时器相关处理
	icsk->icsk_probes_out = 0;

	/* If this ack opens up a zero window, clear backoff.  It was
	 * being used to time the probes, and is probably far higher than
	 * it needs to be for normal retransmission.
	 */
	if (tcp_send_head(sk))
		tcp_ack_probe(sk);
	return 1;

old_ack:
	//虽然该ACK已经收到过了,但是如果其携带了SACK信息,需要更新确认内容
	if (TCP_SKB_CB(skb)->sacked)
		tcp_sacktag_write_queue(sk, skb, prior_snd_una);

uninteresting_ack:
	SOCK_DEBUG(sk, "Ack %u out of %u:%u\n", ack, tp->snd_una, tp->snd_nxt);
	return 0;
}

可见,tcp_ack()的核心操作干了三件事:

  1. 更新发送窗口;
  2. 清除发送队列中已经被确认的数据(包括重传数据)并进行RTT采样;
  3. 进行拥塞控制。

这篇笔记只详细介绍第一件事。

3. 更新发送窗口

快速路径处理情况,因为此时在接收数据,所以输入段的windows字段一定是没有发生变化的,所以无需更新snd_wnd的值,直接更新snd_una即可。

慢速路径处理情况,情况复杂,需要做更多的判断,调用tcp_ack_update_window()完成发送窗口的更新。

/* Update our send window.
 *
 * Window update algorithm, described in RFC793/RFC1122 (used in linux-2.2
 * and in FreeBSD. NetBSD's one is even worse.) is wrong.
 */
static int tcp_ack_update_window(struct sock *sk, struct sk_buff *skb, u32 ack,
				 u32 ack_seq)
{
	struct tcp_sock *tp = tcp_sk(sk);
	int flag = 0;
	//ACK段中携带的通告窗口
	u32 nwin = ntohs(tcp_hdr(skb)->window);

	//协议规定,SYN和SYN+ACK段中是不可以携带窗口扩大因子的,所以这里
	//判断不带SYN标记位时是否需要根据窗口扩大因子调整通告的新窗口大小
	if (likely(!tcp_hdr(skb)->syn))
		nwin <<= tp->rx_opt.snd_wscale;

	if (tcp_may_update_window(tp, ack, ack_seq, nwin)) {
		//需要更新窗口
		flag |= FLAG_WIN_UPDATE;
		//更新snd_wl
		tcp_update_wl(tp, ack, ack_seq);

		if (tp->snd_wnd != nwin) {
			//更新发送窗口
			tp->snd_wnd = nwin;

			/* Note, it is the only place, where
			 * fast path is recovered for sending TCP.
			 */
			//更新了发送窗口大小,需要重新判断是否设置首部预测标记
			tp->pred_flags = 0;
			tcp_fast_path_check(sk);
			//更新已知最大通告窗口
			if (nwin > tp->max_window) {
				tp->max_window = nwin;
				//因为MSS和max_window相关,所以max_window发生了变化,需要重新计算MSS
				tcp_sync_mss(sk, inet_csk(sk)->icsk_pmtu_cookie);
			}
		}
	}
	//更新发送窗口左边界
	tp->snd_una = ack;
	return flag;
}

3.1 发送窗口更新条件

慢速路径下的核心是判断什么时候应该更新发送窗口,这是由tcp_may_update_window()实现的。

/* Check that window update is acceptable.
 * The function assumes that snd_una<=ack<=snd_next.
 */
static inline int tcp_may_update_window(const struct tcp_sock *tp,
					const u32 ack, const u32 ack_seq,
					const u32 nwin)
{
	//cond1: 确认号大于snd_una,说明确认了新数据,可以更新发送窗口左边界;
	//cond2: ACK段的序号大于snd_wl1,说明对方有发送新数据,所以需要更新snd_wl1;
	//cond3: 通告的接收窗口有变化.
	//上面只有有一个条件成立,那么就可以更新发送窗口了(条件2着实没理解...)。
	return (after(ack, tp->snd_una) ||
		after(ack_seq, tp->snd_wl1) ||
		(ack_seq == tp->snd_wl1 && nwin > tp->snd_wnd));
}
TCP协议的数据传输过程中,序列号(`SEQ`)和确认号(`ACK`)是保证数据可靠传输的关键字段。它们的变化机制确保了数据的有序性和完整性。 ### `SEQ`字段的变化机制 - 每个TCP数据段都包含一个序列号(`SEQ`),它表示该数据段中第一个字节在整个数据流中的位置。 - 初始序列号(ISN)是在建立连接时随机生成的,以防止旧网络中残留的数据包干扰新连接。 - 如果发送方发送了一个包含数据数据段,则下一个数据段的`SEQ`值等于当前数据段的`SEQ`值加上该数据段中数据的长度(以字节为单位)。例如,如果一个数据段的`SEQ`是100,并且携带了50字节的数据,则下一个数据段的`SEQ`将是150。 - 对于不携带数据的控制报文(如仅带有`ACK`标志的报文),其序列号不会导致后续数据段的序列号递增。这意味着即使发送了多个仅用于确认的`ACK`报文,也不会改变下一次发送数据时使用的`SEQ`值。 ### `ACK`字段的变化机制 - 确认号(`ACK`)是由接收方返回给发送方的一个值,用来告知发送方期望收到的下一个数据段的第一个字节的序列号。 - 当接收方成功接收到一个数据段后,它会检查该数据段的`SEQ`值,并计算出下一个预期接收数据段的起始位置作为`ACK`值回传给发送方。具体来说,`ACK`值等于接收到的数据段的`SEQ`值加上该数据段中数据的长度。 - 在连接建立阶段,当客户端发送SYN标志位设置的数据段时,它的初始序列号为X;服务器响应SYN-ACK时,其`ACK`字段将设置为X+1,表明它期望下次从客户端收到的序列号是X+1。 - 类似地,服务器也会发送自己的SYN标志位设置的数据段,其中包含它的初始序列号Y;客户端回应的`ACK`字段则设为Y+1,表示它期望下次从服务器收到的序列号是Y+1。 - 对于FIN标志位设置的数据段,同样会导致对方在确认时增加相应的`ACK`值。 通过上述机制,TCP能够实现可靠的数据传输、流量控制以及拥塞控制等功能。这些特性共同作用,使得TCP能够在不可靠的网络环境中提供端到端的可靠性保障[^3]。 ```python # 示例:模拟简单的SEQ和ACK变化逻辑 def simulate_tcp_seq_ack(client_isn, server_isn): # 客户端发送SYN,序列号为client_isn print(f"Client sends SYN with SEQ={client_isn}") # 服务端响应SYN-ACK,序列号为server_isn,确认号为client_isn + 1 print(f"Server responds with SYN-ACK: SEQ={server_isn}, ACK={client_isn + 1}") # 客户端发送ACK,确认号为server_isn + 1 print(f"Client sends ACK: ACK={server_isn + 1}") # 假设客户端发送100字节数据,序列号继续递增 client_data_seq = client_isn + 1 data_length = 100 next_client_seq = client_data_seq + data_length print(f"Client sends data: SEQ={client_data_seq} to {next_client_seq - 1}, Length={data_length}") # 服务端确认收到数据,确认号更新到next_client_seq print(f"Server acknowledges receipt: ACK={next_client_seq}") # 调用函数模拟过程 simulate_tcp_seq_ack(1000, 2000) ``` 以上代码示例展示了如何简单模拟TCP连接建立和服务端与客户端之间数据传输时`SEQ`和`ACK`的变化情况。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值