TCP选项之SACK选项的发送

本文详细探讨了在TCP协议中,当接收方遇到乱序报文时,如何在双方支持SACK选项的情况下生成SACK信息,并在后续报文中发送这些选项。内容涵盖SACK相关的数据结构,如struct tcp_sock和struct tcp_options_received,以及SACK的慢速路径数据接收过程,包括SACK使能判断、DSACK设置和SACK块的添加。最后,阐述了SACK选项在TCP首部构造中的发送步骤。
摘要由CSDN通过智能技术生成

当接收方收到乱序报文时,如果在TCP握手过程中,双方都表示支持SACK选项,那么就会生成SACK选项信息,并且在下一次报文发送过程中将这些选项发送给发送方,这篇笔记记录了SACK选项的生成过程以及SACK选项的发送过程。

1. 相关数据结构

1.1 struct tcp_sock

TCB结构中有如下字段和SACK选项的发送过程有关:

struct tcp_sock {
...
	//用户保存生成的DSACK块,因为DSACK块只能有一个,所以数组长度为1
	struct tcp_sack_block duplicate_sack[1]; /* D-SACK block */
	//用于保存生成的SACK块,下次发送时,会根据该数组内容构造SACK选项。
	//由于最多可以有4个SACK块,所以数组长度定义为了4
	struct tcp_sack_block selective_acks[4];
...
}

1.2 struct tcp_options_received

TCP的选项结构中保存了一些SACK选项使能信息、以及一些SACK信息块的计数信息,如下:

struct tcp_options_received {
...
	u16 	saw_tstamp : 1,	/* Saw TIMESTAMP on last packet		*/
		tstamp_ok : 1,	/* TIMESTAMP seen on SYN packet		*/
		//如果设置为1,表示下次发送需要填充DSACK块,需要填充的块
		//已经放在了tp->duplicate_sack[0]中
		dsack : 1,	/* D-SACK is scheduled			*/
		wscale_ok : 1,	/* Wscale seen on SYN packet		*/
		//在三次握手过程中,如果双方都支持SACK选项,那么该字段设置为1
		sack_ok : 4,	/* SACK seen on SYN packet		*/
		snd_wscale : 4,	/* Window scaling received from sender	*/
		rcv_wscale : 4;	/* Window scaling to send to receiver	*/
	//下次发送可以携带的SACK选项的个数:
	//nums_sacks+重复SACK(最多1)-时间戳选项(最多占1个),当然最多不能超过4个
	u8	eff_sacks;	/* Size of SACK array to send with next packet */
	//tp->selective_acks[]数组中记录的SACK块的数目
	u8	num_sacks;	/* Number of SACK blocks		*/
...
};

2. 慢速路径数据接收

因为只有慢速路径才有可能会触发SACK选项的产生,所以我们分析tcp_data_queue()中和SACK相关的内容。

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
	struct tcphdr *th = tcp_hdr(skb);
	struct tcp_sock *tp = tcp_sk(sk);
	int eaten = -1;

...
	//如果DSACK还没有被清除,则复位它,因为DSACK同时有且仅有一个信息块
	if (tp->rx_opt.dsack) {
		tp->rx_opt.dsack = 0;
		tp->rx_opt.eff_sacks = min_t(unsigned int, tp->rx_opt.num_sacks,
					     4 - tp->rx_opt.tstamp_ok);
	}

	//收到的数据就是想要接收的数据
	if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
...
		//收到了期望的数据,所以可能已经能够和乱序队列中的数据衔接上了,
		//因此如果当前有SACK块,那么检查是否可以移除它们
		if (tp->rx_opt.num_sacks)
			tcp_sack_remove(tp);
...
		return;
	}
	//收到的完全是个重复段
	if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {
		/* A retransmit, 2nd most common case.  Force an immediate ack. */
		NET_INC_STATS_BH(LINUX_MIB_DELAYEDACKLOST);
		//根据是否支持DSACK设置重复SACK
		tcp_dsack_set(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq);
...
		return;
	}
