Chinaunix首页 | 论坛 | 博客
  • 博客访问: 938361
  • 博文数量: 403
  • 博客积分: 27
  • 博客等级: 民兵
  • 技术积分: 165
  • 用 户 组: 普通用户
  • 注册时间: 2011-12-25 22:20
文章分类

全部博文(403)

文章存档

2016年(3)

2015年(16)

2014年(163)

2013年(222)

分类: LINUX

2013-12-20 11:00:33

http://blog.csdn.net/zhaqiwen/article/details/7727092
一、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: 
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 表: 
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; 
 
        /* 
         * 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; 

 
代码稍长了点,逐步来分析它。 
 
一个 socket,经过 listen(2)设置成 server 套接字后,就永远不会再与任何客户端套接字建立连接了。因为一旦它接受了一个连接请求,就会  创建出一个新的 socket,新的 socket 用来描述新到达的连接,而原先的 server套接字并无改变,并且还可以通过下一次 accept()调用  再创建一个新的出来,就像母鸡下蛋一样,“只取蛋,不杀鸡”,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 结构与文件系统挂钩; 
阅读(541) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~