探讨Linux kernel中对序列号超前的ACK包的处理

  在开发的内核模块中遇到这样一个问题:一个数据包有多个请求,每次只让服务器处理一个请求,所以在将请求交到上层的时候需要拆包,只将部分数据交到上层。为了防止客户端重传数据包,要预先给客户端发送一个对完整数据包的确认。这样就会造成一个问题,客户端发送的ACK包的序列号,会比协议栈中期望的序列号大。

  假设完整数据包的起始序列号分别为1883458390、1883458821,上层协议栈拿到的数据包的起始序列号为1883458390、1883458476,这时服务器端sock结构的rcv_nxt应该为1883458476,但是由于我们事先多发送了一个ACK,所以这时客户端的ACK包的序列号为1883458821,而不是1883458476。下面是抓包的部分截图,根据抓包情况来看,这样的ACK包,内核接受了这样的确认包,如图所示(客户端:192.168.9.188;服务器端:192.168.9.191):


OK,现在我们开始来看看内核中是如何处理这样的数据包,为什么会接受这样的ACK包。

  TCP协议的接收函数为tcp_v4_rcv(),该函数SKB进行必要的检查,如数据的长度、校验和初始化等,初始化TCP控制块中的值;接着会调用__inet_lookup_skb()函数在tcp_hashinfo散列表中来查找是否存在对应的传输控制块。如果找到,则调用tcp_v4_do_rcv()来处理(这里我们忽略了Netfilter、IPSec、DMA等细节,这些跟我们探讨的内容无关),基本的代码流程如下图所示:

探讨Linux kernel中对序列号超前的ACK包的处理_第1张图片

  我们的ACK包的校验和、首部长度是没有问题,所以它可以轻松的通过tcp_v4_rcv()的检查,接下来看tcp_v4_do_rcv()中处理。tcp_v4_do_rcv()中会首先检查SKB包对应的sock结构的状态,如果是ESTABLISHED状态,则会调用tcp_rcv_established()来处理,代码片段如下:

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    ......

	if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
		TCP_CHECK_TIMER(sk);
		if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
			rsk = sk;
			goto reset;
		}
		TCP_CHECK_TIMER(sk);
		return 0;
	}
	
	......
}
  我们的ACK包是在ESTABLISHED状态下发送的,所以接下来的处理在tcp_rcv_established()中进行。在tcp_rcv_established()中分快速路径和慢速路径,只有在满足一定的条件后才能执行快速路径,其中一个必须判断条件是“TCP_SKB_CB(skb)->seq == tp->rcv_nxt”。由于我们的ACK包的序列号不等于rcv_nxt(即下一个要接收的数据包的序列号),所以我们的SKB包会在慢速路径中执行。tcp_rcv_established()中关键代码如下:

int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
			struct tcphdr *th, unsigned len)
{
	......
	
	if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
	    TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
	    !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
		......
	}

slow_path:
	if (len < (th->doff << 2) || tcp_checksum_complete_user(sk, skb))
		goto csum_error;

	/*
	 *	Standard slow path.
	 */

	res = tcp_validate_incoming(sk, skb, th, 1);
	if (res <= 0)
		return -res;

step5:
	if (th->ack && tcp_ack(sk, skb, FLAG_SLOWPATH) < 0)
		goto discard;

	tcp_rcv_rtt_measure_ts(sk, skb);

	tcp_urg(sk, skb, th);

	/* step 7: process the segment text */
	tcp_data_queue(sk, skb);

	tcp_data_snd_check(sk);
	tcp_ack_snd_check(sk);
	return 0;

csum_error:
	TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);

