天天看點

Linux核心網絡協定棧5-socket端口管理

一、前情回顧

上一節《socket位址綁定》中提到,應用程式傳遞過來的端口在核心中需要檢查端口是否可用:

if (sk->sk_prot->get_port(sk, snum)) {  

    inet->saddr = inet->rcv_saddr = 0;  

    err = -EADDRINUSE;  

    goto out_release_sock;  

}  

按照前面的例子來分析,這裡是調用了tcp_prot結構變量中的get_prot函數指針,該函數位于net/ipv4/Inet_connection_sock.c中;這個函數比較長,也是我們今天要分析的重點;

二、端口的管理

1、端口管理資料結構

Linux核心将所有socket使用時的端口通過一個哈希表來管理,該哈希表存放在全局變量tcp_hashinfo中,通過tcp_prot變量的h成員引用,該成員是一個聯合類型;對于tcp套接字類型,其引用存放在h. hashinfo成員中;下面是tcp_hashinfo的結構體類型:

struct inet_hashinfo {  

       struct inet_ehash_bucket  *ehash;  

       rwlock_t                     *ehash_locks;  

       unsigned int                ehash_size;  

       unsigned int                ehash_locks_mask;  

       struct inet_bind_hashbucket    *bhash;//管理端口的哈希表  

       unsigned int                bhash_size;//端口哈希表的大小  

       struct hlist_head         listening_hash[INET_LHTABLE_SIZE];  

       rwlock_t                     lhash_lock ____cacheline_aligned;  

       atomic_t                     lhash_users;  

       wait_queue_head_t           lhash_wait;  

       struct kmem_cache                 *bind_bucket_cachep;//哈希表結構高速緩存  

}  

端口管理相關的,目前可以隻關注加注釋的這三個成員,其中bhash為已經哈希表結構,bhash_size為哈希表的大小;所有哈希表中的節點記憶體都是在bind_bucket_cachep高速緩存中配置設定;

下面看一下inet_bind_hashbucket結構體:

struct inet_bind_hashbucket {  

       spinlock_t            lock;  

       struct hlist_head  chain;  

};  

struct hlist_head {  

       struct hlist_node *first;  

struct hlist_node {  

       struct hlist_node *next, **pprev;  

inet_bind_hashbucket是哈希桶結構,lock成員是用于操作時對桶進行加鎖,chain成員是相同哈希值的節點的連結清單;示意圖如下:

Linux核心網絡協定棧5-socket端口管理

2、預設端口的配置設定

當應用程式沒有指定端口時(如socket用戶端連接配接到服務端時,會由核心從可用端口中配置設定一個給該socket);

看看下面的代碼(參見net/ipv4/Inet_connection_sock.c: inet_csk_get_port()函數):

if (!snum) {  

    int remaining, rover, low, high;  

    inet_get_local_port_range(&low, &high);  

    remaining = (high - low) + 1;  

    rover = net_random() % remaining + low;  

    do {  

        head = &hashinfo->bhash[inet_bhashfn(rover, hashinfo->bhash_size)];  

        spin_lock(&head->lock);  

        inet_bind_bucket_for_each(tb, node, &head->chain)  

            if (tb->ib_net == net && tb->port == rover)  

                goto next;  

        break;  

    next:  

        spin_unlock(&head->lock);  

        if (++rover > high)  

            rover = low;  

    } while (--remaining > 0);  

    ret = 1;  

    if (remaining <= 0)  

        goto fail;  

    snum = rover;  

這裡,随機端口的範圍是32768~61000;上面代碼的邏輯如下:

1)  從[32768, 61000]中随機取一個端口rover;

2)  計算該端口的hash值,然後從全局變量tcp_hashinfo的哈希表bhash中取出相同哈希值的連結清單head;

3)  周遊連結清單head,檢查每個節點的網絡裝置是否和目前網絡設定相同,同時檢查節點的端口是否和rover相同;

4)  如果相同,表明端口被占用,繼續下一個端口;如果和連結清單head中的節點都不相同,則跳出循環,繼續後面的邏輯;

inet_bind_bucket_foreach宏利用《建立socket》一文中提到的container_of宏來實作 的,大家可以自己看看;

3、端口重用

當應用程式指定端口時,參考下面的源代碼:

else {  

    head = &hashinfo->bhash[inet_bhashfn(snum, hashinfo->bhash_size)];  

    spin_lock(&head->lock);  

    inet_bind_bucket_for_each(tb, node, &head->chain)  

        if (tb->ib_net == net && tb->port == snum)  

            goto tb_found;  

此時同樣會檢查該端口有沒有被占用;如果被占用,會檢查端口重用(跳轉到tb_found):

tb_found:  

       if (!hlist_empty(&tb->owners)) {  

              if (tb->fastreuse > 0 &&  

                  sk->sk_reuse && sk->sk_state != TCP_LISTEN) {  

                     goto success;  

              } else {  

                     ret = 1;  

                     if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb))  

                            goto fail_unlock;  

              }  

       }  

1)    端口節點結構

struct inet_bind_bucket {  

       struct net             *ib_net;//端口所對應的網絡設定  

       unsigned short            port;//端口号  

