TCP选项之SACK选项的接收(一)

这篇博客探讨了TCP接收方如何处理SACK(选择性确认)选项。内容涉及tcp_paser_options()解析SACK信息,tcp_sacktag_write_queue()更新发送队列的记分牌,以及tcp_maybe_skipping_dsack()和tcp_highest_sack_seq()的功能。重点在于理解SACK确认的段处理逻辑和发送队列的维护。
摘要由CSDN通过智能技术生成

这篇笔记开始记录SACK选项的接收部分处理逻辑,这部分内容较多,会分成几篇来介绍。

接收方对SACK信息的处理

收到ACK段后,首先会用tcp_paser_options()解析输入段携带的选项信息,在该函数中,如果包含了SACK信息,那么skb的控制块的sacked字段就记录了SACK信息距TCP首部的偏移量,如果skb中没有SACK信息,则sacked字段为0.

之后,在tcp_ack()的慢速路径处理过程中,调用tcp_sacktag_write_queue()将发送队列中的已发送段打标签(就是资料中所说的更新记分牌),即标识其是否已经被确认过了。之所以快速路径无需该过程,是因为快速路径不可能收到SACK信息。

  • sacked_out:记录的是[snd_una, snd_nxt)之间被SACK确认的段的个数;
  • highest_sack:记录的是被SACK确认的skb的最大序号;
  • fackets_out:

要特别注意的是,在调用该函数前,发送窗口字段snd_una已经更新过了,传入的参数prior_snd_una是更新之前的值。

