在TCP連接空閒一段時間之後,發送端再次開啓發送時,可能導致大量的數據發送到網絡中。由於一段空閒時間後,TCP發送端不能再使用ACK時鐘發送新報文到網絡中,所以,可能線速發送一整擁塞窗口的數據,容易造成網絡擁塞,而且,網絡狀況可能已經改變。因此,內核中在空閒時間超過RTO之後,將使用慢啓動恢復發送。
空閒檢查
在空閒檢查函數中,如果內核未打開在空閒之後啓用慢啓動的功能,即tcp_slow_start_after_idle值爲零,或者網絡中存在發送的報文;或者擁塞控制算法定義了相關處理,不啓用慢啓動。否則,如果當前時間與最後一次的發送時間差值大於RTO時長,開啓慢啓動。
static inline void tcp_slow_start_after_idle_check(struct sock *sk)
{
const struct tcp_congestion_ops *ca_ops = inet_csk(sk)->icsk_ca_ops;
struct tcp_sock *tp = tcp_sk(sk);
s32 delta;
if (!sock_net(sk)->ipv4.sysctl_tcp_slow_start_after_idle || tp->packets_out ||
ca_ops->cong_control)
return;
delta = tcp_jiffies32 - tp->lsndtime;
if (delta > inet_csk(sk)->icsk_rto)
tcp_cwnd_restart(sk, delta);
}
如下tcp_cwnd_restart函數,重啓動窗口值首先賦值爲tcp_init_cwnd函數的計算值,其一般情況下等於初始窗口值TCP_INIT_CWND(10),但是,如果在路由項中緩存了初始窗口值,等於緩存值。慢啓動閾值ssthresh等於其當前值與擁塞窗口*3/4兩者之間的最大值。其次,重啓動窗口值選擇其與當前窗口值兩者之間的較小值,如果當前擁塞窗口大於重啓動窗口,空閒時長每經過RTO時段,將當前擁塞窗口減半。最後,最終的擁塞窗口等於擁塞窗口與重啓動窗口之間的最大值。
/* RFC2861. Reset CWND after idle period longer RTO to "restart window".
* This is the first part of cwnd validation mechanism.
*/
void tcp_cwnd_restart(struct sock *sk, s32 delta)
{
struct tcp_sock *tp = tcp_sk(sk);
u32 restart_cwnd = tcp_init_cwnd(tp, __sk_dst_get(sk));
u32 cwnd = tp->snd_cwnd;
tcp_ca_event(sk, CA_EVENT_CWND_RESTART);
tp->snd_ssthresh = tcp_current_ssthresh(sk);
restart_cwnd = min(restart_cwnd, cwnd);
while ((delta -= inet_csk(sk)->icsk_rto) > 0 && cwnd > restart_cwnd)
cwnd >>= 1;
tp->snd_cwnd = max(cwnd, restart_cwnd);
tp->snd_cwnd_stamp = tcp_jiffies32;
tp->snd_cwnd_used = 0;
}
如下在傳輸函數__tcp_transmit_skb中,調用tcp_event_data_sent更新最後的發送時間到變量lsndtime中,這裏注意只有當發送數據報文時,才更新發送時間。
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
...
if (skb->len != tcp_header_size) {
tcp_event_data_sent(tp, sk);
static void tcp_event_data_sent(struct tcp_sock *tp, struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
const u32 now = tcp_jiffies32;
if (tcp_packets_in_flight(tp) == 0)
tcp_ca_event(sk, CA_EVENT_TX_START);
tp->lsndtime = now;
空閒檢查路徑
在TCP報文發送流程中,函數skb_entail將skb添加到套接口發送隊列sk_write_queue末尾,函數最後,調用tcp_slow_start_after_idle_check檢查函數,即在發送報文前檢查空閒時間是否超時。
static void skb_entail(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct tcp_skb_cb *tcb = TCP_SKB_CB(skb);
skb->csum = 0;
tcb->seq = tcb->end_seq = tp->write_seq;
tcb->tcp_flags = TCPHDR_ACK;
tcb->sacked = 0;
__skb_header_release(skb);
tcp_add_write_queue_tail(sk, skb);
...
tcp_slow_start_after_idle_check(sk);
}
另外,在接收到對端ACK報文時,如果此報文更新了發送窗口,並且發送隊列不爲空,調用空閒檢查函數。如果發送端是因爲發送窗口爲空,而停止發送,當發送窗口再次打開時,繼續發送之前,需要進行此空閒檢查。
static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack, u32 ack_seq)
{
struct tcp_sock *tp = tcp_sk(sk);
u32 nwin = ntohs(tcp_hdr(skb)->window);
if (likely(!tcp_hdr(skb)->syn))
nwin <<= tp->rx_opt.snd_wscale;
if (tcp_may_update_window(tp, ack, ack_seq, nwin)) {
flag |= FLAG_WIN_UPDATE;
tcp_update_wl(tp, ack_seq);
if (tp->snd_wnd != nwin) {
tp->snd_wnd = nwin;
...
if (!tcp_write_queue_empty(sk))
tcp_slow_start_after_idle_check(sk);
內核版本 5.0