linux内核协议栈 TCP选项之SACK选项的接收Ⅱ

本文深入解析Linux内核协议栈中TCP的SACK选项接收,关注`tcp_sacktag_skip()`和`tcp_sacktag_walk()`两个核心函数在更新记分牌过程中的作用。通过`tcp_match_skb_to_sack()`和`tcp_sacktag_one()`辅助函数,了解如何判断和更新SACK确认的数据包状态,特别是记分牌机制的细节,包括S、R、L三种标记及其组合。
摘要由CSDN通过智能技术生成

Table of Contents

1 tcp_sacktag_skip()

2 tcp_sacktag_walk()

2.1 tcp_match_skb_to_sack()

2.2 tcp_sacktag_one()

2.2.1 记分牌

2.2.2 更新记分牌


《linux内核协议栈 TCP选项之SACK选项的接收Ⅰ》中有看到,对于每个SACK信息块,在和cache信息比较后,都会有两个核心操作来更新记分牌:

  1. tcp_sacktag_skip()
  2. tcp_sacktag_walk()

1 tcp_sacktag_skip()

参数skb指向发送队列中的某个skb,该函数从skb开始向后遍历发送队列,直到遍历到sk->sk_send_head(即下一个要发送的skb,可能为NULL)或者skb的末尾序号(假设序号范围为[seq1, end1))end1大于等于参数skip_to_seq为止,返回终止时的skb指针。

简而言之,该函数的作用就是跳过那些序号小于SACK信息块确认范围的skb。

@skb:从该skb开始向后遍历,skb必须指向发送队列中的某个skb;
@skip_to_seq:目标值;
@fack_count:输出参数,将遍历过程中序号小于SACK信息块确认范围的skb的段数累加到该变量中,FACK算法需要该参数
static struct sk_buff *tcp_sacktag_skip(struct sk_buff *skb, struct sock *sk,
					u32 skip_to_seq, int *fack_count)
{
	tcp_for_write_queue_from(skb, sk) {
		//遍历到发送队列末尾时停止,这种情况下返回sk->sk_send_head,
		//即下一个要发送的skb指针,可能为NULL
		if (skb == tcp_send_head(sk))
			break;
		//end_seq >= skip_to_seq,说明当前skb和SACK信息块的确认范围有交集,停止遍历
		if (!before(TCP_SKB_CB(skb)->end_seq, skip_to_seq))
			break;
		//skb序号小于SACK信息块确认范围,将其段数累加到输出参数fack_cout中
		*fack_count += tcp_skb_pcount(skb);
	}
	return skb;
}

2 tcp_sacktag_walk()

该函数从参数skb指向的位置开始向后遍历发送队列,更新序号位于[start_seq,end_seq)之间的skb的记分牌。

