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

这篇笔记开始记录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个,所以数组长度为4struct 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标记,表示输入段中有DSACKfound_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为-1first_sack_index = 0;for (i = 0; i < num_sacks; i++) {//如果有DSACK,那么它一定在第一个位置。综合前面的判断设置dup_sackint 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);elseNET_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为-1if (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块,计数器加1used_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信息块标记发送队列中的skbskb = 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_seqif (!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);
}


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部