程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> TIME_WAIT狀態下對接收到的數據包如何處理

TIME_WAIT狀態下對接收到的數據包如何處理

編輯:C++入門知識

       1、保證TCP連接關閉的可靠性。如果最終發送的ACK丟失,被動關閉的一端會重傳最終的FIN包,如果執行主動關閉的一端沒有維護這個連接的狀態信息,會發送RST包響應,導致連接不正常關閉。
      2、允許老的重復分組在網絡中消逝。假設在一個連接關閉後,發起建立連接的一端(客戶端)立即重用原來的端口、IP地址和服務端建立新的連接。老的連接上的分組可能在新的連接建立後到達服務端,TCP必須防止來自某個連接的老的重復分組在連接終止後再現,從而被誤解為同一個連接的化身。要實現這種功能,TCP不能給處於TIME_WAIT狀態的連接啟動新的連接。TIME_WAIT的持續時間是2MSL,保證在建立新的連接之前老的重復分組在網絡中消逝。這個規則有一個例外:如果到達的SYN的序列號大於前一個連接的結束序列號,源自Berkeley的實現將給當前處於TIME_WAIT狀態的連接啟動新的化身。
  最初在看《Unix網絡編程》 的時候看到這個狀態,但是在項目中發現對這個狀態的理解有誤,特別是第二個理由。原本認為在TIME_WAIT狀態下肯定不會再使用相同的五元組(協議類型,源目的IP、源目的端口號)建立一個新的連接,看書還是不認真啊!為了加深理解,決定結合內核代碼,好好來看下內核在TIME_WAIT狀態下的處理。其實TIME_WAIT存在的第二個原因的解釋更多的是從被動關閉一方的角度來說明的。如果是執行主動關閉的是客戶端,客戶端戶進入TIME_WAIT狀態,假設客戶端重用端口號來和服務器建立連接,內核會不會允許客戶端來建立連接?內核如何來處理這種情況?書本中不會對這些點講的那麼詳細,要從內核源碼中來找答案。
  我們先來看服務器段進入TIME_WAIT後內核的處理,即服務器主動關閉連接。TCP層的接收函數是tcp_v4_rcv(),和TIME_WAIT狀態相關的主要代碼如下所示:
[cpp]
int tcp_v4_rcv(struct sk_buff *skb) 

    ...... 
 
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest); 
    if (!sk) 
        goto no_tcp_socket; 
process: 
    if (sk->sk_state == TCP_TIME_WAIT) 
        goto do_time_wait;     
        ...... 
 
discard_it: 
    /* Discard frame. */ 
    kfree_skb(skb); 
    return 0; 
    ...... 
do_time_wait: 
    ...... 
 
    switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) { 
    case TCP_TW_SYN: { 
        struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev), 
                            &tcp_hashinfo, 
                            iph->daddr, th->dest, 
                            inet_iif(skb)); 
        if (sk2) { 
            inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row); 
            inet_twsk_put(inet_twsk(sk)); 
            sk = sk2; 
            goto process; 
        } 
        /* Fall through to ACK */ 
    } 
    case TCP_TW_ACK: 
        tcp_v4_timewait_ack(sk, skb); 
        break; 
    case TCP_TW_RST: 
        goto no_tcp_socket; 
    case TCP_TW_SUCCESS:;  
    } 
    goto discard_it; 

