天天看点

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 结构与文件系统挂钩;

继续阅读