TCP的定時器系列 — SYNACK定時器

主要內容:SYNACK定時器的實現,TCP_DEFER_ACCPET選項的實現。

內核版本:3.15.2

我的博客:http://blog.csdn.net/zhangskd

 

在上一篇博客中,已經連帶介紹了SYNACK定時器的創建和刪除,所以本文直接從它的激活和超時處理函數寫起。

 

激活

 

在三次握手期間,服務器端收到SYN包後,會分配一個連接請求塊,並初始化這個連接請求塊。

然後構造和發送SYNACK包,把這個連接請求塊鏈入半連接隊列中,並啓動SYNACK超時定時器。

之後如果再收到ACK,就能完成三次握手了。

 

具體路徑爲:

tcp_v4_do_rcv

    |--> tcp_rcv_state_process

               |--> tcp_v4_conn_request

                          |--> tcp_v4_send_synack

                          |--> inet_csk_reqsk_hash_add

                                     |--> inet_csk_reqsk_queue_added

 

inet_csk_reqsk_queue_hash_add()做的事情是:把連接請求塊鏈入半連接隊列中,設置超時時間,

啓動SYNACK定時器。這便是SYNACK定時器的激活時機,三次握手的詳情可見之前的blog。

static inline void inet_csk_reqsk_queue_added(struct sock *sk, const unsigned long timeout)
{
    /* 更新半連接隊列長度,如果之前的長度爲0 */
    if (reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue) == 0)
        inet_csk_reset_keepalive_timer(sk, timeout); /*啓動定時器 */
}

static inline int reqsk_queue_added(struct request_sock_queue *queue)
{
    struct listen_opt *lopt = queue->listen_opt; /* 半連接隊列 */

    const int prev_qlen = lopt->qlen; /* 之前的半連接隊列長度 */
    lopt->qlen_young++;  /* 更新未重傳過的請求塊數 */
    lopt->qlen++; /* 更新半連接隊列長度 */
    return prev_qlen;
}

void inet_csk_reset_keepalive_timer(struct sock *sk, unsigned long len)
{
    sk_reset_timer(sk, &sk->sk_timer, jiffies + len);
}

 

超時處理函數

 

sk->sk_timer可以同時扮演幾個角色:保活定時器,SYNACK定時器,FIN_WAIT2定時器。

通過判斷此時連接的狀態是LISTEN、ESTABLISHED,還是FIN_WAIT2,就可以直接區分它們了。

static void tcp_keepalive_timer(unsigned long data)
{
    struct sock *sk = (struct sock *) data;
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    u32 elapsed;

    /* Only process if socket is not in use. */
    bh_lock_sock(sk);

    /* 如果用戶進程正在使用此sock,那麼一般過50ms後再來看看。
     * 可見SYNACK定時器不一定能夠準時呢,而實際上它本身的誤差就有200ms,詳細可見下文。
     */
    if (sock_owned_by_user(sk)) {
        /* Try again later. */
        inet_csk_reset_keepalive_timer(sk, HZ/20);
        goto out;
    }

    /* 連接處於LISTEN狀態,那麼肯定是SYNACK定時器了 */
    if (sk->sk_state == TCP_LISTEN) {
        tcp_synack_timer(sk);
        goto out;
    }
     ...
out:
    bh_unlock_sock(sk);
    sock_put(sk);
}

 

對於SYNACK定時器,真正的處理函數是inet_csk_reqsk_queue_prune()。

#define TCP_SYNQ_INTERVAL (HZ/5) /* Period of SYNACK timer */
#define TCP_TIMEOUT_INIT ((unsigned) (1*HZ)) /* RFC6298 2.1 initial RTO value */
#define TCP_RTO_MAX ((unsigned) (120*HZ))