discard:
	__kfree_skb(skb);
	return 0;
}
  在调用tcp_checksum_complete_user()对SKB包进行校验(我们的ACK包校验和没有问题)后,会调用tcp_validate_incoming()来检查SKB包是否是有效的数据包。tcp_validate_incoming()中主要有以下几种检查:

  ①调用tcp_sequence()检查数据包的序列号是否在接收窗口内

  ②检查RST位是否设置

  ③安全和优先级检查(这个现在已经被忽略)

  ④检查数据包是否是接收窗口内的SYN包

  不难看出,和我们探讨的内容相关的是第二项检查,现在就要看ACK包的序列号是否是在接收窗口内。接收窗口的大小和延迟时间、带宽有关,除非是在极端恶劣的网络情况下,我们的ACK包都会在接收窗口内,所以在这里我们可以认为我们的ACK包能通过tcp_validate_incoming()的检查。
  我们再回到tcp_rcv_established()函数中,在tcp_validate_incoming()检查后,会执行到step5标签处,在这里会调用tcp_ack()来处理我们的ACK包,在调用tcp_ack()处理之后的一些处理可以认为和我们的ACK包无关,不影响最终的结果。也就是说,只要在tcp_ack()中能被内核接受的话,我们的ACK包就是“合法的”数据包,内核就会认为它是一个正确的确认包。所以之后我们只关注tcp_ack()中的处理,不会再回到tcp_rcv_established()中。
  在tcp_ack()中主要的检查是确认的序列号是否在SND.UNA和SND.NXT之间,并没有对在这些检查之后,就开始使用确认的序列号更新内核中的相关结构。我们的ACK包的确认序列号是完全正确的,有问题的是序列号,因此在tcp_ack()中我们的ACK包是可以被接收的。

  通过上面的过程,可以看到我们的ACK包之所以能够被内核接收的关键就是它通过了序列号是否在接收窗口内的检查,而且之后对ACK包的处理中完全没有再对序列号进行检查过,因此我们的有问题的ACK包就在夹缝中通过了内核的检查。

tcp_ack()中具体的处理不多说了,有兴趣的可以看下面的注释:

/* This routine deals with incoming acks, but not outgoing ones. */
/*
 * tcp_ack()用于处理接收到有ACK标志的段,当接到有效的ACK后会更新
 * 发送窗口。
 * @skb: 接收到的ACK段
 * @flag: 标志,取值为FLAG_DATA等
 */
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);
	u32 prior_snd_una = tp->snd_una;
	u32 ack_seq = TCP_SKB_CB(skb)->seq;
	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 older than previous acks
	 * then we can probably ignore it.
	 */
	/*
	 * 检验确认的序号是否落在SND.UNA和SND.NXT之间,否则
	 * 是不合法的序号。
	 * 如果确认的序号在SND.NXT的右边,则说明该序号的数据
	 * 发送方还没有发送,直接返回。
	 * 如果确认的序号在SND.UNA的左边,则说明已经接受过
	 * 该序号的ACK了。因为每个有负载的TCP段都会顺便
	 * 携带一个ACK序号,即使这个序号已经确认过。因此
	 * 如果是一个重复的ACK就无需作处理直接返回即可。但
	 * 如果段中带有SACK选项,则需对此进行处理
	 */
	if (before(ack, prior_snd_una))
		goto old_ack;

	/* If the ack includes data we haven't sent yet, discard
	 * this segment (RFC793 Section 3.9).
	 */
	if (after(ack, tp->snd_nxt))
		goto invalid_ack;

	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);

	/*
	 * 进行更新发送窗口等操作,并根据各种信息获取ACK的
	 * 各种标志.
	 */
	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执行的是快速路径,则更新发送窗口的左边界,
		 * 添加FLAG_WIN_UPDATE标记,同时通知拥塞控制算法模块
		 * 本次ACK是快速路径,如有必要,就作相应的处理
		 */
		tcp_update_wl(tp, ack_seq);
		tp->snd_una = ack;
		flag |= FLAG_WIN_UPDATE;

		tcp_ca_event(sk, CA_EVENT_FAST_ACK);

		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPACKS);
	/*
	 * 如果接收ACK执行的是慢速路径,首先判断ACK段中是否有
	 * 数据负载,如果有,则添加FLAG_DATA标记.
	 */
	} else {
		if (ack_seq != TCP_SKB_CB(skb)->end_seq)
			flag |= FLAG_DATA;
		else
			NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPPUREACKS);

		/*
		 * 更新发送窗口,同时添加更新发送窗口后获取的标记.
		 */
		flag |= tcp_ack_update_window(sk, skb, ack, ack_seq);

		/*
		 * 如果接收的段中存在SACK选项,则调用tcp_sacktag_write_queue()
		 * 标记重传队列.
		 */
		if (TCP_SKB_CB(skb)->sacked)
			flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una);

		/*
		 * 检测ACK端中是否存在ECE标志,如果有,则添加FLAG_ECE标志.
		 */
		if (TCP_ECN_rcv_ecn_echo(tp, tcp_hdr(skb)))
			flag |= FLAG_ECE;

		/*
		 * 最后通知拥塞控制算法模块本次ACK是慢速路径,如有必要,
		 * 则做相应的处理.
		 */
		tcp_ca_event(sk, CA_EVENT_SLOW_ACK);
	}

	/* We passed data and got it acked, remove any soft error
	 * log. Something worked...
	 */
	sk->sk_err_soft = 0;
	icsk->icsk_probes_out = 0;
	/*
	 * 设置最近一次收到ACK段的时间
	 */
	tp->rcv_tstamp = tcp_time_stamp;
	/*
	 * 检测是否有已发送但未确认的段,如果没有则跳转到
	 * no_queue处理
	 */
	prior_packets = tp->packets_out;
	if (!prior_packets)
		goto no_queue;

	/* See if we can take anything off of the retransmit queue. */
	/*
	 * 在重传队列中删除删除已确认的段。
	 */
	flag |= tcp_clean_rtx_queue(sk, prior_fackets, prior_snd_una);

	/*
	 * 如果在重传超时后使用FRTO算法,则调用tcp_process_frto()
	 * 进行处理。
	 */
	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;

	/*
	 * 根据ACK的明确与否,更新拥塞窗口,进行
	 * 拥塞控制。
	 * 对于判断ACK的明确与否,只要满足如下条件中的任何一项便
	 * 视为ACK为不明确
	 * 1)接收到的ACK是重复的
	 * 2)接收到SACK块或显式拥塞通知
	 * 3)当前拥塞状态不为Open。
	 * 
	 * tcp_ack()处理接收到的段时,会检测ACK。如果接收的ACK是不明确
	 * 的或拥塞状态为Open状态,则进行拥塞状态机状态的迁移。如果
	 * ACK确认了新的段同时拥塞窗口可以更新,则进行拥塞避免,
	 * 更新拥塞窗口。
	 */
	if (tcp_ack_is_dubious(sk, flag)) {
		/* Advance CWND, if state allows this. */
		/*
		 * ACK是不明确的,说明接收的ACK不明确或在Open拥塞
		 * 状态。如果ACK确认了新的段且拥塞窗口可以更新,
		 * 则更新拥塞窗口,迁移拥塞状态。
		 */
		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 {
		/*
		 * ACK是明确的,说明拥塞状态至少在Open状态,如果
		 * ACK确认了新的段,则更新拥塞窗口
		 */
		if ((flag & FLAG_DATA_ACKED) && !frto_cwnd)
			tcp_cong_avoid(sk, ack, prior_in_flight);
	}

	/*
	 * 如果ACK确认新的段(包括新的数据、SYN段,以及接收到新的SACK选项),
	 * 或者接收到的ACK是重复的,则确认该传输控制块的输出路由缓存项
	 * 是有效的。
	 */
	if ((flag & FLAG_FORWARD_PROGRESS) || !(flag & FLAG_NOT_DUP))
		dst_confirm(sk->sk_dst_cache);

	return 1;

no_queue:
	/* 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.
	 */
	/*
	 * 如果还有待发送的数据,则需根据情况确定是否进行零窗口探测。
	 * 接收到ACK,如果对方的接收窗口没有关闭,需清除持续定时器中
	 * 的指数退避算法指数,停止持续定时器,否则开启持续定时器。
	 * tcp_ack_probe()用来确定师傅需要进行零窗口探测。
	 * 如果接收方的接收窗口已经打开,且足够接收发送方的一个完整的
	 * 段,则暂时不需要零窗口探测,并停止零窗口探测定时器。否则
	 * 需进行零窗口的探测,并重新复位零窗口探测定时器。
	 */
	if (tcp_send_head(sk))
		tcp_ack_probe(sk);
	return 1;

invalid_ack:
	SOCK_DEBUG(sk, "Ack %u after %u:%u\n", ack, tp->snd_una, tp->snd_nxt);
	return -1;

old_ack:
	/*
	 * 如果是已确认的ACK,且其中带有SACK选项信息,则需标记重传队列中
	 * 各个段的记分牌。
	 */
	if (TCP_SKB_CB(skb)->sacked) {
		tcp_sacktag_write_queue(sk, skb, prior_snd_una);
		if (icsk->icsk_ca_state == TCP_CA_Open)
			tcp_try_keep_open(sk);
	}

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

你可能感兴趣的:(探讨Linux kernel中对序列号超前的ACK包的处理)