天天看點

Linux TCP/IP協定棧之Socket的實作分析(socket bind)

當建立了一個Socket 套接字後,對于伺服器來說,接下來的工作,就是調用 bind(2)為伺服器指明本位址、

協定端口号,常常可以看到這樣的代碼:

strut sockaddr_in sin;

sin.sin_family = AF_INET;

sin.sin_addr.s_addr = xxx;

sin.sin_port = xxx;

bind(sock, (struct sockaddr *)&sin, sizeof(sin));

從這個系統調用中可以知道當進行 SYS_BIND 操作的時候:

1. 對于 AF_INET 協定簇來講,其位址格式是 strut sockaddr_in,而對于 socket 來講,strut sockaddr結構

表示的位址格式實作了更高層次的抽像,因為每種協定長簇的位址不一定是相同的,是以系統調用的第三個參數

得指明該協定簇的位址格式的長度

2. 進行 bind(2)系統調用時,除了位址長度外,還得向核心提供:sock 描述符、協定簇名稱、本地位址、端口這

些參數

<b>sys_bind的實作</b>

操作 SYS_BIND  是由 sys_bind()實作的

1335 asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)

1336 {

1337     struct socket *sock;

1338     char address[MAX_SOCK_ADDR];

1339     int err, fput_needed;

1340

1341     if((sock = sockfd_lookup_light(fd, &amp;err, &amp;fput_needed))!=NULL)

1342     {

1343         if((err=move_addr_to_kernel(umyaddr,addrlen,address))&gt;=0) {

1344             err = security_socket_bind(sock, (struct sockaddr *)address, addrlen);

1345             if (!err)

1346                 err = sock-&gt;ops-&gt;bind(sock,

1347                     (struct sockaddr *)address, addrlen);

1348         }

1349         fput_light(sock-&gt;file, fput_needed);

1350     }          

1351     return err;

1352 }<b></b>

在socket 的建立中已經反複分析了 socket 與檔案系統的關系,現在已知socket的描述符号,要找出與之相關的socket結構

應該是件容易的事情

 490 static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)                                          

 491 {

 492     struct file *file;

 493     struct socket *sock;

 494

 495     *err = -EBADF;

 496     file = fget_light(fd, fput_needed);

 497     if (file) {

 498         sock = sock_from_file(file, err);

 499         if (sock)

 500             return sock;

 501         fput_light(file, *fput_needed);

 502     }

 503     return NULL;

 504 }

fget 從目前程序的 files 指針中,根據 sock 對應的描述符号,找到已打開的檔案 file,再根據檔案的

目錄項中的 inode,利用inode 與 sock 被封裝在同一個結構中的事實,調用宏 SOCKET_I找到待查

的 sock 結構。最後做一個小小的判斷,因為正常情況下,sock 的 file 指針是 回指與之相關的 file.

接下來的工作是把使用者态的位址拷貝至核心中來

 228 int move_addr_to_kernel(void __user *uaddr, int ulen, void *kaddr)                                                     

 229 {

 230     if(ulenMAX_SOCK_ADDR)

 231         return -EINVAL;

 232     if(ulen==0)

 233         return 0;

 234     if(copy_from_user(kaddr,uaddr,ulen))

 235         return -EFAULT;

 236     return audit_sockaddr(ulen, kaddr);

 237 }

bind(2)第三個參數必須存在的原因之一,copy_from_user 必須知道拷貝的位元組長度

因為 sock 的 ops 函數指針集,在建立之初,就指向了對應的協定類型,例如如果類型是

SOCK_STREAM,那麼它就指向 inetsw_array[0].ops。也就是 inet_stream_ops:

struct proto_ops inet_stream_ops = {

        .family = PF_INET,

        .bind = inet_bind,

        ……

};

sys_bind()在做完了一個通用的 socket bind 應該做的事情,包括查找對應 sock 結構,拷貝位址。

就調用對應協定族的對應協定類型的 bind 函數,也就是 inet_bind.

<b></b>

inet_bind

