Linux TCP/IP协议栈之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;                
        ……
        /*  回送一个 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->listen_opt;
       
        /*  计算一个 hash值 */
        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;

        /*指针移到 hash 链的未尾*/
        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:
[code]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;
        /* Find possible connection requests. */
        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 表:
[code]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(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, &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;
 
        /*
         * We don't need try_module_get here, as the listening socket (sock)
         * has the protocol module (sock->ops->owner) held.
         */
        __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;
        }
 
        /* File flags are not inherited via accept() unlike another OSes. */
 
        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;
}[/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->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.
/*
*        This will accept the next outstanding connection.
*/
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);
 
        /* We need to make sure that this socket is listening,
         * and that it has something pending.
         */
        error = -EINVAL;
        if (sk->sk_state != TCP_LISTEN)
                goto out;
 
        /* Find already established connection */
        if (!tp->accept_queue) {
                long timeo = sock_rcvtimeo(sk, flags & 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)
                        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)
{
        /* 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->sk_prot -acme */
        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:
/*
*        This does both peername and sockname.
*/
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;
        }
        /*
         *        "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)) < 0)
                goto out_release;
 
函数 sock_map_fd 在创建 socket 中已经见过了。
 
小结:
accept 有几件事情要做
1. 要 accept需要三次握手完成, 连接请求入tp->accept_queue队列(新为客户端分析的 sk, 也在其中), 其才能出队
2. 为 accept分配一个sokcet结构, 并将其与新的sk关联
3. 如果调用时,需要获取客户端地址,即第二个参数不为 NULL,则从新的 sk 中,取得其想的葫芦;
4. 将新的 socket 结构与文件系统挂钩;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

隨意的風

如果你觉得有帮助,期待你的打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值