@skb:从该skb开始向后遍历,skb指向的是发送队列中的某个skb;
@next_dup: 如果不为NULL,指向DSACK的信息块,表示下一个SACK块就是DSACK;
@start_seq, end_seq:SACK信息块确认范围;
@dup_sack_in:[start_seq, end_seq)表示的范围是否就是DSACK信息块
static struct sk_buff *tcp_sacktag_walk(struct sk_buff *skb, struct sock *sk,
					struct tcp_sack_block *next_dup,
					u32 start_seq, u32 end_seq,
					int dup_sack_in, int *fack_count,
					int *reord, int *flag)
{
	//从skb开始向后遍历发送队列
	tcp_for_write_queue_from(skb, sk) {
		int in_sack = 0;
		int dup_sack = dup_sack_in;
		//已经遍历到了tp->sk_send_head,后面的skb还没有发送,结束处理
		if (skb == tcp_send_head(sk))
			break;

		//因为发送队列中skb的序号是递增的,而当前skb的seq大于等于
		//SACK块的末尾序号,说明该skb和SACK信息块已经没有交集了,
		//而且发送队列后续skb也不会和该SACK信息块有交集了,结束处理
		if (!before(TCP_SKB_CB(skb)->seq, end_seq))
			break;

		//假设用序号dseq和dend表示DSACK信息块的序号,DSACK有两种情形:
		//1)dend<=snd_una,即DSACK是由已经ACK的数据段触发的;
		//2)dend>sud_una并且DSACK信息块的序号被后面的SACK信息块包含;
		//情形1根本就无需更新记分牌,所以如果next_dup不为NULL,那么一定
		//是情形2。此时,由于DSACK被SACK包围,所以一定是先处理SACK
		//然后再处理DSACK,此时为了避免重复遍历发送队列,所以需要同时处理这
		//两个SACK信息块。

		//下一个是DSACK块,需要先处理DSACK块。如果dend>=skb->seq,
		//说明DSACK块和当前skb可能有交集
		if ((next_dup != NULL) &&
		    before(TCP_SKB_CB(skb)->seq, next_dup->end_seq)) {
			//传入的序号范围是DSACK的序号范围。函数作用见下文
			in_sack = tcp_match_skb_to_sack(sk, skb,
							next_dup->start_seq,
							next_dup->end_seq);
			//返回大于0,说明skb数据是可以被确认的,又因为
			//这是由于DSACK确认的,所以设置dup_sack为1
			if (in_sack > 0)
				dup_sack = 1;
		}
		//1)in_sack==0:压根就没有DSACK;
		//2)DSACK无法确认skb;
		//3)in_sack<0:DSACK导致了skb分片,但是分片失败了
		if (in_sack <= 0)
			//对于情形1,没得说,这里用SACK检测。情形2和3也一样是因为SACK是DSACK的超集
			in_sack = tcp_match_skb_to_sack(sk, skb, start_seq,
							end_seq);
		//结果小于0,说明skb分割失败,处理结束
		if (unlikely(in_sack < 0))
			break;
		//如果最终检测到skb数据可以被确认,则标记该skb
		if (in_sack)
			*flag |= tcp_sacktag_one(skb, sk, reord, dup_sack, *fack_count);

		*fack_count += tcp_skb_pcount(skb);
	}
	return skb;
}

2.1 tcp_match_skb_to_sack()

辅助函数tcp_match_skb_to_sack()用于判断skb从其开始部分是否有数据可以被SACK确认。该函数还涉及skb切割操作,所以有可能会失败,调用者需要处理出错场景。

注意:该函数有个很重要的前提,就是skb的起始序号必须要小于SACK的末尾序号,即调用前要保证二者的区间有交集,否则调用该函数没有任何意义。

/* Check if skb is fully within the SACK block. In presence of GSO skbs,
 * the incoming SACK may not exactly match but we can find smaller MSS
 * aligned portion of it that matches. Therefore we might need to fragment
 * which may fail and creates some hassle (caller must handle error case
 * returns).
 */
static int tcp_match_skb_to_sack(struct sock *sk, struct sk_buff *skb,
				 u32 start_seq, u32 end_seq)
{
	int in_sack, err;
	unsigned int pkt_len;

	//cond1:start_seq <= skb->seq;
	//cond2:end_seq >= skb->seq;
	//显然,条件1和条件2同时成立,说明skb的序号完全落在了SACK信息块的范围内,
	//比如SACK:[1000,1500),skb:[1100, 1300)
	in_sack = !after(start_seq, TCP_SKB_CB(skb)->seq) &&
		  !before(end_seq, TCP_SKB_CB(skb)->end_seq);

	//当前skb有GSO分段,并且SACK信息块只能确认skb的一部分数据,这时可能有
	//两种情况:1)SACK:[1000,1500),skb:[1200, 1700);
	//2)SACK:[1000,1500),skb:[900, 1700)
	if (tcp_skb_pcount(skb) > 1 && !in_sack &&
	    after(TCP_SKB_CB(skb)->end_seq, start_seq)) {
		//in_sack为1表示是情形1,否则为情形2
		in_sack = !after(start_seq, TCP_SKB_CB(skb)->seq);

		if (!in_sack)
			//情形2:skb开始部分不在SACK确认区间内,从这里分割
			pkt_len = start_seq - TCP_SKB_CB(skb)->seq;
		else
			//情形1:skb前半部分再SACK确认区间内,从这里分割
			pkt_len = end_seq - TCP_SKB_CB(skb)->seq;
		//将skb切割成两部分,第一部分长度为pkt_len,剩余部分为第二部分
		err = tcp_fragment(sk, skb, pkt_len, skb_shinfo(skb)->gso_size);
		//切割skb设计内存分配,所以有可能失败
		if (err < 0)
			return err;
	}
	//如果当前skb可以被SACK确认(对于部分数据情形,已经切分处理),返回1,否则返回0
	return in_sack;
}