...
	//输入段的前半部分是已经收到过的数据,后半部分是新数据,即sep < rcv_nxt < end_seq
	if (before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
		//可见,部分段内容重复也会触发重复SACK
		tcp_dsack_set(tp, TCP_SKB_CB(skb)->seq, tp->rcv_nxt);
...
	}
...
	//到这里,说明收到的是一个seq大于rcv_nxt的乱序包,但是要注意,
	//虽然是乱序包,但是这个包同样是有可能已经接收过的
	SOCK_DEBUG(sk, "out of order segment: rcv_next %X seq %X - %X\n",
		   tp->rcv_nxt, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq);
	//如果之前乱序队列是空的,这里需要特别处理下(为了缩小篇幅,下面删除了不相关的代码)
	if (!skb_peek(&tp->out_of_order_queue)) {
		//因为之前乱序队列是空的,所以这里无需考虑过多,直接构造第一个SACK块即可
		if (tcp_is_sack(tp)) {
			tp->rx_opt.num_sacks = 1;
			tp->rx_opt.dsack     = 0;
			tp->rx_opt.eff_sacks = 1;
			tp->selective_acks[0].start_seq = TCP_SKB_CB(skb)->seq;
			tp->selective_acks[0].end_seq = TCP_SKB_CB(skb)->end_seq;
		}
	} else {
		//下面的代码之所以这么复杂,是为了按照序号升序维护乱序队列,这样在后续处理时方便

		//从乱序队列队尾开始寻找插入点
		struct sk_buff *skb1 = tp->out_of_order_queue.prev;
		u32 seq = TCP_SKB_CB(skb)->seq;
		u32 end_seq = TCP_SKB_CB(skb)->end_seq;

		//收到的数据段的开始序号紧接着乱序队列中最后一包的末尾序号.比如乱序队列最后
		//一包的序号为[500,1000),收到的数据段序号为[1000,1200)
		if (seq == TCP_SKB_CB(skb1)->end_seq) {
			__skb_append(skb1, skb, &tp->out_of_order_queue);
			//如果乱序段和selective_acks[0]的末尾序号刚好能衔接,那么处理非常简单,
			//直接更新selective_acks[0].end_seq即可,其它情况需要将仔细更新SACK块
			if (!tp->rx_opt.num_sacks ||
			    tp->selective_acks[0].end_seq != seq)
				goto add_sack;
			tp->selective_acks[0].end_seq = end_seq;
			return;
		}

		//找到升序插入点
		do {
			if (!after(TCP_SKB_CB(skb1)->seq, seq))
				break;
		} while ((skb1 = skb1->prev) !=
			 (struct sk_buff *)&tp->out_of_order_queue);

		//新接收的段skb和前一个段skb1序号有重叠
		if (skb1 != (struct sk_buff *)&tp->out_of_order_queue &&
		    before(seq, TCP_SKB_CB(skb1)->end_seq)) {
			//新收到的段完全是一个重复段
			if (!after(end_seq, TCP_SKB_CB(skb1)->end_seq)) {
				/* All the bits are present. Drop. */
				__kfree_skb(skb);
				//设置DSACK块信息
				tcp_dsack_set(tp, seq, end_seq);
				goto add_sack;
			}
			//部分重叠,同样设置DSACK块信息
			if (after(seq, TCP_SKB_CB(skb1)->seq)) {
				/* Partial overlap. */
				tcp_dsack_set(tp, seq, TCP_SKB_CB(skb1)->end_seq);
			} else {
				//没有重叠,skb1迁移一个节点,为下面的__skb_insert()做准备
				skb1 = skb1->prev;
			}
		}
		__skb_insert(skb, skb1, skb1->next, &tp->out_of_order_queue);

		//检测新插入的段和乱序队列中其之后的段是否有重叠。前面的判断已经保证了新接收段
		//的seq一定是小于后一个段的seq的,所以下面只能是两种情况:
		//1. 新接收段[1000, 1200),后一个段[1100, 1500)-----部分重叠
		//2. 新接收段[1000, 1200),后一个段[1050, 1100),再后一个段[1150, 1300)----完全重叠
		while ((skb1 = skb->next) !=
		       (struct sk_buff *)&tp->out_of_order_queue &&
		       after(end_seq, TCP_SKB_CB(skb1)->seq)) {
			//部分重叠
			if (before(end_seq, TCP_SKB_CB(skb1)->end_seq)) {
				//重新设定DSACK信息
				tcp_dsack_extend(tp, TCP_SKB_CB(skb1)->seq, end_seq);
				//因为是部分重叠,所以不需要继续检查了
				break;
			}
			//新接收的段已经完全包含了后一个段,所以可以将后一个段从乱序队列中删除了
			__skb_unlink(skb1, &tp->out_of_order_queue);
			//重新设定DSACK信息
			tcp_dsack_extend(tp, TCP_SKB_CB(skb1)->seq,
					 TCP_SKB_CB(skb1)->end_seq);
			__kfree_skb(skb1);
		}

add_sack:
		//设定SACK块
		if (tcp_is_sack(tp))
			tcp_sack_new_ofo_skb(sk, seq, end_seq);
	}
}