int tcp_v4_rcv(struct sk_buff *skb)
{
    ......

    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
    if (!sk)
        goto no_tcp_socket;
process:
    if (sk->sk_state == TCP_TIME_WAIT)
        goto do_time_wait;   
        ......

discard_it:
    /* Discard frame. */
    kfree_skb(skb);
    return 0;
    ......
do_time_wait:
    ......

    switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
    case TCP_TW_SYN: {
        struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
                            &tcp_hashinfo,
                            iph->daddr, th->dest,
                            inet_iif(skb));
        if (sk2) {
            inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
            inet_twsk_put(inet_twsk(sk));
            sk = sk2;
            goto process;
        }
        /* Fall through to ACK */
    }
    case TCP_TW_ACK:
        tcp_v4_timewait_ack(sk, skb);
        break;
    case TCP_TW_RST:
        goto no_tcp_socket;
    case TCP_TW_SUCCESS:;
    }
    goto discard_it;
}  接收到SKb包後,會調用__inet_lookup_skb()查找對應的sock結構。如果套接字狀態是TIME_WAIT狀態,會跳轉到do_time_wait標簽處處理。從代碼中可以看到,主要由tcp_timewait_state_process()函數來處理SKB包,處理後根據返回值來做相應的處理。
  在看tcp_timewait_state_process()函數中的處理之前,需要先看一看不同的返回值會對應什麼樣的處理。
  如果返回值是TCP_TW_SYN,則說明接收到的是一個“合法”的SYN包(也就是說這個SYN包可以接受),這時會首先查找內核中是否有對應的監聽套接字,如果存在相應的監聽套接字,則會釋放TIME_WAIT狀態的傳輸控制結構,跳轉到process處開始處理,開始建立一個新的連接。如果沒有找到監聽套接字會執行到TCP_TW_ACK分支。
  如果返回值是TCP_TW_ACK,則會調用tcp_v4_timewait_ack()發送ACK,然後跳轉到discard_it標簽處,丟掉數據包。
  如果返回值是TCP_TW_RST,則會調用tcp_v4_send_reset()給對端發送RST包,然後丟掉數據包。
  如果返回值是TCP_TW_SUCCESS,則會直接丟掉數據包。
  接下來我們通過tcp_timewait_state_process()函數來看TIME_WAIT狀態下的數據包處理。
  為了方便討論,假設數據包中沒有時間戳選項,在這個前提下,tcp_timewait_state_process()中的局部變量paws_reject的值為0。
  如果需要保持在FIN_WAIT_2狀態的時間小於等於TCP_TIMEWAIT_LEN,則會從FIN_WAIT_2狀態直接遷移到TIME_WAIT狀態,也就是使用描述TIME_WAIT狀態的sock結構代替當前的傳輸控制塊。雖然這時的sock結構處於TIME_WAIT結構,但是還要區分內部狀態,這個內部狀態存儲在inet_timewait_sock結構的tw_substate成員中。
  如果內部狀態為FIN_WAIT_2,tcp_timewait_state_process()中處理的關鍵代碼片段如下所示:
[cpp]
if (tw->tw_substate == TCP_FIN_WAIT2){ 
        /* Just repeat all the checks of tcp_rcv_state_process() */ 
 
 
        /* Out of window, send ACK */ 
        if (paws_reject || 
            !tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq, 
                   tcptw->tw_rcv_nxt, 
                   tcptw->tw_rcv_nxt + tcptw->tw_rcv_wnd)) 
            return TCP_TW_ACK; 
 
 
        if (th->rst) 
            goto kill; 
 
 
        if (th->syn && !before(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt)) 
            goto kill_with_rst; 
 
 
        /* Dup ACK? */ 
        if (!th->ack || 
            !after(TCP_SKB_CB(skb)->end_seq, tcptw->tw_rcv_nxt) || 
            TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq) { 
            inet_twsk_put(tw); 
            return TCP_TW_SUCCESS; 
        } 
 
 
        /* New data or FIN. If new data arrive after half-duplex close,
         * reset.
         */ 
        if (!th->fin || 
            TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1) { 
kill_with_rst: 
            inet_twsk_deschedule(tw, &tcp_death_row); 
            inet_twsk_put(tw); 
            return TCP_TW_RST; 
        } 
 
 
        /* FIN arrived, enter true time-wait state. */ 
        tw->tw_substate      = TCP_TIME_WAIT; 
        tcptw->tw_rcv_nxt = TCP_SKB_CB(skb)->end_seq; 
        if (tmp_opt.saw_tstamp) { 
            tcptw->tw_ts_recent_stamp = get_seconds(); 
            tcptw->tw_ts_recent      = tmp_opt.rcv_tsval; 
        } 
 
 
        /* I am shamed, but failed to make it more elegant.
         * Yes, it is direct reference to IP, which is impossible
         * to generalize to IPv6. Taking into account that IPv6
         * do not understand recycling in any case, it not
         * a big problem in practice. --ANK */ 
        if (tw->tw_family == AF_INET && 
            tcp_death_row.sysctl_tw_recycle && tcptw->tw_ts_recent_stamp && 
            tcp_v4_tw_remember_stamp(tw)) 
            inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout, 
                       TCP_TIMEWAIT_LEN); 
        else 
            inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN, 
                       TCP_TIMEWAIT_LEN); 
 
 
        return TCP_TW_ACK; 
    } 

 if (tw->tw_substate == TCP_FIN_WAIT2){
        /* Just repeat all the checks of tcp_rcv_state_process() */


        /* Out of window, send ACK */
        if (paws_reject ||
            !tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
                   tcptw->tw_rcv_nxt,
                   tcptw->tw_rcv_nxt + tcptw->tw_rcv_wnd))
            return TCP_TW_ACK;


        if (th->rst)
            goto kill;


        if (th->syn && !before(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt))
            goto kill_with_rst;


        /* Dup ACK? */
        if (!th->ack ||
            !after(TCP_SKB_CB(skb)->end_seq, tcptw->tw_rcv_nxt) ||
            TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq) {
            inet_twsk_put(tw);
            return TCP_TW_SUCCESS;
        }


        /* New data or FIN. If new data arrive after half-duplex close,
         * reset.
         */
        if (!th->fin ||
            TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1) {
kill_with_rst:
            inet_twsk_deschedule(tw, &tcp_death_row);
            inet_twsk_put(tw);
            return TCP_TW_RST;
        }


        /* FIN arrived, enter true time-wait state. */
        tw->tw_substate      = TCP_TIME_WAIT;
        tcptw->tw_rcv_nxt = TCP_SKB_CB(skb)->end_seq;
        if (tmp_opt.saw_tstamp) {
            tcptw->tw_ts_recent_stamp = get_seconds();
            tcptw->tw_ts_recent      = tmp_opt.rcv_tsval;
        }


        /* I am shamed, but failed to make it more elegant.
         * Yes, it is direct reference to IP, which is impossible
         * to generalize to IPv6. Taking into account that IPv6
         * do not understand recycling in any case, it not
         * a big problem in practice. --ANK */
        if (tw->tw_family == AF_INET &&
            tcp_death_row.sysctl_tw_recycle && tcptw->tw_ts_recent_stamp &&
            tcp_v4_tw_remember_stamp(tw))
            inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,
                       TCP_TIMEWAIT_LEN);
        else
            inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                       TCP_TIMEWAIT_LEN);


        return TCP_TW_ACK;
    }  如果TCP段序號不完全在接收窗口內,則返回TCP_TW_ACK,表示需要給對端發送ACK。
  如果在FIN_WAIT_2狀態下接收到的是RST包,則跳轉到kill標簽處處理,立即釋放timewait控制塊,並返回TCP_TW_SUCCESS。
  如果是SYN包,但是SYN包的序列號在要接收的序列號之前,則表示這是一個過期的SYN包,則跳轉到kill_with_rst標簽處處理,此時不僅會釋放TIME_WAIT傳輸控制塊,還會返回TCP_TW_RST,要給對端發送RST包。
  如果接收到DACK,則釋放timewait控制塊,並返回TCP_TW_SUCCESS。在這種情況下有一個判斷條件是看包的結束序列號和起始序列號相同時,會作為DACK處理,所以之後的處理是在數據包中的數據不為空的情況下處理。前面的處理中已經處理了SYN包、RST包的情況,接下來就剩以下三種情況:
  1、不帶FIN標志的數據包
  2、帶FIN標志,但是還包含數據
  3、FIN包,不包含數據
  如果是前兩種情況,則會調用inet_twsk_deschedule()釋放time_wait控制塊。inet_twsk_deschedule()中會調用到inet_twsk_put()減少time_wait控制塊的引用,在外層函數中再次調用inet_twsk_put()函數時,就會真正釋放time_wait控制塊。
  如果接收的是對端的FIN包,即第3種情況,則將time_wait控制塊的子狀態設置為TCP_TIME_WAIT,此時才是進入真正的TIME_WAIT狀態。然後根據TIME_WAIT的持續時間的長短來確定是加入到twcal_row隊列還是啟動一個定時器,最後會返回TCP_TW_ACK,給對端發送TCP連接關閉時最後的ACK包。
  到這裡,我們看到了對FIN_WAIT_2狀態(傳輸控制塊狀態為TIME_WAIT狀態下,但是子狀態為FIN_WAIT_2)的完整處理。
  接下來的處理才是對真正的TIME_WAIT狀態的處理,即子狀態也是TIME_WAIT。
  如果在TIME_WAIT狀態下,接收到ACK包(不帶數據)或RST包,並且包的序列號剛好是下一個要接收的序列號,由以下代碼片段處理:
[cpp]
if (!paws_reject && 
        (TCP_SKB_CB(skb)->seq == tcptw->tw_rcv_nxt && 
         (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq || th->rst))) { 
        /* In window segment, it may be only reset or bare ack. */ 
        if (th->rst) { 
            /* This is TIME_WAIT assassination, in two flavors.
             * Oh well... nobody has a sufficient solution to this
             * protocol bug yet.
             */ 
            if (sysctl_tcp_rfc1337 == 0) { 
kill: 
                inet_twsk_deschedule(tw, &tcp_death_row); 
                inet_twsk_put(tw); 
                return TCP_TW_SUCCESS; 
            } 
        } 
        inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN, 
                   TCP_TIMEWAIT_LEN); 
 
 
        if (tmp_opt.saw_tstamp) { 
            tcptw->tw_ts_recent      = tmp_opt.rcv_tsval; 
            tcptw->tw_ts_recent_stamp = get_seconds(); 
        } 
 
 
        inet_twsk_put(tw); 
        return TCP_TW_SUCCESS; 
    } 

 if (!paws_reject &&
        (TCP_SKB_CB(skb)->seq == tcptw->tw_rcv_nxt &&
         (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq || th->rst))) {
        /* In window segment, it may be only reset or bare ack. */
        if (th->rst) {
            /* This is TIME_WAIT assassination, in two flavors.
             * Oh well... nobody has a sufficient solution to this
             * protocol bug yet.
             */
            if (sysctl_tcp_rfc1337 == 0) {
kill:
                inet_twsk_deschedule(tw, &tcp_death_row);
                inet_twsk_put(tw);
                return TCP_TW_SUCCESS;
            }
        }
        inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                   TCP_TIMEWAIT_LEN);


        if (tmp_opt.saw_tstamp) {
            tcptw->tw_ts_recent      = tmp_opt.rcv_tsval;
            tcptw->tw_ts_recent_stamp = get_seconds();
        }


        inet_twsk_put(tw);
        return TCP_TW_SUCCESS;
    }  如果是RST包的話,並且系統配置sysctl_tcp_rfc1337(默認情況下為0,參見/proc/sys/net/ipv4/tcp_rfc1337)的值為0,這時會立即釋放time_wait傳輸控制塊,丟掉接收的RST包。
  如果是ACK包,則會啟動TIME_WAIT定時器後丟掉接收到的ACK包。
  接下來是對SYN包的處理。前面提到了,如果在TIME_WAIT狀態下接收到序列號比上一個連接的結束序列號大的SYN包,可以接受,並建立新的連接,下面這段代碼就是來處理這樣的情況:
[cpp]
if (th->syn && !th->rst && !th->ack && !paws_reject && 
    (after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) || 
     (tmp_opt.saw_tstamp && 
      (s32)(tcptw->tw_ts_recent - tmp_opt.rcv_tsval) < 0))) { 
    u32 isn = tcptw->tw_snd_nxt + 65535 + 2; 
    if (isn == 0) 
        isn++; 
    TCP_SKB_CB(skb)->when = isn; 
    return TCP_TW_SYN; 

    if (th->syn && !th->rst && !th->ack && !paws_reject &&
        (after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) ||
         (tmp_opt.saw_tstamp &&
          (s32)(tcptw->tw_ts_recent - tmp_opt.rcv_tsval) < 0))) {
        u32 isn = tcptw->tw_snd_nxt + 65535 + 2;
        if (isn == 0)
            isn++;
        TCP_SKB_CB(skb)->when = isn;
        return TCP_TW_SYN;
    }  當返回TCP_TW_SYN時,在tcp_v4_rcv()中會立即釋放time_wait控制塊,並且開始進行正常的連接建立過程。
  如果數據包不是上述幾種類型的包,可能的情況有:
  1、不是有效的SYN包。不考慮時間戳的話,就是序列號在上一次連接的結束序列號之前
  2、ACK包,起始序列號不是下一個要接收的序列號
  3、RST包,起始序列號不是下一個要接收的序列號
  4、帶數據的SKB包
  這幾種情況由以下代碼處理:
[cpp]
if (!th->rst) { 
      /* In this case we must reset the TIMEWAIT timer.
       *
       * If it is ACKless SYN it may be both old duplicate
       * and new good SYN with random sequence number <rcv_nxt.
       * Do not reschedule in the last case.
       */ 
      if (paws_reject || th->ack) 
          inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN, 
                     TCP_TIMEWAIT_LEN); 
 
 
      /* Send ACK. Note, we do not put the bucket,
       * it will be released by caller.
       */ 
      return TCP_TW_ACK; 
  } 
  inet_twsk_put(tw); 
  return TCP_TW_SUCCESS; 

  if (!th->rst) {
        /* In this case we must reset the TIMEWAIT timer.
         *
         * If it is ACKless SYN it may be both old duplicate
         * and new good SYN with random sequence number <rcv_nxt.
         * Do not reschedule in the last case.
         */
        if (paws_reject || th->ack)
            inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                       TCP_TIMEWAIT_LEN);


        /* Send ACK. Note, we do not put the bucket,
         * it will be released by caller.
         */
        return TCP_TW_ACK;
    }
    inet_twsk_put(tw);
    return TCP_TW_SUCCESS;  如果是RST包,即第3種情況,則直接返回TCP_TW_SUCCESS,丟掉RST包。
  如果帶有ACK標志的話,則會啟動TIME_WAIT定時器,然後給對端發送ACK。我們知道SYN包正常情況下不會設置ACK標志,所以如果是SYN包不會啟動TIME_WAIT定時器,只會給對端發送ACK,告訴對端已經收到SYN包,避免重傳,但連接應該不會繼續建立。
  還有一個細節需要提醒下,就是我們看到在返回TCP_TW_ACK時,沒有調用inet_twsk_put()釋放對time_wait控制塊的引用。這時因為在tcp_v4_rcv()中調用tcp_v4_timewait_ack()發送ACK時會用到time_wait控制塊,所以需要保持對time_wait控制塊的引用。在tcp_v4_timewait_ack()中發送完ACK後,會調用inet_twsk_put()釋放對time_wait控制塊的引用。
  OK,現在我們對TIME_WAIT狀態下接收到數據包的情況有了一個了解,知道內核會如何來處理這些包。但是看到的這些更多的是以服務器端的角度來看的,如果客戶端主動關閉連接的話,進入TIME_WAIT狀態的是客戶端。如果客戶端在TIME_WAIT狀態下重用端口號來和服務器建立連接,內核會如何處理呢?
  我編寫了一個測試程序:創建一個套接字,設置SO_REUSEADDR選項,建立連接後立即關閉,關閉後立即又重復同樣的過程,發現在第二次調用connect()的時候返回EADDRNOTAVAIL錯誤。這個測試程序很容易理解,寫起來也很容易,就不貼出來了。
  要找到這個錯誤是怎麼返回的,需要從TCP層的連接函數tcp_4_connect()開始。在tcp_v4_connect()中沒有顯示返回EADDRNOTAVAIL錯誤的地方,可能的地方就是在調用inet_hash_connect()返回的。為了確定是不是在inet_hash_connect()中返回的,使用systemtap編寫了一個腳本,發現確實是在這個函數中返回的-99錯誤(EADDRNOTAVAIL的值為99)。其實這個通過代碼也可以看出來,在這個函數之前會先查找目的主機的路由緩存項,調用的是ip_route_connect()函數,跟著這個函數的調用軌跡,沒有發現返回EADDRNOTAVAIL錯誤的地方。
  inet_hash_connect()函數只是對__inet_hash_connect()函數進行了簡單的封裝。在__inet_hash_connect()中如果已綁定了端口號,並且是和其他傳輸控制塊共享綁定的端口號,則會調用check_established參數指向的函數來檢查這個綁定的端口號是否可用,代碼如下所示:
[cpp]
int __inet_hash_connect(struct inet_timewait_death_row *death_row, 
        struct sock *sk, u32 port_offset, 
        int (*check_established)(struct inet_timewait_death_row *, 
            struct sock *, __u16, struct inet_timewait_sock **), 
        void (*hash)(struct sock *sk)) 

    struct inet_hashinfo *hinfo = death_row->hashinfo; 
    const unsigned short snum = inet_sk(sk)->num; 
    struct inet_bind_hashbucket *head; 
    struct inet_bind_bucket *tb; 
    int ret; 
    struct net *net = sock_net(sk); 
 
    if (!snum) { 
        ...... 
    } 
 
    head = &hinfo->bhash[inet_bhashfn(net, snum, hinfo->bhash_size)]; 
    tb  = inet_csk(sk)->icsk_bind_hash; 
    spin_lock_bh(&head->lock); 
    if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) { 
        hash(sk); 
        spin_unlock_bh(&head->lock); 
        return 0; 
    } else { 
        spin_unlock(&head->lock); 
        /* No definite answer... Walk to established hash table */ 
        ret = check_established(death_row, sk, snum, NULL); 
out: 
        local_bh_enable(); 
        return ret; 
    } 