2.2 tcp_sacktag_one()

一旦确认skb中所有的数据都可以被SACK确认后,就调用tcp_sacktag_one()更新该skb的记分牌。

在分析代码之前,需要先理解记分牌到底是什么。

2.2.1 记分牌

所谓的记分牌,就是TCP_SKB_CB(skb)->sacked字段,该字段可以取如下几种值的组合:

//标识skb是否被SACK确认过,标识为字母S
#define TCPCB_SACKED_ACKED 0x01
//标识skb是否被重传过,标识为字母R
#define TCPCB_SACKED_RETRANS 0x02
//标识skb是否被认定为丢失,标识为字母L
#define TCPCB_LOST 0x04
#define TCPCB_TAGBITS 0x07	/* All tag bits			*/

//如果置位,表示skb被曾经被重传过。当一个skb被重传时,会设置TCPCB_RETRANS(2个bit),
//在收到SACK时,就会清除TCPCB_SACKED_RETRANS,但是会保留TCPCB_EVER_RETRANS
#define TCPCB_EVER_RETRANS		0x80	/* Ever retransmitted frame	*/
#define TCPCB_RETRANS		(TCPCB_SACKED_RETRANS|TCPCB_EVER_RETRANS)

上述三种情况分别用标识S、R、和L表示,它们分别由计数器sacked_out、retrans_out和lost_out统计。

S、R、L三个标记可能有如下6种组合:

标记组合inFlight包数描述
01原始的数据包正在传输(正常情况)
S0原始数据包被SACK确认
L0原始的数据包丢了
R2原始的和重传的包都还在传输
L、R1原始的数据包丢了,重传的包还在传输
S、R1原始的数据包被SACK确认了,重传的还在传输

此外,逻辑上L|R|S的组合也是可能出现的,比如原始的数据包丢了,发生了重传,然后重传包被SACK确认,但是这种情况下链路上传输的数据包为0,和S的情况完全一样,所以代码里面也没有对这两种组合做区分;

L|S的组合是不能非法组合,这表示有-1个包在链路上传输。

2.2.2 更新记分牌

