天天看點

TCP連接配接建立系列 — 用戶端的端口選取和重用

主要内容:connect()時的端口選取和端口重用。

核心版本:3.15.2

我的部落格:http://blog.csdn.net/zhangskd

端口選取

connect()時本地端口是如何選取的呢? 

如果使用者已經綁定了端口,就使用綁定的端口。

如果使用者沒有綁定端口,則讓系統自動選取,政策如下:

1. 擷取端口的取值區間,以及區間内端口的個數。

2. 根據初始偏移量,從端口區間内的某個端口開始,周遊整個區間。

     2.1 如果端口是保留的,直接跳過。

     2.2 如果端口已經被使用了。

            2.2.1 不允許複用已經被bind()的端口。

            2.2.2 檢查端口是否能被重用,可以的話就重用此端口。

    2.3 如果端口沒有被使用過,就選擇此端口。

當沒有端口可用時,會報如下錯誤:

-EADDRNOTAVAIL

包含兩種場景:

1. 端口區間内沒有未使用過的端口,且正在使用的端口都不允許複用。

2. 記憶體不夠,無法建立端口的存儲結構。

/* Bind a port for a connect operation and hash it. */
int inet_hash_connect (struct inet_timewait_death_row *death_row, struct sock *sk)
{
    return __inet_hash_connect(death_row, sk, inet_sk_port_offset(sk), 
        __inet_check_established, __inet_hash_nolisten);
}
           

inet_hash_connect()參數的含義如下:

death_row:TIME_WAIT socket的管理結構。

inet_sk_port_offset():根據源IP、目的IP、目的端口,采用MD5計算出一個随機數,作為端口的初始偏移值。

__inet_check_established():判斷正在使用中的端口是否允許重用。