說 bind(2)的最重要的作用就是為套接字綁定位址和端口,那麼要分析inet_bind()之前,要搞清楚

的一件事情就是,這個綁定是綁定到哪兒?或者說是綁定到核心的哪個資料結構的哪個成員變量上面?

有三個地方是可以考慮的:socket 結構,包括 sock 和 sk,inet結構,以及 protoname_sock 結構。

綁定在 socket 結構上是可行的,這樣可以實作最高層面上的抽像,但是因為每一類協定簇 socket 的位址及端口表現

形式差異很大,這樣就得引入專門的轉換處理功能。綁定在 protoname_sock 也是可行的,但是卻是最笨拙的,因為

例如 tcp 和 udp,它們的位址及端口表現形式是一樣的,這樣就浪費了空間加大了代碼處理量。

是以inet 做為一個協定類型的抽像是最理想的地方了,再來回顧一下它的定義

108 struct inet_sock {        

114     /* Socket demultiplex comparisons on incoming packets. */                                                           

115     __u32           daddr;

116     __u32           rcv_saddr;

117     __u16           dport;

118     __u16           num;  

119     __u32           saddr;

去掉了其它成員保留了與位址及端口相關的成員變量,從注釋中可以清楚地了解它們的作用。是以我們說的 bind(2)之

綁定主要就是對這幾個成員變量指派的過程了.

 397 int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)

 398 {

            /*  擷取位址參數 */

 399     struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;

            /*  擷取 sock 對應的 sk */

 400     struct sock *sk = sock-&gt;sk;

            /*  擷取 sk 對應的inet */

 401     struct inet_sock *inet = inet_sk(sk);

            /*  這個臨時變量用來儲存使用者态傳遞下來的端口參數 */

 402     unsigned short snum;

 403     int chk_addr_ret;

 404     int err;

 405

            /*  如果協定簇對應的協定自身還有bind函數調用之,例如 SOCK_RAW 就還有一個raw_bind */

 406     /* If the socket has its own bind function then use it. (RAW) */

 407     if (sk-&gt;sk_prot-&gt;bind) {

 408         err = sk-&gt;sk_prot-&gt;bind(sk, uaddr, addr_len);

 409         goto out;

 410     }

 411     err = -EINVAL;

            /*  校驗位址長度 */

 412     if (addr_len  413         goto out;

 414     /*  判斷位址類型:廣播?多點傳播?單點傳播? */

 415     chk_addr_ret = inet_addr_type(addr-&gt;sin_addr.s_addr);

 416

            /*  ipv4 有一個 ip_nonlocal_bind标志,表示是否綁定非本位址 IP位址,可以通過 

             *  cat /proc/sys/net/ipv4/ip_nonlocal_bind檢視到。

             *  它用來解決某些服務綁定動态 IP位址的情況。作者在注釋中已有詳細說明.

             *  這裡判斷,用來确認如果沒有開啟“綁定非本位址 IP”,位址值及類型是正确的

 417     /* Not specified by any standard per-se, however it breaks too

 418      * many applications when removed.  It is unfortunate since

 419      * allowing applications to make a non-local bind solves

 420      * several problems with systems using dynamic addressing.

 421      * (ie. your servers still start up even if your ISDN link

 422      *  is temporarily down)

 423      */

 424     err = -EADDRNOTAVAIL;

 425     if (!sysctl_ip_nonlocal_bind &amp;&amp;

 426         !inet-&gt;freebind &amp;&amp;

 427         addr-&gt;sin_addr.s_addr != INADDR_ANY &amp;&amp;

 428         chk_addr_ret != RTN_LOCAL &amp;&amp;

 429         chk_addr_ret != RTN_MULTICAST &amp;&amp;

 430         chk_addr_ret != RTN_BROADCAST)

 431         goto out;

            /* 擷取協定端口号 */

 433     snum = ntohs(addr-&gt;sin_port);

 434     err = -EACCES;

            /*  校驗目前程序有沒有使用低于 1024 端口的能力  */

 435     if (snum &amp;&amp; snum  436         goto out;

 437

 438     /*      We keep a pair of addresses. rcv_saddr is the one

 439      *      used by hash lookups, and saddr is used for transmit.

 440      *

 441      *      In the BSD API these are the same except where it

 442      *      would be illegal to use them (multicast/broadcast) in

 443      *      which case the sending device address is used.

 444      */

 445     lock_sock(sk);

 446

 447     /* Check these errors (active socket, double bind). */

 448     err = -EINVAL;

            /*  檢查socket是否已經被綁定過了: 用了兩個檢查項, 一個是 sk 狀态, 另一個是是否已經綁定過端口了

                  當然位址本來就可以為0,是以不能做為檢查項  */

 449     if (sk-&gt;sk_state != TCP_CLOSE || inet-&gt;num)

 450         goto out_release_sock;

 451     /*  綁定inet的接收位址(位址服務綁定位址)和來源位址為使用者态指定位址 */

 452     inet-&gt;rcv_saddr = inet-&gt;saddr = addr-&gt;sin_addr.s_addr;

            /*  若位址類型為廣播或多點傳播,則将位址置 0,表示直接使用網絡裝置 */

 453     if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)

 454         inet-&gt;saddr = 0;  /* Use device */

 455

            /*  調用協定的 get_port 函數,确認是否可綁定端口.

             *  若可以, 則綁定在 inet-&gt;num 之上, 注意這裡雖然沒有把inet傳過去,但是第一個參數sk

             *  它本身和 inet是可以互相轉化的 */

 456     /* Make sure we are allowed to bind here. */

 457     if (sk-&gt;sk_prot-&gt;get_port(sk, snum)) {

 458         inet-&gt;saddr = inet-&gt;rcv_saddr = 0;

 459         err = -EADDRINUSE;

 460         goto out_release_sock;

 461     }

 462      /*  如果端口和位址可以綁定,置标志位 */

 463     if (inet-&gt;rcv_saddr)

 464         sk-&gt;sk_userlocks |= SOCK_BINDADDR_LOCK;

 465     if (snum)

 466         sk-&gt;sk_userlocks |= SOCK_BINDPORT_LOCK;

            /* inet的 sport(來源端口)成員也置為綁定端口 */

 467     inet-&gt;sport = htons(inet-&gt;num);

 468     inet-&gt;daddr = 0;

 469     inet-&gt;dport = 0;  

 470     sk_dst_reset(sk);

 471     err = 0;

 472 out_release_sock:

 473     release_sock(sk);

 474 out:

 475     return err;

 476 }

上述分析中忽略的第一個細節是capable()函數調用,它是 Linux 安全子產品(LSM)的一部份簡單地講其用來對權限做出檢查

檢查是否有權對指定的資源進行操作。這裡它的參數是CAP_NET_BIND_SERVICE表示的含義是: 

/* Allows binding to TCP/UDP sockets below 1024 */

/* Allows binding to ATM VCIs below 32 */

#define CAP_NET_BIND_SERVICE 10[/code]

另一個就是協定的端口綁定,調用了協定的get_port函數,如果是SOCK_STREAM的TCP協定,那麼它

就是tcp_v4_get_port()函數.

<b>協定端口的綁定</b>

要分析這個函數還是得先繞一些基本的東東,這裡涉及到核心中提供hash連結清單的操作的API。

可以參考其它相關資料。

http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html

這裡講了連結清單的實作,順道提了一個 hash 連結清單,覺得寫得還不錯,收藏一下。

對于 TCP已注冊的端口,是采用一個 hash 表來維護的。hash 桶用 struct tcp_bind_hashbucket 結構來表示:

struct tcp_bind_hashbucket {

        spinlock_t                lock;

        struct hlist_head        chain;

hash 表中的每一個 hash節點,用 struct tcp_bind_hashbucket 結構來表示:

struct tcp_bind_bucket {

        unsigned short           port;                        /*  節點中綁定的端口 */

        signed short               fastreuse;

        struct hlist_node        node;

        struct hlist_head        owners;

tcp_hashinfo 的 hash 表資訊,都集中封裝在結構 tcp_hashinfo 當中,而維護已注冊端口隻是它其

中一部份:

extern struct tcp_hashinfo {

        /* Ok, let's try this, I give up, we do need a local binding

         * TCP hash as well as the others for fast bind/connect.          */

        struct tcp_bind_hashbucket *__tcp_bhash;

        int __tcp_bhash_size;

} tcp_hashinfo;

#define tcp_bhash        (tcp_hashinfo.__tcp_bhash)

#define tcp_bhash_size        (tcp_hashinfo.__tcp_bhash_size)

其使用的 hash 函數是 tcp_bhashfn:

/* These are AF independent. */

static __inline__ int tcp_bhashfn(__u16 lport)

{

        return (lport &amp; (tcp_bhash_size - 1));

}

這樣,如果要取得某個端口對應的 hash 鍊的首部hash 桶節點的話,可以使用:

struct tcp_bind_hashbucket *head;

head = &amp;tcp_bhash[tcp_bhashfn(snum)];

如果要新綁定一個端口就是先建立一個 struct tcp_bind_hashbucket 結構的 hash 節點,然後把它插入到對應的

hash 鍊中去:

struct tcp_bind_bucket *tb;

tb = tcp_bucket_create(head, snum);

struct tcp_bind_bucket *tcp_bucket_create(struct tcp_bind_hashbucket *head,

                                          unsigned short snum)

        struct tcp_bind_bucket *tb = kmem_cache_alloc(tcp_bucket_cachep,

                                                      SLAB_ATOMIC);

        if (tb) {

                tb-&gt;port = snum;

                tb-&gt;fastreuse = 0;

                INIT_HLIST_HEAD(&amp;tb-&gt;owners);

                hlist_add_head(&amp;tb-&gt;node, &amp;head-&gt;chain);

        }

        return tb;

另外sk 中還維護了一個類似的 hash 連結清單,同時需要調用 tcp_bind_hash()函數把 hash 節點插入進去:

struct sock {

        struct sock_common        __sk_common;

#define sk_bind_node              __sk_common.skc_bind_node

/* @skc_bind_node: bind hash linkage for various protocol lookup tables */

struct sock_common {

        struct hlist_node        skc_bind_node;

if (!tcp_sk(sk)-&gt;bind_hash)

        tcp_bind_hash(sk, tb, snum);

void tcp_bind_hash(struct sock *sk, struct tcp_bind_bucket *tb,

                   unsigned short snum)

        inet_sk(sk)-&gt;num = snum;

        sk_add_bind_node(sk, &amp;tb-&gt;owners);

        tcp_sk(sk)-&gt;bind_hash = tb;

這裡就順道綁定了 inet 的 num成員變量,并置協定的 bind_hash 指針為目前配置設定的 hash 節點。

而sk_add_bind_node 函數,就是一個插入 hash 表節點的過程:

static __inline__ void sk_add_bind_node(struct sock *sk,

                                        struct hlist_head *list)

        hlist_add_head(&amp;sk-&gt;sk_bind_node, list);

如果要周遊 hash 表的話,例如在插入之前,先判斷端口是否已經在 hash表當中了。就可以調用:

#define tb_for_each(tb, node, head) hlist_for_each_entry(tb, node, head, node)

spin_lock(&amp;head-&gt;lock);

tb_for_each(tb, node, &amp;head-&gt;chain)

        if (tb-&gt;port == snum)                 found,do_something;

有了這些基礎知識,再來看 tcp_v4_get_port()的實作,就要容易得多了:

static int tcp_v4_get_port(struct sock *sk, unsigned short snum)

        struct tcp_bind_hashbucket *head;

        struct hlist_node *node;

        struct tcp_bind_bucket *tb;

        int ret;

        local_bh_disable();

        /*  如果端口值為 0,意味着讓系統從本地可用端口用選擇一個,并置 snum為配置設定的值 */

        if (!snum) {

                int low = sysctl_local_port_range[0];

                int high = sysctl_local_port_range[1];

                int remaining = (high - low) + 1;

                int rover;

                spin_lock(&amp;tcp_portalloc_lock);

                if (tcp_port_rover                         rover = low;

                else

                        rover = tcp_port_rover;

                do {

                        rover++;

                        if (rover &gt; high)

                                rover = low;

                         head = &amp;tcp_bhash[tcp_bhashfn(rover)];

                        spin_lock(&amp;head-&gt;lock);

                        tb_for_each(tb, node, &amp;head-&gt;chain)

                                if (tb-&gt;port == rover)

                                        goto next;

                        break;

                next:

                        spin_unlock(&amp;head-&gt;lock);

                } while (--remaining &gt; 0);

                tcp_port_rover = rover;

                spin_unlock(&amp;tcp_portalloc_lock);

                /* Exhausted local port range during search? */

                ret = 1;

                if (remaining                         goto fail; 

                /* OK, here is the one we will use.  HEAD is

                 * non-NULL and we hold it's mutex.

                 */

                snum = rover;

        } else {

                /*  否則,就在 hash 表中,查找端口是否已經存在 */

                head = &amp;tcp_bhash[tcp_bhashfn(snum)];

                spin_lock(&amp;head-&gt;lock);

                tb_for_each(tb, node, &amp;head-&gt;chain)

                        if (tb-&gt;port == snum)

                                goto tb_found;

        tb = NULL;

        goto tb_not_found;

tb_found:

        /*  稍後有對應的代碼:

         *  第一次配置設定 tb 後,會調用 tcp_bind_hash加入至相應的 sk,這裡先做一個判斷,

         *  來确定這一步工作是否進行過 */

        if (!hlist_empty(&amp;tb-&gt;owners)) {

        /* socket的SO_REUSEADDR選項,用來确定是否允許本地位址重用,例如同時啟動多個伺服器、多個套接字

         * 綁定至同一端口等等,sk_reuse 成員對應其值,因為如果一個綁定的 hash節點已經存在,而且不允許重用的話,

         * 那麼則表示因沖突導緻出錯,調用 tcp_bind_conflict 來處理之 */

                if (sk-&gt;sk_reuse &gt; 1)

                        goto success;

                if (tb-&gt;fastreuse &gt; 0 &amp;&amp;

                    sk-&gt;sk_reuse &amp;&amp; sk-&gt;sk_state != TCP_LISTEN) {

                } else {

                        ret = 1;

                        if (tcp_bind_conflict(sk, tb))

                                goto fail_unlock;

                }

tb_not_found:

        /*  如果不存在,則配置設定 hash節點,綁定端口 */

        ret = 1;

        if (!tb &amp;&amp; (tb = tcp_bucket_create(head, snum)) == NULL)

                goto fail_unlock;

        if (hlist_empty(&amp;tb-&gt;owners)) {

                if (sk-&gt;sk_reuse &amp;&amp; sk-&gt;sk_state != TCP_LISTEN)

                        tb-&gt;fastreuse = 1;

                        tb-&gt;fastreuse = 0;

        } else if (tb-&gt;fastreuse &amp;&amp; (!sk-&gt;sk_reuse || sk-&gt;sk_state == TCP_LISTEN))

success:

        if (!tcp_sk(sk)-&gt;bind_hash)

                tcp_bind_hash(sk, tb, snum);

        BUG_TRAP(tcp_sk(sk)-&gt;bind_hash == tb);

        ret = 0;

fail_unlock:

        spin_unlock(&amp;head-&gt;lock);

fail:

        local_bh_enable();

        return ret;

到這裡,可以為這部份下一個小結了,所謂綁定,就是:

1. 設定核心中 inet 相關變量成員的值,以待後用;

2. 協定中,如TCP協定,記錄綁定的協定端口的資訊,采用 hash 連結清單存儲,sk 中也同時維護了這麼一個連結清單。

    兩者的差別應該是前者給協定用, 後者給socket 用。

繼續閱讀