天天看點

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

<b>sys_listen</b>

對面向連接配接的協定,在調用 bind(2)後,進一步調用 listen(2),讓套接字進入監聽狀态:

int listen(int sockfd, int backlog);

backlog 表示建立連接配接請求時,最大的未處理的積壓請求數。

這裡說到讓套接字進入某種狀态,也就是說,涉及到套接字的狀态變遷,前面 create 和bind 時,也遇到過相應的代碼。

sock和 sk 都有相應的狀态字段,先來看 sock 的:

typedef enum {

        SS_FREE = 0,                        /*  套接字未配置設定  */

        SS_UNCONNECTED,              /*  套接字未連接配接  */

        SS_CONNECTING,                  /*  套接字正在處理連接配接 */

        SS_CONNECTED,                   /*  套接字已連接配接 */

        SS_DISCONNECTING              /*  套接字正在處理關閉連接配接 */

}socket_state;

在建立套接字時,被初始化為SS_UNCONNECTED

對于面向連接配接模式的SOCK_TREAM來講,這樣描述狀态顯然是不夠的。這樣在sk中, 使用sk_state維護了一個有限狀态機

來描述套接字的狀态:

enum {

  TCP_ESTABLISHED = 1,

  TCP_SYN_SENT,

  TCP_SYN_RECV,

  TCP_FIN_WAIT1,

  TCP_FIN_WAIT2,

  TCP_TIME_WAIT,

  TCP_CLOSE,

  TCP_CLOSE_WAIT,

  TCP_LAST_ACK,

  TCP_LISTEN,

  TCP_CLOSING,       /* now a valid state */

  TCP_MAX_STATES /* Leave at the end! */

};