__inet_hash_nolisten():根據四元組,計算sk在ehash哈希表中的索引,把sk鍊入ehash哈希表。

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 **),
    int (*hash)(struct sock *sk, struct inet_timewait_sock *twp))
{
    struct inet_hashinfo *hinfo = death_row->hashinfo; /* tcp_hashinfo */
    const unsigned short snum = inet_sk(sk)->inet_num; /* 本端端口 */
    struct inet_bind_hashbucket *head;
    struct inet_bind_bucket *tb;
    int ret;
    struct net *net = sock_net(sk);
    int twrefcnt = 1;

    /* snum為0時,表示使用者沒有綁定端口,預設讓系統自動選取端口 */
    if (! snum) {
        int i, remaining, low, high, port;
        static u32 hint; /* 用于儲存上次查找的位置 */
        u32 offset = hint + port_offset;
        struct inet_timewait_sock *tw = NULL;
        
        /* 系統自動配置設定時,擷取端口号的取值範圍 */
        inet_get_local_port_range(net, &low, &high);
        remaining = (high - low) + 1; /* 取值範圍内端口号的個數 */
 
        local_bh_disable();
        for (i = 1; i <= remaining; i++) {
            /* 根據MD5計算得到的port_offset值,以及hint,擷取範圍内的一個端口 */
            port = low + (i + offset) % remaining; 
 
            /* 如果此端口号屬于保留的,那麼直接跳過 */
            if (inet_is_reserved_local_port(port))
                continue;

            /* 根據端口号,找到所在的哈希桶 */
            head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)];
            spin_lock(&head->lock); /* 鎖住此哈希桶 */

            /* 從頭周遊哈希桶 */
            inet_bind_bucket_for_each(tb, &head->chain) {
                /* 如果此端口已經被使用了 */
                if (net_eq(ib_net(tb), net) && tb->port == port) {

                    /* 不允許使用已經被bind()綁定的端口,無論此端口是否能夠被複用 */
                    if (tb->fastreuse >= 0 || tb->fastreuseport >= 0)
                        goto next_port;

                    WARN_ON(hlist_empty(&tb->owners));

                   /* 檢查端口是否允許重用 */
                    if (! check_established(death_row, sk, port, &tw))
                        goto ok; /* 成功,該端口可以被重複使用 */
                    goto next_port; /* 失敗 */
                }
            }
 
            /* 走到這裡,表示該端口尚未被使用。
             * 建立一個inet_bind_bucket執行個體,并把它加入到哈希桶中。
             */
            tb = inet_bind_bucket_create(hinfo->bind_bucket, cachep, net, head, port);

            /* 如果記憶體不夠,則退出端口選擇。
             * 會導緻connect()失敗,傳回-EADDRNOTAVAIL。
             */
            if (! tb) {
                spin_unlock(&head->lock);
                break;
            }
 
            tb->fastreuse = -1;
            tb->fastreuseport = -1;
            goto ok;
 
            next_port:
                spin_unlock(&head->lock);
        } /* end of for */

        local_bh_enable();
 
        /* 有兩種可能:記憶體不夠、端口區間内的端口号用光 */
        return -EADDRNOTAVAIL; /* Cannot assign requested address */

 ok:
        hint += i; /* 下一次connect()時,查找端口增加了這段偏移 */
 
        /* Head lock still held and bh's disabled.
         * 把tb指派給icsk->icsk_bind_hash,更新inet->inet_num,把sock鍊入tb->owners哈希鍊中。
         * 更新該端口的綁定次數,系統總的端口綁定次數。
         */
        inet_bind_hash(sk, tb, port);
 
        /* 如果sk尚未鍊入ehash哈希表中 */
        if (sk_unhashed(sk)) {
            inet_sk(sk)->inet_sport = htons(port); /* 儲存本地端口 */
            twrefcnt += hash(sk, tw); /* 把sk鍊入到ehash哈希表中,把tw從ehash表中删除 */
        }

        if (tw)
            twrefcnt += inet_twsk_bind_unhash(tw, hinfo); /* 把tw從該端口的使用者連結清單中删除 */

        spin_unlock(&head->lock);

        if (tw) {
            /* 把tw從tcp_death_row、ehash、bhash的哈希表中删除,更新tw的引用計數 */
            inet_twsk_deschedule(tw, death_row);

            while (twrefcnt) {
                twrefcnt--;
                inet_twsk_put(tw); /* 釋放tw結構體 */
            }
        }
 
        ret = 0;
        goto out;
    }
    
    /* 走到這裡,表示使用者已經自己綁定了端口 */
    head = &hinfo->bhash[inet_bhashfn(net, snum, hinfo->bhash_size)]; /* 端口所在的哈希桶 */
    tb = inet_csk(sk)->icsk_bind_hash; /* 端口的存儲執行個體 */

    spin_lock_bh(&head->lock);

    /* 如果sk是此端口的使用者隊列的第一個節點 */
    if (sk_head(&tb->owners) == sk && ! sk->sk_bind_node.next) {
        hash(sk, NULL); /* 計算sk在ehash中的索引,指派給sk->sk_hash,把sk鍊入到ehash表中 */
        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在ehash哈希表中的索引,儲存到sk->sk_hash中,然後把sk鍊入ehash哈希表。

int __inet_hash_nolisten(struct sock *sk, struct inet_timewait_sock *tw)
{
    struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
    struct hlist_nulls_head *list;
    spinlock_t *lock;
    struct inet_ehash_bucket *head;
    int twrefcnt = 0; 

    WARN_ON(! sk_unhashed(sk));

    sk->sk_hash = inet_sk_ehashfn(sk); /* 根據四元組,計算在ehash哈希表中的索引 */
    head = inet_ehash_bucket(hashinfo, sk->sk_hash); /* 根據索引,找到對應的哈希桶 */
    list = &head->chain;
    lock = inet_ehash_lockp(hashinfo, sk->sk_hash); /* 根據索引,找到對應哈希桶的鎖 */

    spin_lock(lock);
    __sk_nulls_add_node_rcu(sk, list); /* 把sk->sk_null_node鍊傳入連結表 */ 

    if (tw) { /* 如果複用了TIME_WAIT sock的端口 */
        WARN_ON(sk->sk_hash != tw->tw_hash);

        /* 把tw從ehash表中删除,傳回值如果為1,表示釋放鎖之後,需要調用inet_twsk_put() */
        twrefcnt = inet_twsk_unhash(tw); 
    }

    spin_unlock(lock);
    sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1); /* 增加TCP協定的引用計數 */

    return twrefcnt;
}
           