       signed short         fastreuse;//是否可重用  

       struct hlist_node  node;//作為bhash中chain連結清單的節點  

       struct hlist_head  owners;//綁定在該端口上的socket連結清單  

前面提到的哈希桶結構中的chain連結清單中的每個節點,其宿主結構體是inet_bind_bucket,該結構體通過成員node鍊傳入連結表;

2)    檢查端口是否可重用

這裡涉及到兩個屬性,一個是socket的sk_reuse,另一個是inet_bind_bucket的fastreuse;

sk_reuse可以通過setsockopt()庫函數進行設定,其值為0或1,當為1時,表示當一個socket進入TCP_TIME_WAIT狀态(連接配接關閉已經完成)後,它所占用的端口馬上能夠被重用,這在調試伺服器時比較有用,重新開機程式不用進行等待;而fastreuse代表該端口是否允許被重用:

l 當該端口第一次被使用時(owners為空),如果sk_reuse為1且socket狀态不為TCP_LISTEN,則設定fastreuse為1,否則設定為0;

l 當該端口同時被其他socket使用時(owners不為空),如果目前端口能被重用,但是目前socket的sk_reuse為0或其狀态為TCP_LISTEN,則将fastreuse設定為0,标記為不能重用;

3)    當不能重用時,再次檢查沖突

此時會調用inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb)再次檢查端口是否沖突;回想《建立socket》一文中提到,建立socket成功後,要使用相應的協定來初始化socket,對于tcp協定來說,其初始化方法是net/ipv4/Tcp_ipv4.c:tcp_v4_init_sock(),其中就做了如下一步的設定:

icsk->icsk_af_ops = &ipv4_specific;  

struct inet_connection_sock_af_ops ipv4_specific = {  

       .queue_xmit    = ip_queue_xmit,  

       .send_check          = tcp_v4_send_check,  

       .rebuild_header      = inet_sk_rebuild_header,  

       .conn_request        = tcp_v4_conn_request,  

       .syn_recv_sock     = tcp_v4_syn_recv_sock,  

       .remember_stamp        = tcp_v4_remember_stamp,  

       .net_header_len     = sizeof(struct iphdr),  

       .setsockopt      = ip_setsockopt,  

       .getsockopt     = ip_getsockopt,  

       .addr2sockaddr      = inet_csk_addr2sockaddr,  

       .sockaddr_len        = sizeof(struct sockaddr_in),  

       .bind_conflict          = inet_csk_bind_conflict,  

#ifdef CONFIG_COMPAT  

       .compat_setsockopt = compat_ip_setsockopt,  

       .compat_getsockopt = compat_ip_getsockopt,  

#endif  

下面看看這裡再次檢查沖突的代碼:

int inet_csk_bind_conflict(const struct sock *sk,  

                        const struct inet_bind_bucket *tb)  

{  

       const __be32 sk_rcv_saddr = inet_rcv_saddr(sk);  

       struct sock *sk2;  

       struct hlist_node *node;  

       int reuse = sk->sk_reuse;  

       sk_for_each_bound(sk2, node, &tb->owners) {  

              if (sk != sk2 &&  

                  !inet_v6_ipv6only(sk2) &&  

                  (!sk->sk_bound_dev_if ||  

                   !sk2->sk_bound_dev_if ||  

                   sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {  

                     if (!reuse || !sk2->sk_reuse ||  

                         sk2->sk_state == TCP_LISTEN) {  

                            const __be32 sk2_rcv_saddr = inet_rcv_saddr(sk2);  

                            if (!sk2_rcv_saddr || !sk_rcv_saddr ||  

                                sk2_rcv_saddr == sk_rcv_saddr)  

                                   break;  

                     }  

       return node != NULL;  

上面函數的邏輯是:從owners中周遊綁定在該端口上的socket,如果某socket跟目前的socket不是同一個,并且是綁定在同一個網絡裝置接口上的,并且它們兩個之中至少有一個的sk_reuse表示自己的端口不能被重用或該socket已經是TCP_LISTEN狀态了,并且它們兩個之中至少有一個沒有指定接收IP位址,或者兩個都指定接收位址,但是接收位址是相同的,則沖突産生,否則不沖突。

也就是說,不使用同一個接收位址的socket可以共用端口号,綁定在不同的網絡裝置接口上的socket可以共用端口号,或者兩個socket都表示自己可以被重用,并且還不在TCP_LISTEN狀态,則可以重用端口号。

4、建立inet_bind_bucket

當在bhash中沒有找到指定的端口時,需要建立新的桶節點,然後挂入bhash中:

tb_not_found:  

       ret = 1;  

       if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,  

                                   net, head, snum)) == NULL)  

              goto fail_unlock;  

       if (hlist_empty(&tb->owners)) {  

              if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)  

                     tb->fastreuse = 1;  

              else  

                     tb->fastreuse = 0;  

       } else if (tb->fastreuse &&  

                 (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))  

              tb->fastreuse = 0;  

success:  

       if (!inet_csk(sk)->icsk_bind_hash)  

              inet_bind_hash(sk, tb, snum);  

有興趣的可以自己看看這段代碼的實作,這裡就不再展開了。

繼續閱讀