http://simohayha.iteye.com/blog/578744
上一篇处理ack的blog中我们知道当我们接收到ack的时候,我们会判断sack段,如果包含sack段的话,我们就要进行处理。这篇blog就主要来介绍内核如何处理sack段。
SACK是包含在tcp的option中的,由于tcp的头的长度的限制,因此SACK也就是最多包含4个段,也就是32个字节。我们先来看tcp中的SACK段的表示:
1 2 3 4 |
|
可以看到很简单,就是一个段的起始序列号和一个结束序列号。
前一篇blog我们知道tcp_skb_cb的sacked域也就是sack option的偏移值,而在tcp的option它的组成是由3部分组成的,第一部分为option类型,第二部分为当前option的长度,第三部分才是数据段,因此我们如果要取得SACK的段,就必须这样计算。
这里ack_skb也就是我们要处理的skbuffer。
1 2 3 4 5 |
|
这里很奇怪,内核还有一个tcp_sack_block_wire类型的结构,它和tcp_sack_block是完全一样的。
而我们如果要得到当前的SACK段的个数我们要这样做:
1 2 |
|
这里ptr1也就是sack option的长度(字节数),而TCPOLEN_SACK_BASE为类型和长度字段的长度,因此这两个值的差也就是sack段的总长度,而这里每个段都是8个字节,因此我们右移3位就得到了它的个数,最后sack的段的长度不能大于4,因此我们要取一个最小值。
上面的结构下面这张图非常清晰的展示了,这几个域的关系:
然后我们来看SACK的处理,在内核中SACK的处理是通过tcp_sacktag_write_queue来实现的,这个函数比较长,因此这里我们分段来看。
先来看函数的原型
1 2 3 |
|
第一个参数是当前的sock,第二个参数是要处理的skb,第三个参数是接受ack的时候的snd_una.
在看之前这里有几个重要的域要再要说明下。
1 tcp socket的sacked_out域,这个域保存了所有被sack的段的个数。
2 还有一个就是tcp_sacktag_state结构,这个结构保存了当前skb的一些信息。
1 2 3 4 5 |
|
3 tcp socket的highest_sack域,这个域也就是被sack确认的最大序列号的skb。
先来看第一部分,这部分的代码主要功能是初始化一些用到的值,比如sack的指针,当前有多少sack段等等,以及一些合法性校验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
在看接下来的部分之前我们先来看tcp_highest_sack_reset和tcp_check_dsack函数,先是tcp_highest_sack_reset函数。
1 2 3 4 5 |
|
这里原因很简单,因为当sacked_out为0,则说明没有通过sack确认的段,此时highest_sack自然就指向写队列的头。
第二个是tcp_check_dsack函数,这个函数比较复杂,他主要是为了检测D-SACK,也就是重复的sack。
有关dsack的概念可以去看RFC 2883和3708.
我这里简要的提一下dsack的功能,D-SACK的功能主要是使接受者能够通过sack的块来报道接收到的重复的段,从而使发送者更好的进行拥塞控制。
这里D-SACK的判断是通过RFC2883中所描述的进行的。如果是下面两种情况,则说明收到了一个D-SACK。
1 如果SACK的第一个段所ack的区域被当前skb的ack所确认的段覆盖了一部分,则说明我们收到了一个d-sack,而代码中也就是sack第一个段的起始序列号小于snd_una。下面的图描述了这种情况:
2 如果sack的第二个段完全包含了第二个段,则说明我们收到了重复的sack,下面这张图描述了这种关系。
最后要注意的是,这里收到D-SACK后,我们需要打开当前sock d-sack的option。并设置dsack的flag。
然后我们还需要判断dsack的数据是否已经被ack完全确认过了,如果确认过了,我们就需要更新undo_retrans域,这个域表示重传的数据段的个数。
来看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
然后回到tcp_sacktag_write_queue,接下来这部分很简单,主要是提取sack的段到sp中,并校验每个段的合法性,然后统计一些信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
然后接下来的代码就是排序sack的段,也就是按照序列号的大小来排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
然后就是cache的初始化,这里的tcp socket的recv_sack_cache域要注意,这个域保存了上一次处理的sack的段的序列号。可以看到这个域类型也是tcp_sack_block,而且大小也是4,
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
然后就是开始真正处理重传队列中的skb了。
我们要知道重传队列中的skb有三种类型,分别是SACKED(S), RETRANS® 和LOST(L),而每种类型所处理的数据包的个数分别保存在sacked_out, retrans_out 和lost_out中。
而处于重传队列的skb也就是会处于下面6中状态:
1 2 3 4 5 6 7 |
|
这里Tag也就是上面所说的三种类型,而InFlight也就是表示还在网络中的段的个数。
然后重传队列中的skb的状态变迁是通过下面这几种事件来触发的:
1 2 3 4 5 6 7 8 9 10 11 |
|
在进入这段代码分析之前,我们先来看几个重要的域。
tcp socket的high_seq域,这个域是我们进入拥塞控制的时候最大的发送序列号,也就是snd_nxt.
然后这里还有FACK的概念,FACK算法也就是收到的不同的SACK块之间的hole,他就认为是这些段丢失掉了。因此这里tcp socket有一个fackets_out域,这个域表示了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
|
上面的代码并不复杂,这里主要有两个函数,我们需要详细的来分析,一个是tcp_sacktag_skip,一个是tcp_sacktag_walk。
先来看tcp_sacktag_skip,我们给重传队列的skb的tag赋值时,我们需要遍历整个队列,可是由于我们有序列号,因此我们可以先确认起始的skb,然后从这个skb开始遍历,这里这个函数就是用来确认起始skb的,这里确认的步骤主要是通过start_seq来确认的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
然后是最关键的一个函数tcp_sacktag_walk,这个函数主要是遍历重传队列,找到对应需要设置的段,然后设置tcp_cb的sacked域为TCPCB_SACKED_ACKED,这里要注意,还有一种情况就是sack确认了多个skb,这个时候我们就需要合并这些skb,然后再处理。
然后来看代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
|
然后我们来看tcp_sacktag_one函数,这个函数用来设置对应的tag,这里所要设置的也就是tcp_cb的sacked域。我们再来回顾一下它的值:
1 2 3 4 5 6 |
|
如果一切都正常的话,我们最终就会设置skb的这个域为TCPCB_SACKED_ACKED,也就是已经被sack过了。
这个函数处理比较简单,主要就是通过序列号以及sacked本身的值最终来确认sacked要被设置的值。
这里我们还记得,一开始sacked是被初始化为sack option的偏移(如果是正确的sack)的.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
最后我们来看tcp_sacktag_write_queue的最后一部分,也就是更新cache的部分。
它也就是将处理过的sack清0,没处理过的保存到cache中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|