这篇笔记记录了TCP在收到SACK选项后,对发送队列中skb的记分牌的更新细节。
在笔记TCP选项之SACK选项的接收(一)中有看到,对于每个SACK信息块,在和cache信息比较后,都会有两个核心操作来更新记分牌:
- tcp_sacktag_skip()
- 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包数 | 描述 |
---|---|---|
0 | 1 | 原始的数据包正在传输(正常情况) |
S | 0 | 原始数据包被SACK确认 |
L | 0 | 原始的数据包丢了 |
R | 2 | 原始的和重传的包都还在传输 |
L、R | 1 | 原始的数据包丢了,重传的包还在传输 |
S、R | 1 | 原始的数据包被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;
}