当接收方收到乱序报文时,如果在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--;
}
}
...
}