天天看點

Linux TCP/IP協定棧之Socket的實作分析(Accept 接受一個連接配接)

<b>Tcp棧的三次握手簡述</b>

進一步的分析,都是以 tcp 協定為例,因為 udp要相對簡單得多,分析完 tcp,udp的基本已經被覆寫了。

這裡主要是分析 socket,但是因為它将與 tcp/udp傳輸層互動,是以不可避免地接觸到這一層面的代碼,

這裡隻是摘取其主要流程的一些代碼片段,以更好地分析accept的實作過程。

當套接字進入 LISTEN後,意味着伺服器端已經可以接收來自用戶端的請求。當一個 syn 包到達後,伺服器認為它是一個

tcp請求封包,根據tcp協定,TCP 網絡棧将會自動應答它一個 syn+ack 封包,并且将它放入 syn_table 這個 hash 表

中,靜靜地等待用戶端第三次握手封包的來到。一個 tcp 的 syn 封包進入 tcp 堆棧後,會按以下函數調用,

最終進入 tcp_v4_conn_request:

tcp_v4_rcv

        -&gt;tcp_v4_do_rcv

                -&gt;tcp_rcv_state_process

                        -&gt;tp-&gt;af_specific-&gt;conn_request

tcp_ipv4.c 中,tcp_v4_init_sock初始化時,有tp-&gt;af_specific = &amp;ipv4_specific;

struct tcp_func ipv4_specific = {

        .queue_xmit = ip_queue_xmit,

        .send_check = tcp_v4_send_check,

        .rebuild_header = tcp_v4_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 = v4_addr2sockaddr,

        .sockaddr_len = sizeof(struct sockaddr_in),

};

是以 af_specific-&gt;conn_request實際指向的是 tcp_v4_conn_request:

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)

{

        struct open_request *req;

        ……

        /*  配置設定一個連接配接請求 */

        req = tcp_openreq_alloc();

        if (!req)

                goto drop;

        ……        

        /*  根據資料包的實際要素,如來源/目的位址等,初始化它*/

        tcp_openreq_init(req, &amp;tmp_opt, skb);

        req-&gt;af.v4_req.loc_addr = daddr;

        req-&gt;af.v4_req.rmt_addr = saddr;

        req-&gt;af.v4_req.opt = tcp_v4_save_options(sk, skb);

        req-&gt;class = &amp;or_ipv4;                

        /*  回送一個 syn+ack 的二次握手封包 */

        if (tcp_v4_send_synack(sk, req, dst))

                goto drop_and_free;

        if (want_cookie) {

                ……

        } else {                 /*  将連接配接請求 req 加入連接配接監聽表 syn_table */

                tcp_v4_synq_add(sk, req);

        }

        return 0;        

}

syn_table 在前面分析的時候已經反複看到了。它的作用就是記錄 syn 請求封包,建構一個 hash 表。

這裡調用的 tcp_v4_synq_add()就完成了将請求添加進該表的操作:

static void tcp_v4_synq_add(struct sock *sk, struct open_request *req)

        struct tcp_sock *tp = tcp_sk(sk);

        struct tcp_listen_opt *lopt = tp-&gt;listen_opt;

        /*  計算一個 hash值 */

        u32 h = tcp_v4_synq_hash(req-&gt;af.v4_req.rmt_addr, req-&gt;rmt_port, lopt-&gt;hash_rnd);

        req-&gt;expires = jiffies + TCP_TIMEOUT_INIT;

        req-&gt;retrans = 0;

        req-&gt;sk = NULL;

        /*指針移到 hash 鍊的未尾*/

        req-&gt;dl_next = lopt-&gt;syn_table[h];

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

        /*加入目前節點*/

        lopt-&gt;syn_table[h] = req;

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

        tcp_synq_added(sk);

這樣所有的 syn 請求都被放入這個表中,留待第三次 ack 的到來的比對。當第三次 ack 來到後,會進入下列函數:

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)

        …… 

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

                struct sock *nsk = tcp_v4_hnd_req(sk, skb);

 因為目前 sk還是 TCP_LISTEN狀态,是以會進入 tcp_v4_hnd_req:

[code]static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)

        struct tcphdr *th = skb-&gt;h.th;

        struct iphdr *iph = skb-&gt;nh.iph;

        struct sock *nsk;

        struct open_request **prev;

        /* Find possible connection requests. */

        struct open_request *req = tcp_v4_search_req(tp, &amp;prev, th-&gt;source,

                                                     iph-&gt;saddr, iph-&gt;daddr);

        if (req)

                return tcp_check_req(sk, skb, req, prev);

