TCP空閒連接的重啓動

在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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章