通過源IP、目的IP、源端口、目的端口,計算得到一個32位的哈希值。

指派給sk->sk_hash,作為索引,用于定位ehash中的哈希桶。

static unsigned int inet_sk_ehashfn(const struct sock *sk)
{
    const struct inet_sock *inet = inet_sk(sk);
    const __be32 laddr = inet->inet_rcv_saddr;
    const __u16 lport = inet->inet_num;
    const __be32 faddr = inet->inet_daddr;
    const __be16 fport = inet->inet_dport;
    struct net *net = sock_net(sk);

    return inet_ehashfn(net, laddr, lport, faddr, fport);
}

static inline unsigned int inet_ehashfn (struct net *net, const __be32 laddr, const __u16 lport,
    const __be32 faddr, const __be16 fport)
{
    return jhash_3words((__force __u32) laddr, (__force __u32) faddr,
        ((__u32) lport) << 16 | (__force __u32) fport,
        inet_ehash_secret + net_hash_mix(net));
} 
 
u32 inet_ehash_secret __read_mostly;

/* inet_ehash_secret must be set exactly once */
void build_ehash_secret(void)
{
    u32 rnd;
    do {
        get_random_bytes(&rnd, sizeof(rnd));
    } while (rnd == 0);
 
    /* cmpxchg(void *ptr, unsigned long old, unsigned long new)
     * 比較*ptr和old:
     * 如果相等,則将new寫入*ptr,傳回old。
     * 如果不相等,傳回*ptr。
     * 這裡用于確定inet_ehash_secret隻被寫入一次。
     */
    cmpxchg(&inet_ehash_secret, 0, rnd);
} 
           

端口重用

__inet_check_established()用來檢查已經在使用中的端口是否可以重用。

如果在ehash哈希表中沒有找到一條四元組相同的連接配接,這個端口當然允許重用。

如果在ehash哈希表中找到一條完全一樣的連接配接,即四元組相同、綁定的裝置相同,

那麼還要符合以下條件:

1. 連接配接的狀态為TCP_TIME_WAIT。

2. 使用了TCP_TIMESTAMP選項。

3. 使用tcp_tw_reuse,并且此連接配接最近收到資料包的時間在1s以前。