tcp_v4_search_req 就是查找比對 syn_table 表:

[code]static struct open_request *tcp_v4_search_req(struct tcp_sock *tp,

                                              struct open_request ***prevp,

                                              __u16 rport,

                                              __u32 raddr, __u32 laddr)

        struct open_request *req, **prev;

        for (prev = &amp;lopt-&gt;syn_table[tcp_v4_synq_hash(raddr, rport, lopt-&gt;hash_rnd)];

             (req = *prev) != NULL;

             prev = &amp;req-&gt;dl_next) {

                if (req-&gt;rmt_port == rport &amp;&amp;

                    req-&gt;af.v4_req.rmt_addr == raddr &amp;&amp;

                    req-&gt;af.v4_req.loc_addr == laddr &amp;&amp;

                    TCP_INET_FAMILY(req-&gt;class-&gt;family)) {

                        BUG_TRAP(!req-&gt;sk);

                        *prevp = prev;

                        break;

                }

        return req;

hash 表的查找還是比較簡單的,調用 tcp_v4_synq_hash 計算出 hash 值,找到 hash 鍊入口,周遊該

鍊即可。 排除逾時等意外因素,剛才加入 hash 表的 req 會被找到,這樣,tcp_check_req()函數将會被繼續調用:

struct sock *tcp_check_req(struct sock *sk,struct sk_buff *skb,

                           struct open_request *req,

                           struct open_request **prev)

        tcp_acceptq_queue(sk, req, child);

req 被找到,表明三次握手已經完成,連接配接已經成功建立,tcp_check_req 最終将調用tcp_acceptq_queue(),

把這個建立好的連接配接加入至 tp-&gt;accept_queue 隊列,等待使用者調用 accept(2)來讀取之。

static inline void tcp_acceptq_queue(struct sock *sk, struct open_request *req,

                                         struct sock *child)

        req-&gt;sk = child;

        sk_acceptq_added(sk);

        if (!tp-&gt;accept_queue_tail) {

                tp-&gt;accept_queue = req;

        } else {

                tp-&gt;accept_queue_tail-&gt;dl_next = req;

        tp-&gt;accept_queue_tail = req;

        req-&gt;dl_next = NULL;

<b>sys_accept</b>

當 listen(2)調用準備就緒的時候,伺服器可以通過調用 accept(2)接受或等待(注意這個“或等

待”是相當的重要)連接配接隊列中的第一個請求:

int accept(int s, struct sockaddr * addr ,socklen_t *addrlen);

accept(2)調用,隻是針對有連接配接模式。socket 一旦經過 listen(2)調用進入監聽狀态後,就被動地調用

accept(2)接受來自用戶端的連接配接請求。accept(2)調用是阻塞的,也就是說如果沒有連接配接請求到達,它會去睡覺,

等到連接配接請求到來後(或者是逾時)才會傳回。同樣地操作碼 SYS_ACCEPT 對應的是函數sys_accept

asmlinkage long sys_accept(int fd, struct sockaddr __user *upeer_sockaddr, int __user

*upeer_addrlen) {

        struct socket *sock, *newsock;

        int err, len;

        char address[MAX_SOCK_ADDR];

        sock = sockfd_lookup(fd, &amp;err);

        if (!sock)

                goto out;

        err = -ENFILE;

        if (!(newsock = sock_alloc())) 

                goto out_put;

        newsock-&gt;type = sock-&gt;type;

        newsock-&gt;ops = sock-&gt;ops;

        err = security_socket_accept(sock, newsock);

        if (err)

                goto out_release;

        /*

         * We don't need try_module_get here, as the listening socket (sock)

         * has the protocol module (sock-&gt;ops-&gt;owner) held.

         */

        __module_get(newsock-&gt;ops-&gt;owner);

        err = sock-&gt;ops-&gt;accept(sock, newsock, sock-&gt;file-&gt;f_flags);

        if (err                 goto out_release;

        if (upeer_sockaddr) {

                if(newsock-&gt;ops-&gt;getname(newsock, (struct sockaddr *)address, &amp;len, 2)                        err = -ECONNABORTED;

                        goto out_release;

                err = move_addr_to_user(address, len, upeer_sockaddr, upeer_addrlen);

                if (err                         goto out_release;

        /* File flags are not inherited via accept() unlike another OSes. */

        if ((err = sock_map_fd(newsock))                 goto out_release; 

        security_socket_post_accept(sock, newsock);

out_put:

        sockfd_put(sock);

out:

        return err;

out_release:

        sock_release(newsock);

        goto out_put;

}[/code]

代碼稍長了點,逐漸來分析它。

一個 socket,經過 listen(2)設定成 server 套接字後,就永遠不會再與任何用戶端套接字建立連接配接了。

因為一旦它接受了一個連接配接請求,就會建立出一個新的socket,新的 socket 用來描述新到達的連接配接,而原先的 server

套接字并無改變,并且還可以通過下一次 accept(2)調用 再建立一個新的出來,就像母雞下蛋一樣,“隻取蛋,不殺雞”,

server 套接字永遠保持接受新的連接配接請求的能力。

函數先通過 sockfd_lookup(),根據 fd,找到對應的 sock,然後通過 sock_alloc配置設定一個新的 sock。

接着就調用協定簇的 accept()函數:

/*

*        Accept a pending connection. The TCP layer now gives BSD semantics.

*/

int inet_accept(struct socket *sock, struct socket *newsock, int flags)

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

        int err = -EINVAL;

        struct sock *sk2 = sk1-&gt;sk_prot-&gt;accept(sk1, flags, &amp;err);

        if (!sk2)

                goto do_err;

        lock_sock(sk2);

        BUG_TRAP((1 sk_state) &amp;

                 (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_CLOSE));

        sock_graft(sk2, newsock);

        newsock-&gt;state = SS_CONNECTED;

        err = 0;

        release_sock(sk2); do_err:

函數第一步工作是調用協定的 accept 函數,然後調用 sock_graft()函數,

接下來設定新的套接字的狀态為 SS_CONNECTED.

*        This will accept the next outstanding connection.

struct sock *tcp_accept(struct sock *sk, int flags, int *err)

        struct sock *newsk;

        int error;

        lock_sock(sk);

        /* We need to make sure that this socket is listening,

         * and that it has something pending.

        error = -EINVAL;

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

        /* Find already established connection */

        if (!tp-&gt;accept_queue) {

                long timeo = sock_rcvtimeo(sk, flags &amp; O_NONBLOCK);

                /* If this is a non blocking socket don't sleep */

                error = -EAGAIN;

                if (!timeo)

                        goto out;

                error = wait_for_connect(sk, timeo);

                if (error)

        req = tp-&gt;accept_queue;

        if ((tp-&gt;accept_queue = req-&gt;dl_next) == NULL)

                tp-&gt;accept_queue_tail = NULL; 

        newsk = req-&gt;sk;

        sk_acceptq_removed(sk);

        tcp_openreq_fastfree(req);

        BUG_TRAP(newsk-&gt;sk_state != TCP_SYN_RECV);

        release_sock(sk);

        return newsk;

        *err = error;

        return NULL;

tcp_accept()函數,當發現 tp-&gt;accept_queue 準備就緒後,就直接調用

                tp-&gt;accept_queue_tail = NULL;

出隊,并取得相應的 sk。 否則,就在擷取逾時時間後,調用 wait_for_connect 等待連接配接的到來。這也是說,

強調“或等待”的原因所在了。

OK,繼續回到 inet_accept 中來,當取得一個就緒的連接配接的 sk(sk2)後,先校驗其狀态,再調用sock_graft()函數。

在 sys_accept 中,已經調用了 sock_alloc,配置設定了一個新的 socket 結構(即 newsock),但 sock_alloc

必竟不是 sock_create,它并不能為 newsock 配置設定一個對應的 sk。是以這個套接字并不完整。

另一方面,當一個連接配接到達到,根據用戶端的請求,産生了一個新的 sk(即 sk2,但這個配置設定過程

沒有深入 tcp 棧去分析其實作,隻分析了它對應的 req 入隊的代碼)。呵呵,将兩者一關聯,就 OK

了,這就是 sock_graft 的任務:

static inline void sock_graft(struct sock *sk, struct socket *parent)

        write_lock_bh(&amp;sk-&gt;sk_callback_lock);

        sk-&gt;sk_sleep = &amp;parent-&gt;wait;

        parent-&gt;sk = sk;

        sk-&gt;sk_socket = parent;

        write_unlock_bh(&amp;sk-&gt;sk_callback_lock);

這樣,一對一的聯系就建立起來了。這個為 accept 配置設定的新的 socket 也大功告成了。接下來将其狀

态切換為 SS_CONNECTED,表示已連接配接就緒,可以來讀取資料了——如果有的話。

順便提一下,新的 sk 的配置設定,是在:

                     -&gt;tcp_check_req

                              -&gt;tp-&gt;af_specific-&gt;syn_recv_sock(sk, skb, req, NULL);

即 tcp_v4_syn_recv_sock函數,其又調用 tcp_create_openreq_child()來配置設定的。

struct sock *tcp_create_openreq_child(struct sock *sk, struct open_request *req, struct sk_buff *skb)

        /* allocate the newsk from the same slab of the master sock,

         * if not, at sk_free time we'll try to free it from the wrong

         * slabcache (i.e. is it TCPv4 or v6?), this is handled thru sk-&gt;sk_prot -acme */

        struct sock *newsk = sk_alloc(PF_INET, GFP_ATOMIC, sk-&gt;sk_prot, 0);

        if(newsk != NULL) {

                          ……

                         memcpy(newsk, sk, sizeof(struct tcp_sock));

                         newsk-&gt;sk_state = TCP_SYN_RECV;

等到分析 tcp 棧的實作的時候,再來仔細分析它。但是這裡新的 sk 的有限狀态機被切換至了

TCP_SYN_RECV(按我的想法,似乎應進入 establshed 才對呀,是不是哪兒看漏了,隻有看了後頭的代碼再來印證了)

回到 sys_accept 中來,如果調用者要求傳回用戶端的位址,則調用新的 sk 的getname 函數指針,

也就是 inet_getname:

*        This does both peername and sockname.

int inet_getname(struct socket *sock, struct sockaddr *uaddr,

                        int *uaddr_len, int peer)

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

        struct inet_sock *inet        = inet_sk(sk);

        struct sockaddr_in *sin        = (struct sockaddr_in *)uaddr;

        sin-&gt;sin_family = AF_INET;

        if (peer) {

                if (!inet-&gt;dport ||

                    (((1 sk_state) &amp; (TCPF_CLOSE | TCPF_SYN_SENT)) &amp;&amp;

                     peer == 1))

                        return -ENOTCONN;

                sin-&gt;sin_port = inet-&gt;dport;

                sin-&gt;sin_addr.s_addr = inet-&gt;daddr;

                __u32 addr = inet-&gt;rcv_saddr;                 if (!addr)

                        addr = inet-&gt;saddr;

                sin-&gt;sin_port = inet-&gt;sport;

                sin-&gt;sin_addr.s_addr = addr;

        memset(sin-&gt;sin_zero, 0, sizeof(sin-&gt;sin_zero));

        *uaddr_len = sizeof(*sin);

        return 0;

函數的工作是建構 struct sockaddr_in  結構出來,接着在 sys_accept中,調用 move_addr_to_user()

函數來拷貝至使用者空間:

int move_addr_to_user(void *kaddr, int klen, void __user *uaddr, int __user *ulen)

        int err;

        int len;

        if((err=get_user(len, ulen)))

                return err;

        if(len&gt;klen)

                len=klen;

        if(len MAX_SOCK_ADDR)

                return -EINVAL;

        if(len)

        {

                 if(copy_to_user(uaddr,kaddr,len))

                        return -EFAULT;

         *        "fromlen shall refer to the value before truncation.."

         *                        1003.1g

        return __put_user(klen, ulen);

也就是調用 copy_to_user的過程了。

sys_accept 的最後一步工作,是将新的 socket 結構,與檔案系統挂鈎:

        if ((err = sock_map_fd(newsock))                 goto out_release;

函數 sock_map_fd 在建立 socket 中已經見過了。

小結:

accept 有幾件事情要做

1. 要 accept需要三次握手完成, 連接配接請求入tp-&gt;accept_queue隊列(新為用戶端分析的 sk, 也在其中), 其才能出隊

2. 為 accept配置設定一個sokcet結構, 并将其與新的sk關聯

3. 如果調用時,需要擷取用戶端位址,即第二個參數不為 NULL,則從新的 sk 中,取得其想的葫蘆;

4. 将新的 socket 結構與檔案系統挂鈎;

繼續閱讀