天天看點

Linux網絡協定棧 -- socket accept接收連接配接

一、tcp 棧的三次握手簡述

進一步的分析,都是以 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 

        ->tcp_v4_do_rcv 

                ->tcp_rcv_state_process 

                        ->tp->af_specific->conn_request

tcp_ipv4.c 中,tcp_v4_init_sock 初始化時,有 

tp->af_specific = &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->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, &tmp_opt, skb); 

        req->af.v4_req.loc_addr = daddr; 

        req->af.v4_req.rmt_addr = saddr; 

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

        req->class = &or_ipv4;                 

        …… 

        if (tcp_v4_send_synack(sk, req, dst)) 

                goto drop_and_free; 

        if (want_cookie) { 

                …… 

        } else {                  

                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->listen_opt; 

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

        req->expires = jiffies + TCP_TIMEOUT_INIT; 

        req->retrans = 0; 

        req->sk = NULL; 

        req->dl_next = lopt->syn_table[h]; 

        write_lock(&tp->syn_wait_lock); 

        lopt->syn_table[h] = req; 

        write_unlock(&tp->syn_wait_lock); 

        tcp_synq_added(sk); 

}

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

tcp_v4_rcv 

        ->tcp_v4_do_rcv

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

        …… 

        if (sk->sk_state == TCP_LISTEN) { 

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

        …… 

}

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

static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb) 

        struct tcphdr *th = skb->h.th; 

        struct iphdr *iph = skb->nh.iph; 

        struct tcp_sock *tp = tcp_sk(sk); 

        struct sock *nsk; 

        struct open_request **prev; 

        struct open_request *req = tcp_v4_search_req(tp, &prev, th->source, 

                                                     iph->saddr, iph->daddr); 

        if (req) 

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

        …… 

}

tcp_v4_search_req 就是查找比對 syn_table 表: 

static struct open_request *tcp_v4_search_req(struct tcp_sock *tp, 

                                              struct open_request ***prevp, 

                                              __u16 rport, 

                                              __u32 raddr, __u32 laddr) 

        struct tcp_listen_opt *lopt = tp->listen_opt; 

        struct open_request *req, **prev; 

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

             (req = *prev) != NULL; 

             prev = &req->dl_next) { 

                if (req->rmt_port == rport && 

                    req->af.v4_req.rmt_addr == raddr && 

                    req->af.v4_req.loc_addr == laddr && 

                    TCP_INET_FAMILY(req->class->family)) { 

                        BUG_TRAP(!req->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->accept_queue 隊列,等待使用者調用 accept(2)來讀取之。 

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

                                         struct sock *child) 

        struct tcp_sock *tp = tcp_sk(sk); 

        req->sk = child; 

        sk_acceptq_added(sk); 

        if (!tp->accept_queue_tail) { 

                tp->accept_queue = req; 

        } else { 

                tp->accept_queue_tail->dl_next = req; 

        } 

        tp->accept_queue_tail = req; 

        req->dl_next = NULL; 

二、sys_accept

如上,當 listen(2)調用準備就緒的時候,伺服器可以通過調用 accept(2)接受或等待(注意這個“或等待”是相當的重要)連接配接隊列中的第一個請求: 

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

accept()調用,隻是針對有連接配接模式。socket 一旦經過 listen()調用進入監聽狀态後,就被動地調用accept(),接受來自客  戶端的連接配接請求。accept()調用是阻塞的,也就是說如果沒有連接配接請求到達,它會去睡覺,等到連接配接請求到來後(或者是逾時),才會傳回。同樣地,操  作碼 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, &err); 

        if (!sock) 

                goto out; 

        err = -ENFILE; 

        if (!(newsock = sock_alloc()))  

                goto out_put; 

        newsock->type = sock->type; 

        newsock->ops = sock->ops; 

        err = security_socket_accept(sock, newsock); 

        if (err) 

                goto out_release; 

        __module_get(newsock->ops->owner); 

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

        if (err < 0) 

                goto out_release; 

        if (upeer_sockaddr) { 

                if(newsock->ops->getname(newsock, (struct sockaddr *)address, &len, 2)<0) { 

                        err = -ECONNABORTED; 

                        goto out_release; 

                } 

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

                if (err < 0) 

                        goto out_release; 

        } 

        if ((err = sock_map_fd(newsock)) < 0) 

                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; 

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

一個 socket,經過 listen(2)設定成 server 套接字後,就永遠不會再與任何用戶端套接字建立連接配接了。因為一旦它接受了一個連接配接請求,就會  建立出一個新的 socket,新的 socket 用來描述新到達的連接配接,而原先的 server套接字并無改變,并且還可以通過下一次 accept()調用  再建立一個新的出來,就像母雞下蛋一樣,“隻取蛋,不殺雞”,server 套接字永遠保持接受新的連接配接請求的能力。 

函數先通過 sockfd_lookup(),根據 fd,找到對應的 sock,然後通過 sock_alloc配置設定一個新的 sock。接着就調用協定簇的 accept()函數: 

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

        struct sock *sk1 = sock->sk; 

        int err = -EINVAL; 

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

        if (!sk2) 

                goto do_err; 

        lock_sock(sk2); 

        BUG_TRAP((1 << sk2->sk_state) & 

                 (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_CLOSE)); 

        sock_graft(sk2, newsock); 

        newsock->state = SS_CONNECTED; 

        err = 0; 

        release_sock(sk2); do_err: 

        return err; 

}