2.1 判断SACK是否使能

static inline int tcp_is_sack(const struct tcp_sock *tp)
{
	//在三次握手阶段,如果通信双方都携带了SACK允许选项,那么这个字段将被设置为1
	return tp->rx_opt.sack_ok;
}

2.2 设置DSACK

一旦接收到重复报文,就会调用tcp_dsack_set()设置DSACK内容,当前设置的前提是SACK特性可以使用,以及系统参数sysctl_tcp_dsack(/proc/sys/net/ipv4/tcp_dsack)开启。

static void tcp_dsack_set(struct tcp_sock *tp, u32 seq, u32 end_seq)
{
	//SACK特性可用并且系统使能了DSACK
	if (tcp_is_sack(tp) && sysctl_tcp_dsack) {
		//根据收到的是新数据还是老数据更新相应的DSACK统计量
		if (before(seq, tp->rcv_nxt))
			NET_INC_STATS_BH(LINUX_MIB_TCPDSACKOLDSENT);
		else
			NET_INC_STATS_BH(LINUX_MIB_TCPDSACKOFOSENT);
		//设置DSACK字段,表示需要发送DSACK信息,这样下次发送段时,
		//会将DSACK块放到SACK选项放到第一个位置
		tp->rx_opt.dsack = 1;
		//将DSACK块信息记录到duplicate_sack[0]中
		tp->duplicate_sack[0].start_seq = seq;
		tp->duplicate_sack[0].end_seq = end_seq;
		//更新下次待发送的SACK选项个数(需要减去一个DSACK块)
		tp->rx_opt.eff_sacks = min(tp->rx_opt.num_sacks + 1,
					   4 - tp->rx_opt.tstamp_ok);
	}
}

2.3 将SACK块加入到selective_acks数组

如果检测到一个,tcp_sack_new_ofo_skb()