/* Timer for listening sockets. */
static void tcp_synack_timer(struct sock *sk)
{
    inet_csk_reqsk_queue_prune(sk, TCP_SYNQ_INTERVAL, TCP_TIMEOUT_INIT, TCP_RTO_MAX);
}
void inet_csk_reqsk_queue_prune(struct sock *parent, const unsigned long interval,
    const unsigned long timeout, const unsigned long max_rto)
{
    struct inet_connection_sock *icsk = inet_csk(parent);
    struct request_sock_queue *queue = &icsk->icsk_accept_queue;
    struct listen_sock *lopt = queue->listen_opt; /* 半連接隊列 */

    /* 如果沒有設置TCP_SYNCNT選項,默認最多允許重傳5次SYNACK */
    int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries; 
    int thresh = max_retries;
    unsigned long now = jiffies;
    struct request_sock **reqp, *req;
    int i, budget;

   /* 半連接隊列要存在且至少有個連接請求塊 */
   if (lopt == NULL || lopt->qlen == 0)
       return;

    /* 如果半連接隊列的長度超過了最大值的一半,需要降低SYNACK的最大重傳次數,詳細見下文(1) */
    if (lopt->qlen >> (lopt->max_qlen_log - 1)) {
        int young = (lopt->qlen_young << 1);

        /* 半連接隊列中,未重傳過的連接請求塊的比重越低,則允許的最大重傳次數就越少。
         * 不這樣做的話,老的連接請求塊會存活很長時間,導致沒有足夠的空間接納新的連接請求塊。
         * 具體來說,默認的thresh爲5,當未重傳過的連接請求塊的佔比:
         * < 1/2,thresh = 4
         * < 1/4,thresh = 3
         * < 1/8,thresh = 2
         */
        while (thresh > 2) {
            if (lopt->qlen < young)
                break;
            thresh--;
            young <<= 1;
        }
    }

    /* 如果設置了TCP_DEFER_ACCEPT選項,則更新SYNACK的最大重傳次數,詳細見下文(2) */
    if (queue->rskq_defer_accept)
        max_retries = queue->rskq_defer_accept; 

    /* 連接的初始超時時間是1s,SYNACK定時器在首次觸發之後,接下來每200ms就觸發一次。
     * Q:連接請求塊的超時時間依然是1s,那麼SYNACK定時器爲什麼要更加頻繁的觸發呢?
     * A:增加了定時器的精確度,誤差從1s降到200ms,也能更加及時的剔除太老的連接請求塊。
     * 默認1s內遍歷2次的半連接表。
     */
    budget = 2 * (lopt->nr_table_entries / (timeout / interval));
    i = lopt->clock_hand; /* 半連接表中的第i個連接請求塊隊列,是上次遍歷到的+1,這樣不用每次都從頭開始 */

    do {
        reqp = &lopt->syn_table[i]; /* 半連接表中的第i個連接請求塊隊列 */
        while ((req = *reqp) != NULL) { /* 遍歷隊列 */
            if (time_after_eq(now, req->expires)) { /* 如果SYNACK超時了 */
                int expire = 0, resend = 0; /* expire表示是否要丟棄本連接請求塊,resend表示是否要重傳SYNACK */

                /* 判斷expire和resend的值,詳細見下文(3) */
                syn_ack_recalc(req, thresh, max_retries, queue->rskq_defer_accept, &expire, &resend);
                req->rsk_ops->syn_ack_timeout(parent, req); /* 增加timeout統計計數 */

                /* 有意思的條件判斷,先考慮expire,再考慮resend。
                 * 條件爲真時,表示此連接請求塊還不是太老,不用刪除。
                 */
                if (! expire && (! resend || ! inet_rtx_syn_ack(parent, req) || inet_rsk(req)->acked)) {
                    unsigned long timeo;
                    if (req->num_timeout++ == 0) /* 如果是尚未重傳過的 */
                        lopt->qlen_young--;

                        timeo = min(timeout << req->num_timeout, max_rto); /* 超時時間指數增大 */
                        req->expires = now + timeo;
                        reqp = &req->dl_next;
                        continue;
                }

                /* Drop this request */
                inet_csk_reqsk_queue_unlink(parent, req, reqp); /* 把連接請求塊從半連接隊列中刪除 */
                reqsk_queue_removed(queue, req); /* 更新半連接隊列長度相關變量 */
                reqsk_free(req); /* 釋放連接請求塊 */
                continue;
            }
            reqp = &req->dl_next;
        }
        i = (i + 1) & (lopt->nr_table_entries - 1);
    } while (--budget > 0);

    lopt->clock_hand = i; /* 本地變量到第(i - 1)個連接請求塊隊列,下次從第i個開始 */

    if (lopt->qlen)
        inet_csk_reset_keepalive_timer(parent, interval); /* 重置SYNACK定時器,超時時間爲200ms */
}

 

最大重傳次數的動態調整

 

如果半連接隊列的長度超過了最大值的一半,就需要降低SYNACK所允許的最大重傳次數。

影響因素是半連接隊列中未重傳過的連接請求塊所佔的比重。

在半連接隊列中,未重傳過的連接請求塊的比重越低,則允許重傳的次數就越少。

因爲不這樣做的話,老的連接請求塊會存活很長時間,導致沒有足夠的空間接納新的連接請求塊,

而顯然新的連接請求塊(即未重傳過的連接請求塊)更加可信和有價值。

 

具體來說,默認的thresh爲5,當未重傳過的連接請求塊的佔比:

PCT < 1/2,thresh = 4

PCT < 1/4,thresh = 3

PCT < 1/8,thresh = 2

 

代碼中的註釋很詳細,贊一個:

Normally all the openreqs are young and become mature (i.e. converted to established socket) for

first timeout. If synack was not acknowledged for 1 second, it means one of the following things:

synack was lost, ack was lost, rtt is high or nobody planned to ack (i.e. synflood). When server is a

bit loaded, queue is populated with old open requests, reducing effective size of queue. When server

is well loaded, queue size reduces to zero after several minutes of work. It is not synflood, it is normal

operation. The solution is pruning too old entries overriding normal timeout, when situation becomes

dangerous.

Essentially, we reserve half of room for young embrions; and abort old ones without pity, if old ones

are about to clog our table.

 

TCP_DEFER_ACCEPT選項

 

用於三次握手階段,使用這個選項時,收到客戶端發送的純ACK後,會直接把純ACK丟棄。