int __inet_hash_connect(struct inet_timewait_death_row *death_row,
        struct sock *sk, u32 port_offset,
        int (*check_established)(struct inet_timewait_death_row *,
            struct sock *, __u16, struct inet_timewait_sock **),
        void (*hash)(struct sock *sk))
{
    struct inet_hashinfo *hinfo = death_row->hashinfo;
    const unsigned short snum = inet_sk(sk)->num;
    struct inet_bind_hashbucket *head;
    struct inet_bind_bucket *tb;
    int ret;
    struct net *net = sock_net(sk);

    if (!snum) {
        ......
    }

    head = &hinfo->bhash[inet_bhashfn(net, snum, hinfo->bhash_size)];
    tb  = inet_csk(sk)->icsk_bind_hash;
    spin_lock_bh(&head->lock);
    if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) {
        hash(sk);
        spin_unlock_bh(&head->lock);
        return 0;
    } else {
        spin_unlock(&head->lock);
        /* No definite answer... Walk to established hash table */
        ret = check_established(death_row, sk, snum, NULL);
out:
        local_bh_enable();
        return ret;
    }
}  (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next)這個判斷條件就是用來判斷是不是只有當前傳輸控制塊在使用已綁定的端口,條件為false時,會執行else分支,檢查是否可用。這麼看來,調用bind()成功並不意味著這個端口就真的可以用。
  check_established參數對應的函數是__inet_check_established(),在inet_hash_connect()中可以看到。在上面的代碼中我們還注意到調用check_established()時第三個參數為NULL,這在後面的分析中會用到。
  __inet_check_established()函數中,會分別在TIME_WAIT傳輸控制塊和除TIME_WIAT、LISTEN狀態外的傳輸控制塊中查找是已綁定的端口是否已經使用,代碼片段如下所示:
[cpp]
/* called with local bh disabled */ 
static int __inet_check_established(struct inet_timewait_death_row *death_row, 
                    struct sock *sk, __u16 lport, 
                    struct inet_timewait_sock **twp) 

    struct inet_hashinfo *hinfo = death_row->hashinfo; 
    struct inet_sock *inet = inet_sk(sk); 
    __be32 daddr = inet->rcv_saddr; 
    __be32 saddr = inet->daddr; 
    int dif = sk->sk_bound_dev_if; 
    INET_ADDR_COOKIE(acookie, saddr, daddr) 
    const __portpair ports = INET_COMBINED_PORTS(inet->dport, lport); 
    struct net *net = sock_net(sk); 
    unsigned int hash = inet_ehashfn(net, daddr, lport, saddr, inet->dport); 
    struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash); 
    spinlock_t *lock = inet_ehash_lockp(hinfo, hash); 
    struct sock *sk2; 
    const struct hlist_nulls_node *node; 
    struct inet_timewait_sock *tw; 
 
    spin_lock(lock); 
 
    /* Check TIME-WAIT sockets first. */ 
    sk_nulls_for_each(sk2, node, &head->twchain) { 
        tw = inet_twsk(sk2); 
 
 
        if (INET_TW_MATCH(sk2, net, hash, acookie, 
                    saddr, daddr, ports, dif)) { 
            if (twsk_unique(sk, sk2, twp)) 
                goto unique; 
            else 
                goto not_unique; 
        } 
    } 
    tw = NULL; 
 
    /* And established part... */ 
    sk_nulls_for_each(sk2, node, &head->chain) { 
        if (INET_MATCH(sk2, net, hash, acookie, 
                    saddr, daddr, ports, dif)) 
            goto not_unique; 
    } 
 
unique: 
    ...... 
    return 0; 
 
not_unique: 
    spin_unlock(lock); 
    return -EADDRNOTAVAIL; 