@dup_sack:如果为1,表示skb是被DSACK确认的
static int tcp_sacktag_one(struct sk_buff *skb, struct sock *sk,
			   int *reord, int dup_sack, int fack_count)
{
	struct tcp_sock *tp = tcp_sk(sk);
	//取出skb中原来的记分牌
	u8 sacked = TCP_SKB_CB(skb)->sacked;
	int flag = 0;

	//skb由DSACK标记,并且当前skb之前就重传过
	if (dup_sack && (sacked & TCPCB_RETRANS)) {
		if (after(TCP_SKB_CB(skb)->end_seq, tp->undo_marker))
			tp->undo_retrans--;
		if (sacked & TCPCB_SACKED_ACKED)
			*reord = min(fack_count, *reord);
	}

	//要标记的skb是已经被确认过的数据,该skb马上就要被删除了,所以什么都不需要做,返回
	if (!after(TCP_SKB_CB(skb)->end_seq, tp->snd_una))
		return flag;

	//skb还没有被SACK确认过,进行标记
	if (!(sacked & TCPCB_SACKED_ACKED)) {
		//如果skb被重传过,即记分牌有R标记
		if (sacked & TCPCB_SACKED_RETRANS) {
			//如果之前skb被判定为丢失,即记分牌为L|R,那么现在收到了数据,可以清楚这两个标记;
			//如果之前没有被判定为丢失,即记分牌为R,那么现在只是认为原始数据收到了,重传数据
			//还在链路上传输,这时什么都不用做
			if (sacked & TCPCB_LOST) {
				//清除L和R标记
				TCP_SKB_CB(skb)->sacked &= ~(TCPCB_LOST|TCPCB_SACKED_RETRANS);
				//递减丢包计数器
				tp->lost_out -= tcp_skb_pcount(skb);
				//递减重传计数器
				tp->retrans_out -= tcp_skb_pcount(skb);

				/* clear lost hint */
				tp->retransmit_skb_hint = NULL;
			}
		} else {
			//这里,skb依然有可能被重传过,即可能有TCPCB_EVER_RETRANS标记

			//该skb确实没有被重传过
			if (!(sacked & TCPCB_RETRANS)) {
				/* New sack for not retransmitted frame,
				 * which was in hole. It is reordering.
				 */
				if (before(TCP_SKB_CB(skb)->seq, tcp_highest_sack_seq(tp)))
					*reord = min(fack_count, *reord);

				/* SACK enhanced F-RTO (RFC4138; Appendix B) */
				if (!after(TCP_SKB_CB(skb)->end_seq, tp->frto_highmark))
					flag |= FLAG_ONLY_ORIG_SACKED;
			}
			//如果skb被判定为丢失,那么收到了SACK,可以清除TCPCB_LOST标记
			if (sacked & TCPCB_LOST) {
				TCP_SKB_CB(skb)->sacked &= ~TCPCB_LOST;
				//更新lost_out计数
				tp->lost_out -= tcp_skb_pcount(skb);

				/* clear lost hint */
				tp->retransmit_skb_hint = NULL;
			}
		}
		//核心在这里,设置TCPCB_SACKED_ACKED标记
		TCP_SKB_CB(skb)->sacked |= TCPCB_SACKED_ACKED;
		//更新flag,表示SACK确认了数据
		flag |= FLAG_DATA_SACKED;
		//更新sacked_out计数
		tp->sacked_out += tcp_skb_pcount(skb);

		fack_count += tcp_skb_pcount(skb);

		/* Lost marker hint past SACKed? Tweak RFC3517 cnt */
		if (!tcp_is_fack(tp) && (tp->lost_skb_hint != NULL) &&
		    before(TCP_SKB_CB(skb)->seq,
			   TCP_SKB_CB(tp->lost_skb_hint)->seq))
			tp->lost_cnt_hint += tcp_skb_pcount(skb);

		if (fack_count > tp->fackets_out)
			tp->fackets_out = fack_count;
		//更新tp->highest_sack
		if (!before(TCP_SKB_CB(skb)->seq, tcp_highest_sack_seq(tp)))
			tcp_advance_highest_sack(sk, skb);
	}

	/* D-SACK. We can detect redundant retransmission in S|R and plain R
	 * frames and clear it. undo_retrans is decreased above, L|R frames
	 * are accounted above as well.
	 */
	//收到了对重传数据包的DSACK,说明之前的重传是不必要的
	if (dup_sack && (TCP_SKB_CB(skb)->sacked & TCPCB_SACKED_RETRANS)) {
		//清除重传标记,递减重传计数器
		TCP_SKB_CB(skb)->sacked &= ~TCPCB_SACKED_RETRANS;
		tp->retrans_out -= tcp_skb_pcount(skb);
		tp->retransmit_skb_hint = NULL;
	}

	return flag;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值