所以就不會馬上創建和初始化一個新的sock,不會把此連接請求塊從半連接隊列移動到全連接隊列,

更不會喚醒監聽進程來accept。TCP_DEFER_ACCEPT,顧名思義,就是延遲連接的accept。

 

Q:那麼使用這個選項有什麼好處呢?

A:想象一個場景,三次握手建立連接後,客戶端過了很長時間、或者根本沒有發送請求,那麼服務端

豈不是瞎忙活了。這時如果設置了這個選項,只有當客戶端發送帶有負荷的ACK過來時,纔會真正的建立

連接、分配資源、喚醒監聽進程。有人覺得這個選項可以節約連接建立時間,這是沒有依據的,節省服務器

端的資源倒是真的。

 

選項的設置:

#define TCP_DEFER_ACCEPT 9 /* Wake up listener only when data arrive */

static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval,
    unsigned int optlen)
{
    ...
    case TCP_DEFER_ACCEPT:
        /* Translate value in seconds to number of retransmits */
        icsk->icsk_accept_queue.rskq_defer_accept = secs_to_retrans(val, TCP_TIMEOUT_INIT / HZ,
            TCP_RTO_MAX / HZ);
    ...
}
/* Convert seconds to retransmits based on initial and max timeout */
static u8 secs_to_retrans(int seconds, int timeout, int rto_max)
{
    u8 res = 0;

    if (seconds > 0) {
        int period = timeout;
        res = 1;

        while(seconds > period && res < 255) {
            res++;
            timeout <<= 1;

            if (timeout > rto_max)
                timeout = rto_max;

            period += timeout;
        }
    }

    return res;
}

secs_to_retrans()用於把需要延遲的時間,轉換爲SYNACK的最大重傳次數。

我們知道客戶端的純ACK會被無情的丟棄,所以之後客戶端如果沒有及時發送帶有負荷的ACK(就是請求),

會導致服務端SYNACK發生超時,超時後就要重傳此SYNACK。

 

Q:假如TCP_DEFER_ACCEPT告訴服務器,要延遲的時間爲10s,那麼SYNACK的最大重傳次數要設成多少呢?

A:用超時時間的指數退避來計算。當SYNACK的最大重傳次數爲4時,才能等夠10s的延遲時間。

第一次重傳,等待時間+1s

第二次重傳,等待時間+2s

第三次重傳,等待時間+4s

第四次重傳,等待時間+8s

第四次重傳過後,總共的等待時間爲15s,已經等夠了10s的延遲時間了。

如果客戶端的請求還不過來,就放棄這個連接請求塊了。

 

放棄連接和重傳SYNACK的判斷

 

判斷是否要丟棄老連接請求塊、是否要重傳SYNACK包,要分兩種情況考慮:

 

(1) 沒有使用TCP_DEFER_ACCEPT選項

丟棄條件:超過了動態調整後的最大重傳次數。

重傳條件:不丟棄時就重傳SYNACK。

 

(2) 使用了TCP_DEFER_ACCPET選項

丟棄條件(同時滿足):

1. 超過了動態調整後的最大重傳次數。

2. 沒有收到過純ACK,或者已經超過了設置的最大延遲時間。

 

重傳條件(滿足一條即可):

1. 沒有收到過純ACK。

2. 已收到純ACK,本次是最後一次重傳機會了。

/* Decide when to expire the request and when to resend SYN-ACK */

static inline void syn_ack_recalc (struct request_sock *req, const int thresh, const int max_retries,
    const u8 rskq_defer_accept, int *expire, int *resend)
{
    /* 如果沒有使用TCP_DEFER_ACCEPT選項 */
    if (!rskq_defer_accept) {
        *expire = req->num_timeout >= thresh; /* 超過了動態調整後的最大重傳次數則放棄本連接請求塊 */
        *resend = 1; /* 始終爲1,但其實是隻有不放棄時(expire爲0)纔會真的重傳 */
        return;
    }

    /* 啓用TCP_DEFER_ACCEPT時,放棄的條件更加嚴格,還需要滿足以下兩個條件之一:
     * 1. 沒有收到過純ACK。
     * 2. 超過了設置的最大延遲時間。
     * 滿足了以上兩個條件之一,就不值得再搶救了,已棄療。
     */
    *expire = req->num_timeout >= thresh && (!inet_rsk(req)->acked || req->num_timeout >= max_entries);

    /* 要重傳SYNACK的情況有兩種:
     * 1. 沒有收到過純ACK時。
     * 2. 已收到純ACK,本次是最後一次重傳機會了。
     */
    /* Do not resend while waiting for data after ACK, start to resend on end of defering period to give
     * last chance for data or ACK to create established socket.
     */
    *resend = ! inet_rsk(req)->acked || req->num_timeout >= rskq_defer_accept - 1;
}

SYNACK的重傳函數。

int inet_rtx_syn_ack(struct sock *parent, struct request_sock *req)
{
    int err = req->rsk_ops->rtx_syn_ack(parent, req); /* 調用tcp_v4_rtx_synack()來重傳SYNACK */

    if (! err)
        req->num_retrans++; /* 增加SYNACK的重傳次數 */
    return err;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章