static int
tcp_sacktag_write_queue(struct sock *sk, struct sk_buff *ack_skb, u32 prior_snd_una)
{
	const struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	//ptr指向的就是SACK选项的起始位置
	unsigned char *ptr = (skb_transport_header(ack_skb) + TCP_SKB_CB(ack_skb)->sacked);
	//sp_wire在ptr的基础上前移两个字节,跳过选项的kind和len,指向SACK信息块的开始
	struct tcp_sack_block_wire *sp_wire = (struct tcp_sack_block_wire *)(ptr+2);
	//用于保存最后解析出来的SACK信息块,因为一个skb中最多可以携带4个,所以数组长度为4
	struct tcp_sack_block sp[4];
	struct tcp_sack_block *cache;
	struct sk_buff *skb;
	//每个SACK信息块为8字节,所以num_sacks记录的是该skb中携带的SACK信息块的个数
	int num_sacks = (ptr[1] - TCPOLEN_SACK_BASE) >> 3;
	int used_sacks;
	int reord = tp->packets_out;
	int flag = 0;
	//记录本skb中重复的SACK信息块的个数
	int found_dup_sack = 0;
	int fack_count;
	int i, j;
	int first_sack_index;

	//如果之前还没有SACK确认过的段,那么复位highest_sack域为发送队列的头部,即当前发送队列中
	//最大的序号(该数据可能还尚未发送),这是合理的,因为毕竟当前没有SACK信息嘛
	if (!tp->sacked_out) {
		if (WARN_ON(tp->fackets_out))
			tp->fackets_out = 0;
		tcp_highest_sack_reset(sk);
	}
	//检查是否有DSACK信息块,如果有,设置FLAG_DSACKING_ACK标记,表示输入段中有DSACK
	found_dup_sack = tcp_check_dsack(tp, ack_skb, sp_wire,
					 num_sacks, prior_snd_una);
	if (found_dup_sack)
		flag |= FLAG_DSACKING_ACK;

	/* Eliminate too old ACKs, but take into
	 * account more or less fresh ones, they can
	 * contain valid SACK info.
	 */
	//检查ACK序号是否太老了,如果太老,认为是异常段,不再继续处理
	if (before(TCP_SKB_CB(ack_skb)->ack_seq, prior_snd_una - tp->max_window))
		return 0;
	//当前根本就没有需要确认的段,所以也没有必要继续处理
	if (!tp->packets_out)
		goto out;

	//遍历输入段携带的SACK信息块,将解析出来的SACK信息块记录到sp[]中;

	//used_sacks记录了其中有效的SACK信息块个数;
	used_sacks = 0;
	//后面会对sp[]数组按照SACK信息块中seq的大小做升序排列,first_sack_index
	//记录了排序后原本输入段携带的第一个SACK信息块在sp[]中的位置,如果第一个
	//SACK块无效,那么最终first_sack_index为-1
	first_sack_index = 0;
	for (i = 0; i < num_sacks; i++) {
		//如果有DSACK,那么它一定在第一个位置。综合前面的判断设置dup_sack
		int dup_sack = !i && found_dup_sack;
		//保存TCP首部的SACK选项块到sp数组中,主机字节序
		sp[used_sacks].start_seq = ntohl(get_unaligned(&sp_wire[i].start_seq));
		sp[used_sacks].end_seq = ntohl(get_unaligned(&sp_wire[i].end_seq));

		//如果SACK信息块无效,分别进行相关统计
		if (!tcp_is_sackblock_valid(tp, dup_sack,
					    sp[used_sacks].start_seq,
					    sp[used_sacks].end_seq)) {
			if (dup_sack) {
				if (!tp->undo_marker)
					NET_INC_STATS_BH(LINUX_MIB_TCPDSACKIGNOREDNOUNDO);
				else
					NET_INC_STATS_BH(LINUX_MIB_TCPDSACKIGNOREDOLD);
			} else {
				/* Don't count olds caused by ACK reordering */
				if ((TCP_SKB_CB(ack_skb)->ack_seq != tp->snd_una) &&
				    !after(sp[used_sacks].end_seq, tp->snd_una))
					continue;
				NET_INC_STATS_BH(LINUX_MIB_TCPSACKDISCARD);
			}
			//第一个SACK是无效的SACK,所以设置first_sack_index为-1
			if (i == 0)
				first_sack_index = -1;
			continue;
		}

		//SACK信息块的确认范围在[prior_snd_una, snd_nxt)区间的左侧,
		//即确认了已经被完全确认的数据,忽略这种SACK块
		if (!after(sp[used_sacks].end_seq, prior_snd_una))
			continue;
		//有效的SACK块,计数器加1
		used_sacks++;
	}

	//冒泡法将sp[]数组中的SACK块按照start_seq升序排列
	for (i = used_sacks - 1; i > 0; i--) {
		for (j = 0; j < i; j++) {
			if (after(sp[j].start_seq, sp[j + 1].start_seq)) {
				struct tcp_sack_block tmp;

				tmp = sp[j];
				sp[j] = sp[j + 1];
				sp[j + 1] = tmp;

				/* Track where the first SACK block goes to */
				//注意first_sack_index会跟踪原始的第一个SACK信息块所在位置
				if (j == first_sack_index)
					first_sack_index = j + 1;
			}
		}
	}

	//skb指向发送队列的首部,准备根据SACK信息块标记发送队列中的skb
	skb = tcp_write_queue_head(sk);
	fack_count = 0;
	i = 0;

	//一旦收到对端的SACK信息,那么说明发生了丢包或者乱序,而且这种状况可能往往无法
	//立即恢复,这意味着发送方会连续多次收到SACK信息,而且这些SACK信息很可能是重
	//复的。为了减少对发送队列的遍历次数,这里发送方在用SACK信息块更新发送队列时采
	//用了cache机制。本质上也很简单,就是将上一次的SACK信息块保存下来,在本次处理
	//过程中,如果发现上一次已经处理过了该范围的SACK,那么就可以跳过不处理。cache
	//信息块就保存在tp->recv_sack_cache[]中。下面初始化cache指针

	//如果之前没有SACK确认过的数据,那么cache一定是空的,初始化cache指向
	//recv_sack_cache的末尾,表示没有可用的cahce信息
	if (!tp->sacked_out) {
		/* It's already past, so skip checking against it */
		cache = tp->recv_sack_cache + ARRAY_SIZE(tp->recv_sack_cache);
	} else {
		cache = tp->recv_sack_cache;
		//如果上次只收到了3个信息块,那么recv_sack_cache数组是不满的,上次的SACK
		//信息块保存在了数组的末尾三个位置,所以这里跳过开头的无效SACK信息
		while (tcp_sack_cache_ok(tp, cache) && !cache->start_seq && !cache->end_seq)
			cache++;
	}

	//遍历SACK信息块,更新发送队列中各个skb的记分牌
	while (i < used_sacks) {
		u32 start_seq = sp[i].start_seq;
		u32 end_seq = sp[i].end_seq;
		//标识当前遍历的块是否是DSACK块
		int dup_sack = (found_dup_sack && (i == first_sack_index));
		struct tcp_sack_block *next_dup = NULL;

		//如果下一个块是DSACK块,那么netx_dup指向该DSACK信息块
		if (found_dup_sack && ((i + 1) == first_sack_index))
			next_dup = &sp[i + 1];

		/* Event "B" in the comment above. */
		// high_seq是进入Recovery或Loss时的snd_nxt,如果high_seq被SACK了,
		//那么很可能有数据包丢失了,不然就可以ACK掉high_seq返回Open态了。
		if (after(end_seq, tp->high_seq))
			flag |= FLAG_DATA_LOST;

		//下面的逻辑首先是检查cache和SACK信息块是否有交集,
		//如果有,那么就可以跳过当前SACK块,提高效率

		/* Skip too early cached blocks */
		//如果cache的区间为[100, 200), 而当前SACK信息块的区间为[300, 400),
		//这种cache块已经没有意义了,直接跳过这些cache块
		while (tcp_sack_cache_ok(tp, cache) &&
		       !before(start_seq, cache->end_seq))
			cache++;
		/* Can skip some work by looking recv_sack_cache? */
		//cache和SACK块一定是有交集的
		if (tcp_sack_cache_ok(tp, cache) && !dup_sack &&
		    after(end_seq, cache->start_seq)) {

			//满足该条件,那么一定属于这两种情况:
			//1. cache[100, 300), SACK[50, 400)
			//2. cache[100, 300), SACK[50, 200)
			//这两种情况的共性就是从[50, 100)这一段需要被处理
			if (before(start_seq, cache->start_seq)) {
				//前移发送队列,使得遍历指针知道seq为100的的地方
				skb = tcp_sacktag_skip(skb, sk, start_seq, &fack_count);
				//用[50, 100)即[start_seq, cache->start_seq)标记发送队列
				skb = tcp_sacktag_walk(skb, sk, next_dup,
						       start_seq,
						       cache->start_seq,
						       dup_sack, &fack_count,
						       &reord, &flag);
			}

			/* Rest of the block already fully processed? */
			//如果属于上面的第二种情况,那么SACK信息块的后半段已经全部被cache包含,
			//这部分已经标记过了,没必要重新标记,所以继续处理下一个SACK信息块
			if (!after(end_seq, cache->end_seq))
				goto advance_sp;

			//下面处理上面的第一种情况,这种情况下可以跳过这个cache,但是不能跳过
			//SACK,因为它的后半段需要继续和下一个cache比较

			//next_dup不为NULL的唯一一种情况就是输入段的DSACK信息块表示的确认范围被
			//后面的SACK信息块完全包裹,比如DSACK为[100, 200), 后面SACK为[50, 300),
			//只有这种情况,排序后,DSACK块才能排在SACK之后,这样next_dup才不为NULL。

			//这种场景下,有可能DSACK和cache也有一部分重合
			skb = tcp_maybe_skipping_dsack(skb, sk, next_dup,
						       cache->end_seq,
						       &fack_count, &reord,
						       &flag);

			/* ...tail remains todo... */
			//这里,真的是没有理解为什么要这么做???
			if (tcp_highest_sack_seq(tp) == cache->end_seq) {
				/* ...but better entrypoint exists! */
				skb = tcp_highest_sack(sk);
				if (skb == NULL)
					break;
				fack_count = tp->fackets_out;
				cache++;
				goto walk;
			}
			//跳过发送队列中那些序号小于cache->end_seq的skb,它们已经被标记过了
			skb = tcp_sacktag_skip(skb, sk, cache->end_seq, &fack_count);
			/* Check overlap against next cached too (past this one already) */
			cache++;
			continue;
		}
		//这个SACK信息块和cache块没有重叠,并且其start_seq一定大于所有的cache块的end_seq

		if (!before(start_seq, tcp_highest_sack_seq(tp))) {
			skb = tcp_highest_sack(sk);
			if (skb == NULL)
				break;
			fack_count = tp->fackets_out;
		}
		//跳过发送队列中那些序号小于start_seq的段
		skb = tcp_sacktag_skip(skb, sk, start_seq, &fack_count);

walk:
		//更新序号位于[start_seq,end_seq)之间的skb的记分牌
		skb = tcp_sacktag_walk(skb, sk, next_dup, start_seq, end_seq,
				       dup_sack, &fack_count, &reord, &flag);

advance_sp:
		/* SACK enhanced FRTO (RFC4138, Appendix B): Clearing correct
		 * due to in-order walk
		 */
		if (after(end_seq, tp->frto_highmark))
			flag &= ~FLAG_ONLY_ORIG_SACKED;
		//当前SACK块处理完毕,继续处理下一个
		i++;
	}

	//更新cache,cache的大小同样是4个,并且每次收到SACK,上一次的cache内容都会清除,
	//然后将本次接收的SACK块排序后的结果保存在cache中
	for (i = 0; i < ARRAY_SIZE(tp->recv_sack_cache) - used_sacks; i++) {
		tp->recv_sack_cache[i].start_seq = 0;
		tp->recv_sack_cache[i].end_seq = 0;
	}
	for (j = 0; j < used_sacks; j++)
		tp->recv_sack_cache[i++] = sp[j];

	//标记重传
	tcp_mark_lost_retrans(sk);
	//更新计数器
	tcp_verify_left_out(tp);
	//更新乱序信息
	if ((reord < tp->fackets_out) &&
	    ((icsk->icsk_ca_state != TCP_CA_Loss) || tp->undo_marker) &&
	    (!tp->frto_highmark || after(tp->snd_una, tp->frto_highmark)))
		tcp_update_reordering(sk, tp->fackets_out - reord, 0);

out:
	return flag;
}

