http://www.pagefault.info/?p=145
这次主要来看一下内核拥塞控制算法cubic的实现,在linux kernel中实现了很多种拥塞控制算法,不过新的内核(2.6.19之后)默认是cubic(想得到当前内核使用的拥塞控制算法可以察看/proc/sys/net/ipv4/tcp_congestion_control这个值).下面是最新的redhat 6的拥塞控制算法(rh5还是bic算法):
1 2 |
|
这个算法的paper在这里:
http://netsrv.csc.ncsu.edu/export/cubic_a_new_tcp_2008.pdf
拥塞控制算法会在tcp_ack中被调用,如果是正常的ack(比如不是重复的,不是sack等等)就会进入拥塞控制算法。
cubic会调用tcp_slow_start这个方法(基本上每种拥塞控制算法都会调用它),这个方法主要是处理slow start,而内核中的slow start是这样子的,接收一个ack,snd_cwnd就会加1,然后当cwnd大于设置的拥塞窗口阀值snd_ssthresh的时候,就会进入拥塞避免状态。而在发送数据包的时候,会判断in_flight(可以认为是发送还没确认的数据包,它等于发送未确认的数据包-sack的数据段-丢失的数据段+重传的数据段,我的前面的blog有详细解释这个数据段)是否大于snd_cwnd,如果大于等于则不会发送数据,如果小于才会继续发送数据。
而进入拥塞避免状态之后,窗口的增长速度将会减缓,
来看一下我用jprobe hook tcp_slow_start(slow start处理函数) 和 tcp_cong_avoid_ai (拥塞避免处理)的数据。
在下面的数据中sk表示当前socket的地址, in_flight packet表示发送还未接收的包, snd_cwnd表示发送拥塞窗口。 然后详细解释下count后面的两个值,其中第一个是snd_cwnd_cnt,表示在当前的拥塞窗口中已经发送的数据段的个数,而第二个是struct bictcp的一个域cnt,它是cubic拥塞算法的核心,主要用来控制在拥塞避免状态的时候,什么时候才能增大拥塞窗口,具体实现是通过比较它和snd_cwnd_cnt,来决定是否增大拥塞窗口,而这个值的计算,我这里不会分析,想了解的,可以去看cubic的paper。
还有一个需要注意的地方就是ssthresh,可以看到这个值在一开始初始化为一个最大的值,然后在进入拥塞避免状态的时候被设置为前一次拥塞窗口的大小.这个处理可以看rfc2581的这段:
The initial value of ssthresh may be arbitrarily high (i.e., the size of the advertised window), but it may be reduced in response to congestion. When cwnd < ssthresh, the slow-start algorithm is used and when cwnd > ssthresh, the congestion avoidance algorithm is used. When cwnd and ssthresh are equal, the sender may use either of them.
我们后面会看到这个值在cubic中是如何被设置的。
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 |
|
可以看到在slow start的状态,发送拥塞窗口就是很简单的每次加1,而当进入拥塞避免之后,明显的拥塞窗口的增大速度变慢很多。
接下来来看具体的代码是如何实现的.
首先来看bictcp_cong_avoid,也就是cubic拥塞控制算法的handler(一般来说在tcp_ack中被调用),它有3个参数,第一个是对应的sock,第二个是对应的ack序列号,而第三个就是比较重要的一个变量,表示发送还没有被ack的数据包(在linux 内核tcp拥塞处理一中详细介绍过内核中这些变量),这个变量是拥塞控制的核心。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
接下来就是看tcp_is_cwnd_limited,这个函数主要是实现RFC2861中对拥塞窗口的检测。它返回1说明拥塞窗口被限制,我们需要增加拥塞窗口,否则的话,就不需要增加拥塞窗口。
然后这里还有两个判断,先来看第一个 gso的概念,gso是Generic Segmentation Offload的简写,他的主要功能就是尽量的延迟数据包的传输,以便与在最恰当的时机传输数据包,这个机制是处于数据包离开协议栈与进入驱动之间。比如如果驱动支持TSO的话,gso就会将多个unsegmented的数据段传递给驱动。而TSO是TCP Segmentation Offload的缩写,它表示驱动支持协议栈发送大的MTU的数据段,然后硬件负责来切包,然后将数据发送出去,这样子的话,就能提高系统的吞吐。这几个东西(还有GRO),以后我会详细分析,现在只需要大概知道他们是干什么的。
而在这里如果支持gso,就有可能是tso defer住了数据包,因此这里会进行几个相关的判断,来看需不需要增加拥塞窗口。。
然后是burst的概念,主要用来控制网络流量的突发性增大,也就是说当left数据(还能发送的数据段数)大于burst值的时候,我们需要暂时停止增加窗口,因为此时有可能我们这边数据发送过快。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
接下来来看snd_ssthresh是如何被设置的,这个值在加载cubic模块的时候可以传递一个我们制定的值给它,不过,默认是很大的值,我这里是2147483647,然后在接收ack期间(slow start)期间会调整这个值,在cubic中,默认是16(一般来说说当拥塞窗口到达16的时候,snd_ssthresh会被设置为16).
在cubic中有两个可以设置snd_ssthresh的地方一个是hystart_update,一个是bictcp_recalc_ssthresh,后一个我这里就不介绍了,以后介绍拥塞状态机的时候会详细介绍,现在只需要知道,只有遇到拥塞的时候,需要调整snd_ssthres的时候,我们才需要调用bictcp_recalc_ssthresh。
而hystart_update是在bictcp_acked中被调用,而bictcp_acked则是基本每次收到ack都会调用这个函数,我们来看在bictcp_acked中什么情况就会调用hystart_update:
1 2 3 4 |
|
其中hystart是hybrid slow start打开的标志,默认是开启,hystart_low_window是设置snd_ssthresh的最小拥塞窗口值,默认是16。而tp->snd_ssthresh默认是一个很大的值,因此这里就知道了,当拥塞窗口增大到16的时候我们就会进去hystart_update来更新snd_ssthresh.因此hystart_updat换句话来说也就是主要用于是否退出slow start。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
然后是slow start的处理,这里有关abc的处理,注释都很详细了,这里就不解释了,我们主要看abc关闭的部分。这里使用cnt,也是主要为了打开abc之后的slow start。
这是abc(Appropriate Byte Counting)相关的rfc:
http://www.faqs.org/rfcs/rfc3465.html
Appropriate Byte Countin会导致拥塞控制算法很激进,比如打开它之后就不一定每次ack都会执行slow start,而且窗口也会增加的快很多。
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 |
|
最后是拥塞避免的处理。这里主要的步骤就是通过判断当前的拥塞窗口下已经发送的数据段的个数是否大于算法计算出来的值w,如果大于我们才能增加拥塞窗口值,否则之需要增加snd_cwnd_cnt。
1 2 3 4 5 6 7 8 9 10 11 12 |
|