//参数[seq, end_seq)就是要添加的
static void tcp_sack_new_ofo_skb(struct sock *sk, u32 seq, u32 end_seq)
{
	struct tcp_sock *tp = tcp_sk(sk);
	//指向保存SACK块的数组的第一个元素
	struct tcp_sack_block *sp = &tp->selective_acks[0];
	//当前selective_acks[]数组中已有多少个SACK块
	int cur_sacks = tp->rx_opt.num_sacks;
	int this_sack;

	//如果之前没有任何SACK块,那么这是一个新的SACK,直接填充到selective_acks[0]即可
	if (!cur_sacks)
		goto new_sack;

	//下面这个循环尝试将[seq, end_seq)与selective_acks[]数组中现有的SACK块合并,
	//如果合并成功,那么不需要更新任何计数信息,直接返回
	for (this_sack = 0; this_sack < cur_sacks; this_sack++, sp++) {
		//tcp_sack_extend()尝试将[seq, end_seq)和sp合并,如果合并成功则返回1
		if (tcp_sack_extend(sp, seq, end_seq)) {
			//根据RFC 2018的规定,selective_acks[]数组的第一个SACK块应该是最新的乱序报文,
			//所以这里需要将合并后的sp移动到数组的第一个位置
			for (; this_sack > 0; this_sack--, sp--)
				tcp_sack_swap(sp, sp - 1);
			//因为发生了SACK块的合并(扩展了左右边界),所以原来不连续的SACK块可能会变得连续,
			//tcp_sack_maybe_coalesce()函数重新检测这些SACK块的边界,尽可能将它们合并
			if (cur_sacks > 1)
				tcp_sack_maybe_coalesce(tp);
			return;
		}
	}

	/* Could not find an adjacent existing SACK, build a new one,
	 * put it at the front, and shift everyone else down.  We
	 * always know there is at least one SACK present already here.
	 *
	 * If the sack array is full, forget about the last one.
	 */
	//见注释,这是一个独立的SACK块,所以要将其放到第一个位置,
	//并且如果当前已有4个SACK块存在,那么需要丢弃最后一个
	if (this_sack >= 4) {
		this_sack--;
		tp->rx_opt.num_sacks--;
		sp--;
	}
	for (; this_sack > 0; this_sack--, sp--)
		*sp = *(sp - 1);

new_sack:
	//将参数指定的序号生成一个新的SACK块填充到sp指向的位置
	sp->start_seq = seq;
	sp->end_seq = end_seq;
	//更新SACK块的数目
	tp->rx_opt.num_sacks++;
	//结合时间戳选项、重复SACK更新下次可以发送的SACK块的数目
	tp->rx_opt.eff_sacks = min(tp->rx_opt.num_sacks + tp->rx_opt.dsack,
				   4 - tp->rx_opt.tstamp_ok);
}

3. 发送SACK选项

SACK选项的发送当然是在TCP段发送过程中构造TCP首部选项时确定的,这个过程由tcp_build_and_update_options()完成,该函数有tcp_transmit_skb()调用。

static void tcp_build_and_update_options(__be32 *ptr, struct tcp_sock *tp,
					 __u32 tstamp, __u8 **md5_hash)
{
...
	if (tp->rx_opt.eff_sacks) {
		//如果有DSACK,sp指向DSACK信息块,否则指向普通SACK信息块
		struct tcp_sack_block *sp = tp->rx_opt.dsack ? tp->duplicate_sack : tp->selective_acks;
		int this_sack;
		//填充位、类型、长度
		*ptr++ = htonl((TCPOPT_NOP  << 24) |
			       (TCPOPT_NOP  << 16) |
			       (TCPOPT_SACK <<  8) |
			       (TCPOLEN_SACK_BASE + (tp->rx_opt.eff_sacks *
						     TCPOLEN_SACK_PERBLOCK)));
		//这里实际上利用了struct tcp_sock中duplicate_sack[]和selective_acks紧邻这一前提,
		//eff_sacks已经考虑了DSACK和SACK,所以当填充了DSACK后,紧接着就可以填充SACK
		for (this_sack = 0; this_sack < tp->rx_opt.eff_sacks; this_sack++) {
			*ptr++ = htonl(sp[this_sack].start_seq);
			*ptr++ = htonl(sp[this_sack].end_seq);
		}
		//DSACK选项一旦发送之后就会被清除,如果再次收到重复段,才会重新生成。但是普通的SACK选项
		//即使发送过了,也不会清除,所以下次发送时只要没有清除就还会携带
		if (tp->rx_opt.dsack) {
			tp->rx_opt.dsack = 0;
			tp->rx_opt.eff_sacks--;
		}
	}
...
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值