函數第一步工作是調用協定的 accept 函數,然後調用 sock_graft()函數,接下來,設定新的套接字的狀态為 SS_CONNECTED。 

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

        struct tcp_sock *tp = tcp_sk(sk); 

        struct open_request *req; 

        struct sock *newsk; 

        int error; 

        lock_sock(sk); 

        error = -EINVAL; 

        if (sk->sk_state != TCP_LISTEN) 

                goto out; 

        if (!tp->accept_queue) { 

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

                error = -EAGAIN; 

                if (!timeo) 

                        goto out; 

                error = wait_for_connect(sk, timeo); 

                if (error) 

                        goto out; 

        } 

        req = tp->accept_queue; 

        if ((tp->accept_queue = req->dl_next) == NULL) 

                tp->accept_queue_tail = NULL;  

        newsk = req->sk; 

        sk_acceptq_removed(sk); 

        tcp_openreq_fastfree(req); 

        BUG_TRAP(newsk->sk_state != TCP_SYN_RECV); 

        release_sock(sk); 

        return newsk; 

out: 

        release_sock(sk); 

        *err = error; 

        return NULL; 

}

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

        req = tp->accept_queue; 

        if ((tp->accept_queue = req->dl_next) == NULL) 

                tp->accept_queue_tail = NULL; 

        newsk = req->sk;

出隊,并取得相應的 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(&sk->sk_callback_lock); 

        sk->sk_sleep = &parent->wait; 

        parent->sk = sk; 

        sk->sk_socket = parent; 

        write_unlock_bh(&sk->sk_callback_lock); 

}

這樣,一對一的聯系就建立起來了。這個為 accept 配置設定的新的 socket 也大功告成了。接下來将其狀态切換為 SS_CONNECTED,表示已連接配接就緒,可以來讀取資料了——如果有的話。 

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

tcp_v4_rcv 

        ->tcp_v4_do_rcv 

                     ->tcp_check_req 

                              ->tp->af_specific->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) 

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

        if(newsk != NULL) { 

                          …… 

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

                         newsk->sk_state = TCP_SYN_RECV; 

                          …… 

}

等到分析 tcp 棧的實作的時候,再來仔細分析它。但是這裡新的 sk 的有限狀态機被切換至了 TCP_SYN_RECV(按我的想法,似乎應進入 establshed 才對呀,是不是哪兒看漏了,隻有看了後頭的代碼再來印證了) 

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

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

                        int *uaddr_len, int peer) 

        struct sock *sk                = sock->sk; 

        struct inet_sock *inet        = inet_sk(sk); 

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

        sin->sin_family = AF_INET; 

        if (peer) { 

                if (!inet->dport || 

                    (((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_SYN_SENT)) && 

                     peer == 1)) 

                        return -ENOTCONN; 

                sin->sin_port = inet->dport; 

                sin->sin_addr.s_addr = inet->daddr; 

        } else { 

                __u32 addr = inet->rcv_saddr;                 if (!addr) 

                        addr = inet->saddr; 

                sin->sin_port = inet->sport; 

                sin->sin_addr.s_addr = addr; 

        } 

        memset(sin->sin_zero, 0, sizeof(sin->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>klen) 

                len=klen; 

        if(len<0 || len> MAX_SOCK_ADDR) 

                return -EINVAL; 

        if(len) 

        { 

                 if(copy_to_user(uaddr,kaddr,len)) 

                        return -EFAULT; 

        } 

        return __put_user(klen, ulen); 

}

也就是調用 copy_to_user的過程了。 

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

        if ((err = sock_map_fd(newsock)) < 0) 

                goto out_release;

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

小結: 

accept 有幾件事情要做:

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

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

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

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

繼續閱讀