/* 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->inet_rcv_saddr;
    __be32 saddr = inet->inet_daddr;
    int dif = sk->sk_bound_dev_if;
    /* 根據目的IP和源IP,生成一個64位的值 */
    INET_ADDR_COOKIE(acookie, saddr, daddr);
    /* 根據目的端口和源端口,生成一個32位的值 */
    const __portpair ports = INET_COMBINED_PORTS(inet->inet_dport, lport);
    struct net *net = sock_net(sk);
    /* 通過連接配接的四元組,計算得到一個哈希值 */
    unsigned int hash = inet_ehashfn(net, daddr, lport, saddr, inet->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;
    int twrefcnt = 0;

    spin_lock(lock); /* 鎖住哈希桶 */

    /* Check TIME-WAIT sockets first. 周遊哈希桶 */
    sk_nulls_for_each(sk2, node, &head->chain) {
        if (sk2->sk_hash != hash) /* 先比較哈希值,相同的才繼續比對 */
            continue;
 
        /* 如果連接配接完全比對:四元組相同、綁定的裝置相同 */
        if (likely(INET_MATCH(sk2, net, acookie, saddr, daddr, ports, dif))) {

            /* 此版本把ESTABLISHED和TIME_WAIT狀态的連接配接放在同一個哈希桶中,
             * 是以需要判斷連接配接狀态是否為TIME_WAIT。
             */
            if (sk2->sk_state == TCP_TIME_WAIT) {
                tw = inet_twsk(sk2);

                /* 滿足以下條件就允許複用:
                 * 1. 使用TCP Timestamp選項。
                 * 2. 符合以下任一情況即可:
                 *     2.1 twp == NULL,主動建立連接配接時,如果使用者已經綁定端口了,那麼會符合。
                 *     2.2 啟用tcp_tw_reuse,且距離上次收到資料包的時間大于1s。
                 */
                if (twsk_unique(sk, sk2, twp)
                    break;
            }

            goto not_unique;
        }
    }

    /* 走到這裡有兩種情況:
     * 1. 周遊玩哈希桶,都沒有找到四元組一樣的。
     * 2. 找到了四元組一樣的,但是符合重用的條件。
     */
 
    /* Must record num and sport now. Otherwise we will see
     * in hash table socket with a funny identity.
     */
    inet->inet_num = lport; /* 儲存源端口 */
    inet->inet_sport = htons(lport);
    sk->sk_hash = hash; /* 儲存ehash表的哈希值 */

    WARN_ON(! sk_unhashed(sk)); /* 要求新連接配接sk還沒被鍊入ehash哈希表中 */
    __sk_nulls_add_node_rcu(sk, &head->chain); /* 把此sk鍊入ehash哈希表中 */
 
   /* tw不為空,說明已經找到一條完全比對的、處于TIME_WAIT狀态的連接配接,
    * 并且經過判斷,此連接配接的端口可以複用。
    */
    if (tw) {
        twrefcnt = inet_twsk_unhash(tw); /* 把此twsk從ehash表中删除 */
        NET_INC_STATS_BH(net, LINUX_MIB_TIMEWAITRECYCLED);
    }

    spin_unlock(lock); /* 釋放哈希桶的鎖 */

    if (twrefcnt) /* 如果需要釋放twsk */
        inet_twsk_put(tw); /* 釋放twsk執行個體 */

    sock_prot_inuse_add(sock_net(sk), sk->s_prot, 1); /* 增加TCP協定的引用計數 */
 
    /* 如果twp不為NULL,各種哈希表删除操作,就交給調用函數來處理 */
    if (twp) {
        *twp = tw;
    } else if (tw) {
        /* 把tw從death_row、ehash、bhash的哈希表中删除,更新tw的引用計數 */
        inet_twsk_deschedule(tw, death_row);
        inet_twsk_put(tw); /* 釋放tw結構體 */
    }

    return 0;

not_unique:
    spin_unlock(lock);
    return -EADDRNOTAVAIL;
}  
           

端口初始偏移值

根據源IP、目的IP、目的端口,采用MD5計算出一個數值,即傳回值offset。

static inline u32 inet_sk_port_offset (const struct sock *sk)
{
    const struct inet_sock *inet = inet_sk(sk);
    return secure_ipv4_port_ephemeral(inet->inet_rcv_saddr, inet->inet_daddr, inet->inet_dport);
}

#define MD5_DIGEST_WORDS 4
#define MD5_MESSAGE_BYTES 64
#define NET_SECRET_SIZE (MD5_MESSAGE_BYTES / 4)
static u32 net_secret[NET_SECRET_SIZE] ____cacheline_aligned;

static __always_inline void net_secret_init(void)
{
    net_get_random_once(net_secret, sizeof(net_secret)); /* 隻取一次随機數 */
}

u32 secure_ipv4_port_ephemeral(__be32 saddr, __be32 daddr, __be16 dport)
{
    u32 hash[MD5_DIGEST_WORDS];
    net_secret_init(); /* 随機生成MD5消息 */

    hash[0] = (__force u32) saddr;
    hash[1] = (__force u32) daddr;
    hash[2] = (__force u32) dport ^ net_secret[14];
    hash[3] = net_secret[15];

    md5_transform(hash, net_secret); /* 計算MD5值,結果儲存在hash數組中 */

    return hash[0];
}

           

繼續閱讀