還有一個相應的用來進行狀态位運算的枚舉結構:

  TCPF_ESTABLISHED = (1   TCPF_SYN_SENT = (1   TCPF_SYN_RECV = (1   TCPF_FIN_WAIT1 = (1   TCPF_FIN_WAIT2 = (1   TCPF_TIME_WAIT = (1   TCPF_CLOSE = (1   TCPF_CLOSE_WAIT = (1   TCPF_LAST_ACK = (1   TCPF_LISTEN = (1   TCPF_CLOSING = (1 };

值得一提的是,sk 的狀态不等于 TCP的狀态,雖然 sk 是面向協定棧,但它的狀态并不能同 TCP狀态一一直接劃等号。

雖然這些狀态值都用 TCP-XXX 來表式,但是隻是因為 TCP協定狀态非常複雜。sk 結構隻是利用它的一個子集來抽像

描述而已。

同樣地,操作碼 SYS_LISTEN的任務會落到 sys_listen()函數身上:

/* Maximum queue length specifiable by listen.  */

#define SOMAXCONN        128

int sysctl_somaxconn = SOMAXCONN;

asmlinkage long sys_listen(int fd, int backlog)

{

        struct socket *sock;

        int err;

        if ((sock = sockfd_lookup(fd, &amp;err)) != NULL) {

                if ((unsigned) backlog &gt; sysctl_somaxconn)

                        backlog = sysctl_somaxconn;

                err = security_socket_listen(sock, backlog);

                if (err) {

                        sockfd_put(sock);

                        return err;

                }

                err=sock-&gt;ops-&gt;listen(sock, backlog);

                sockfd_put(sock);

        }

        return err;

}

同樣地,函數會最終轉向協定簇的 listen 函數,也就是 inet_listen():

/*

*        Move a socket into listening state.

*/

int inet_listen(struct socket *sock, int backlog)

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

        unsigned char old_state;

        lock_sock(sk);

        err = -EINVAL;

        /* 在 listen 之前,sock 必須為未連接配接狀态,且隻有 SOCK_STREAM 類型,才有 listen(2)*/

        if (sock-&gt;state != SS_UNCONNECTED || sock-&gt;type != SOCK_STREAM)

                goto out;

        /*  臨時儲存狀态機狀态 */

        old_state = sk-&gt;sk_state;        

        /*  隻有狀态機處于 TCP_CLOSE  或者是 TCP_LISTEN  這兩種狀态時,才可能對其調用listen(2)

         *  這個判斷證明了 listen(2)是可以重複調用地(當然是在轉向 TCP_LISTEN 後沒有再進行狀态變遷*/

        if (!((1                 goto out;

        /*  如果接口已經處理 listen 狀态,隻修改其 max_backlog,否則先調用 tcp_listen_start,

         *  繼續置協定的 listen 狀态 */

        if (old_state != TCP_LISTEN) {

                err = tcp_listen_start(sk);

                if (err)

                        goto out;

        sk-&gt;sk_max_ack_backlog = backlog;

        err = 0;

out:

        release_sock(sk);

inet_listen 函數在确認 sock-&gt;state 和 sk-&gt;sk_state 狀态後,會進一步調用 tcp_listen_start 函數,最且

最後設定 sk_max_ack_backlog  。

tcp 的 tcp_listen_start 函數,完成兩個重要的功能,一個是初始化 sk 的一些相關成員變量,另一方

面是切換有限狀态機的狀态。 sk_max_ack_backlog表示監聽時最大的 backlog 數量,它由使用者空間

傳遞的參數決定。而 sk_ack_backlog表示目前的的 backlog數量。

當 tcp 伺服器收到一個 syn 封包時,它表示了一個連接配接請求的到達。核心使用了一個 hash 表來維護這個連接配接請求表:

struct tcp_listen_opt

        u8                        max_qlen_log;        /* log_2 of maximal queued SYNs */

        int                        qlen;

        int                        qlen_young;

        int                        clock_hand;

        u32                      hash_rnd;

        struct open_request        *syn_table[TCP_SYNQ_HSIZE];

syn_table是open_request結構,就是連接配接請求表,連接配接表中的最大項,也就是最大允許的 syn 封包的數

量,由 max_qlen_log 來決定。當套接字進入 listen 狀态,也就是說可以接收 syn 封包了,那麼在此

之前,需要先初始化這個表: 

int tcp_listen_start(struct sock *sk)

        struct inet_sock *inet = inet_sk(sk);           //擷取 inet結構指針

        struct tcp_sock *tp = tcp_sk(sk);                //擷取協定指針

        struct tcp_listen_opt *lopt;

        //初始化 sk 相關成員變量

        sk-&gt;sk_max_ack_backlog = 0;

        sk-&gt;sk_ack_backlog = 0;

        tp-&gt;accept_queue = tp-&gt;accept_queue_tail = NULL;

        rwlock_init(&amp;tp-&gt;syn_wait_lock);

        tcp_delack_init(tp);

        //初始化連接配接請求 hash 表

        lopt = kmalloc(sizeof(struct tcp_listen_opt), GFP_KERNEL);

        if (!lopt)

                return -ENOMEM;

        memset(lopt, 0, sizeof(struct tcp_listen_opt));

        //初始化 hash 表容量,最小為 6,其實際值由 sysctl_max_syn_backlog 決定

        for (lopt-&gt;max_qlen_log = 6; ; lopt-&gt;max_qlen_log++)

                if ((1 max_qlen_log) &gt;= sysctl_max_syn_backlog)

                        break;

        get_random_bytes(&amp;lopt-&gt;hash_rnd, 4);

        write_lock_bh(&amp;tp-&gt;syn_wait_lock);

        tp-&gt;listen_opt = lopt;

        write_unlock_bh(&amp;tp-&gt;syn_wait_lock);

        /* There is race window here: we announce ourselves listening,

         * but this transition is still not validated by get_port().

         * It is OK, because this socket enters to hash table only

         * after validation is complete.

         */

         /*  修改狀态機狀态,表示進入 listen 狀态,根據作者注釋,當宣告自己進入 listening 狀态後,

          *  但是這個狀态轉換并沒有得到 get_port 的确認。是以需要調用 get_port()函數。

          *  對于一點,暫時還沒有完全搞明白,隻有留待後面再來分析它 */

        sk-&gt;sk_state = TCP_LISTEN;

        if (!sk-&gt;sk_prot-&gt;get_port(sk, inet-&gt;num)) {

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

                sk_dst_reset(sk);

                sk-&gt;sk_prot-&gt;hash(sk);

                return 0;

        sk-&gt;sk_state = TCP_CLOSE;

        tp-&gt;listen_opt = NULL;

        kfree(lopt);

        return -EADDRINUSE;

在切換了有限狀态機狀态後,調用了

sk-&gt;sk_prot-&gt;hash(sk);

也就是 tcp_v4_hash()函數。這裡涉到到另一個 hash 表:TCP監聽 hash 表。

<b>TCP監聽 hash表</b>

所謂 TCP 監聽表,指的就核心維護“目前有哪些套接字在監聽”的一個表,當一個資料包進入 TCP

棧的時候,核心查詢這個表中對應的 sk,以找到相應的資料 結構。(因為 sk 是面向網絡棧調用的,找到了sk,

就找到了 tcp_sock,就找到了 inet_sock,就找到了 sock,就找到了 fd……就到了組  織了)。

TCP所有的 hash 表都用了tcp_hashinfo來封裝,前面分析 bind已見過它:

extern struct tcp_hashinfo {

        ……

        /* All sockets in TCP_LISTEN state will be in here.  This is the only

         * table where wildcard'd TCP sockets can exist.  Hash function here

         * is just local port number.

        struct hlist_head __tcp_listening_hash[TCP_LHTABLE_SIZE];

        spinlock_t __tcp_portalloc_lock;

} tcp_hashinfo;

#define tcp_listening_hash (tcp_hashinfo.__tcp_listening_hash)

函數 tcp_v4_hash 将一個處理監聽狀态下的 sk 加入至這個 hash 表:

static void tcp_v4_hash(struct sock *sk)

        if (sk-&gt;sk_state != TCP_CLOSE) {

                local_bh_disable();                

                __tcp_v4_hash(sk, 1);

                local_bh_enable();

因為__tcp_v4_hash 不隻用于監聽 hash 表,它也用于其它 hash 表,其第二個參數 listen_possible 為

真的時候,表示處理的是監聽 hash表:

static __inline__ void __tcp_v4_hash(struct sock *sk, const int listen_possible)

        struct hlist_head *list;

        rwlock_t *lock;

        BUG_TRAP(sk_unhashed(sk));

        if (listen_possible &amp;&amp; sk-&gt;sk_state == TCP_LISTEN) {

                list = &amp;tcp_listening_hash[tcp_sk_listen_hashfn(sk)];

                lock = &amp;tcp_lhash_lock;

                tcp_listen_wlock();

        } else {

                                ……

        __sk_add_node(sk, list);

        sock_prot_inc_use(sk-&gt;sk_prot);

        write_unlock(lock);

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

                wake_up(&amp;tcp_lhash_wait);

else 中的部份用于另一個 hash 表,暫時不管它。代表很簡單,如果确認是處理的是監聽 hash 表。

則先根據 sk計算一個 hash 值,在hash 桶中找到入口。再調用__sk_add_node 加入至該 hash 鍊。

tcp_sk_listen_hashfn()函數事實上是 tcp_lhashfn 的包裹,前面已經分析過了。

__sk_add_node()函數也就是一個簡單的核心 hash處理函數 hlist_add_head()的包裹:

static __inline__ void __sk_add_node(struct sock *sk, struct hlist_head *list)

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

小結

一個套接字的 listen,主要需要做的工作有以下幾件:

1. 初始化 sk 相關的成員變量,最重要的是 listen_opt,也就是連接配接請求 hash 表。

2. 将 sk 的有限狀态機轉換為 TCP_LISTEN,即監聽狀态;

3. 将 sk 加入監聽 hash表;

4. 設定允許的最大請求積壓數,也就是 sk 的成員 sk_max_ack_backlog 的值。 

繼續閱讀