小结一下,该函数的核心逻辑如下:

  1. 解析输入段中携带的SACK信息块,包括DSACK判断以及合法性判断;
  2. 结合上一次的SACK信息块cache信息更新发送队列中skb的记分牌;
  3. 标记需要重传的段,更新乱序信息;

tcp_maybe_skipping_dsack()

如上,该函数被用来跳过可能的DSACK信息块。

static struct sk_buff *tcp_maybe_skipping_dsack(struct sk_buff *skb,
						struct sock *sk,
						struct tcp_sack_block *next_dup,
						u32 skip_to_seq,
						int *fack_count, int *reord,
						int *flag)
{
	//没有DSACK信息块
	if (next_dup == NULL)
		return skb;
	//跳过可能的重合部分。这里也有可能next_dup->end_seq <= skip_to_seq,
	//这种情况tcp_sacktag_walk()将什么都不会做
	if (before(next_dup->start_seq, skip_to_seq)) {
		skb = tcp_sacktag_skip(skb, sk, next_dup->start_seq, fack_count);
		tcp_sacktag_walk(skb, sk, NULL,
				 next_dup->start_seq, next_dup->end_seq,
				 1, fack_count, reord, flag);
	}

	return skb;
}

tcp_highest_sack_seq()

/* Start sequence of the highest skb with SACKed bit, valid only if
 * sacked > 0 or when the caller has ensured validity by itself.
 */
static inline u32 tcp_highest_sack_seq(struct tcp_sock *tp)
{
	if (!tp->sacked_out)
		return tp->snd_una;
	//sacked_out>0,怎么会出现highest_sack为NULL的场景???
	if (tp->highest_sack == NULL)
		return tp->snd_nxt;

	return TCP_SKB_CB(tp->highest_sack)->seq;
}

//当sacked_out统计时,复位highest_sack的指向为发送队列的首部,因为此时我们没有任何的SACK信息,
//最大的已被SACK确认的skb的指针指向队首,这种设置也很奇怪,因为对首skb毕竟还没有被SACK
static inline void tcp_highest_sack_reset(struct sock *sk)
{
	tcp_sk(sk)->highest_sack = tcp_write_queue_head(sk);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值