/* called with local bh disabled */
static int __inet_check_established(struct inet_timewait_death_row *death_row,
                    struct sock *sk, __u16 lport,
                    struct inet_timewait_sock **twp)
{
    struct inet_hashinfo *hinfo = death_row->hashinfo;
    struct inet_sock *inet = inet_sk(sk);
    __be32 daddr = inet->rcv_saddr;
    __be32 saddr = inet->daddr;
    int dif = sk->sk_bound_dev_if;
    INET_ADDR_COOKIE(acookie, saddr, daddr)
    const __portpair ports = INET_COMBINED_PORTS(inet->dport, lport);
    struct net *net = sock_net(sk);
    unsigned int hash = inet_ehashfn(net, daddr, lport, saddr, inet->dport);
    struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);
    spinlock_t *lock = inet_ehash_lockp(hinfo, hash);
    struct sock *sk2;
    const struct hlist_nulls_node *node;
    struct inet_timewait_sock *tw;

    spin_lock(lock);

    /* Check TIME-WAIT sockets first. */
    sk_nulls_for_each(sk2, node, &head->twchain) {
        tw = inet_twsk(sk2);


        if (INET_TW_MATCH(sk2, net, hash, acookie,
                    saddr, daddr, ports, dif)) {
            if (twsk_unique(sk, sk2, twp))
                goto unique;
            else
                goto not_unique;
        }
    }
    tw = NULL;

    /* And established part... */
    sk_nulls_for_each(sk2, node, &head->chain) {
        if (INET_MATCH(sk2, net, hash, acookie,
                    saddr, daddr, ports, dif))
            goto not_unique;
    }

unique:
    ......
    return 0;

not_unique:
    spin_unlock(lock);
    return -EADDRNOTAVAIL;
}  可以看到返回EADDRNOTVAIL錯誤的有兩種情況:
   1、在TIME_WAIT傳輸控制塊中找到匹配的端口,並且twsk_unique()返回true時
   2、在除TIME_WAIT和LISTEN狀態外的傳輸塊中存在匹配的端口。
  第二種情況很好容易理解了,只要狀態在FIN_WAIT_1、ESTABLISHED等的傳輸控制塊使用的端口和要查找的匹配,就會返回EADDRNOTVAIL錯誤。第一種情況還要取決於twsk_uniqueue()的返回值,所以接下來我們看twsk_uniqueue()中什麼情況下會返回true。
  如果是TCP套接字,twsk_uniqueue()中會調用tcp_twsk_uniqueue()來判斷,返回true的條件如下所示:
[cpp]
int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp) 

    const struct tcp_timewait_sock *tcptw = tcp_twsk(sktw); 
    struct tcp_sock *tp = tcp_sk(sk); 
 
 
    if (tcptw->tw_ts_recent_stamp && 
        (twp == NULL || (sysctl_tcp_tw_reuse && 
                 get_seconds() - tcptw->tw_ts_recent_stamp > 1))) { 
        ...... 
        return 1; 
    } 
 
 
    return 0; 

int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
    const struct tcp_timewait_sock *tcptw = tcp_twsk(sktw);
    struct tcp_sock *tp = tcp_sk(sk);


    if (tcptw->tw_ts_recent_stamp &&
        (twp == NULL || (sysctl_tcp_tw_reuse &&
                 get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
        ......
        return 1;
    }


    return 0;
}我們前面提到過,__inet_hash_connect()函數調用check_established指向的函數時第三個參數為NULL,所以現在我們只需要關心tcptw->tw_ts_recent_stamp是否非零,只要這個值非零,tcp_twsk_unique()就會返回true, 在上層connect()函數中就會返回EADDRNOTVAIL錯誤。tcptw->tw_ts_recent_stamp存儲的是最近接收到段的時間戳值,所以正常情況下這個值不會為零。當然也可以通過調整系統的參數,讓這個值可以為零,這不是本文討論的重點,感興趣的可以參考tcp_v4_connect()中的代碼進行修改。
  在導致返回EADDRNOTVAIL的兩種情況中,第一種情況可以有辦法避免,但是如果的第二次建立連接的時間和第一次關閉連接之間的時間間隔太小的話,此時第一個連接可能處在FIN_WAIT_1、FIN_WAIT_2等狀態,此時沒有系統參數可以用來避免返回EADDRNOTVAIL。如果你還是想無論如何都要在很短的時間內重用客戶端的端口,這樣也有辦法,要麼是用kprobe機制,要麼用systemtap腳本,改變__inet_check_established()函數的返回值。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved