tcp connection setup的实现(三)

42 篇文章 1 订阅
21 篇文章 0 订阅
先来看下accept的实现. 

其实accept的作用很简单,就是从accept队列中取出三次握手完成的socket,并将它关联到vfs上(其实操作和调用sys_socket时新建一个socket类似).然后返回.这里还有个要注意的,如果这个传递给accept的socket是非阻塞的话,就算accept队列为空,也会直接返回,而是阻塞的话就会休眠掉,等待accept队列有数据后唤醒他. 

接下来我们就来看它的实现,accept对应的系统调用是 sys_accept,而他则会调用do_accept,因此我们直接来看do_accept: 

Java代码   收藏代码
  1. long do_accept(int fd, struct sockaddr __user *upeer_sockaddr,  
  2.            int __user *upeer_addrlen, int flags)  
  3. {  
  4.     struct socket *sock, *newsock;  
  5.     struct file *newfile;  
  6.     int err, len, newfd, fput_needed;  
  7.     struct sockaddr_storage address;  
  8. .............................................  
  9. ///这个函数前面已经分析过了,也就是通过fd,得到相应的socket.  
  10.     sock = sockfd_lookup_light(fd, &err, &fput_needed);  
  11.     if (!sock)  
  12.         goto out;  
  13.   
  14.     err = -ENFILE;  
  15. ///新建一个socket,也就是这个函数将要返回的socket.这里注意我们得到的是一个socket,而不是sock.下面会解释为什么这么做.  
  16.     if (!(newsock = sock_alloc()))  
  17.         goto out_put;  
  18.   
  19.     newsock->type = sock->type;  
  20.     newsock->ops = sock->ops;  
  21.   
  22.     /* 
  23.      * We don't need try_module_get here, as the listening socket (sock) 
  24.      * has the protocol module (sock->ops->owner) held. 
  25.      */  
  26.     __module_get(newsock->ops->owner);  
  27. ///找到一个新的可用的文件句柄,以及file结构.是为了与刚才新建的socket关联起来.  
  28.     newfd = sock_alloc_fd(&newfile, flags & O_CLOEXEC);  
  29.     if (unlikely(newfd < 0)) {  
  30.         err = newfd;  
  31.         sock_release(newsock);  
  32.         goto out_put;  
  33.     }  
  34. ///将新的socket和file关联起来.(这里所做的和我们第一篇所分析的信件socket的步骤是一样的,不理解的,可以去看我前面的blog  
  35.     err = sock_attach_fd(newsock, newfile, flags & O_NONBLOCK);  
  36.     if (err < 0)  
  37.         goto out_fd_simple;  
  38.   
  39.     err = security_socket_accept(sock, newsock);  
  40.     if (err)  
  41.         goto out_fd;  
  42. ///调用inet_accept  
  43.     err = sock->ops->accept(sock, newsock, sock->file->f_flags);  
  44.     if (err < 0)  
  45.         goto out_fd;  
  46. ///这里也就是取得accept到的句柄的源地址.也就是填充传递进来的upeer_sockaddr.  
  47.     if (upeer_sockaddr) {  
  48.         if (newsock->ops->getname(newsock, (struct sockaddr *)&address,  
  49.                       &len, 2) < 0) {  
  50.             err = -ECONNABORTED;  
  51.             goto out_fd;  
  52.         }  
  53.         err = move_addr_to_user((struct sockaddr *)&address,  
  54.                     len, upeer_sockaddr, upeer_addrlen);  
  55.         if (err < 0)  
  56.             goto out_fd;  
  57.     }  
  58.   
  59.     /* File flags are not inherited via accept() unlike another OSes. */  
  60. ///最终将新的file结构和fd关联起来,其实也就是最终将这个fd关联到当前进程的files中.  
  61.     fd_install(newfd, newfile);  
  62.     err = newfd;  
  63.   
  64.     security_socket_post_accept(sock, newsock);  
  65.   
  66. out_put:  
  67. ///文件描述符的引用计数加一.  
  68.     fput_light(sock->file, fput_needed);  
  69. out:  
  70. ///返回句柄.  
  71.     return err;  
  72. .......................................  
  73. }  


可以看到流程很简单,最终的实现都集中在inet_accept中了.而inet_accept主要做的就是 

1 调用inet_csk_accept来进行对accept队列的操作.它会返回取得的sock. 

2 将从inet_csk_accept返回的sock链接到传递进来的(也就是在do_accept中new的socket)中.这里就知道我们上面为什么只需要new一个socket而不是sock了.因为sock我们是直接从accept队列中取得的. 


3 设置新的socket的状态为SS_CONNECTED. 

Java代码   收藏代码
  1. int inet_accept(struct socket *sock, struct socket *newsock, int flags)  
  2. {  
  3.     struct sock *sk1 = sock->sk;  
  4.     int err = -EINVAL;  
  5. ///调用inet_csk_accept.  
  6.     struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);  
  7.   
  8.     if (!sk2)  
  9.         goto do_err;  
  10.   
  11.     lock_sock(sk2);  
  12. ///测试tcp连接的状态.  
  13.     WARN_ON(!((1 << sk2->sk_state) &  
  14.           (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_CLOSE)));  
  15. ///将返回的sock链接到socket.  
  16.     sock_graft(sk2, newsock);  
  17. ///设置状态.  
  18.     newsock->state = SS_CONNECTED;  
  19.     err = 0;  
  20.     release_sock(sk2);  
  21. do_err:  
  22.     return err;  
  23. }  


inet_csk_accept就是从accept队列中取出sock然后返回. 

在看他的源码之前先来看几个相关函数的实现: 

首先是reqsk_queue_empty,他用来判断accept队列是否为空: 

Java代码   收藏代码
  1. static inline int reqsk_queue_empty(struct request_sock_queue *queue)  
  2. {  
  3.     return queue->rskq_accept_head == NULL;  
  4. }  


然后是reqsk_queue_get_child,他主要是从accept队列中得到一个sock: 

Java代码   收藏代码
  1. static inline struct sock *reqsk_queue_get_child(struct request_sock_queue *queue,  
  2.                          struct sock *parent)  
  3. {  
  4. ///首先从accept队列中remove这个socket并返回.  
  5.     struct request_sock *req = reqsk_queue_remove(queue);  
  6. ///取得socket.  
  7.     struct sock *child = req->sk;  
  8.   
  9.     WARN_ON(child == NULL);  
  10. ///这里主要是将sk_ack_backlog减一,也就是accept当前的数目减一.  
  11.     sk_acceptq_removed(parent);  
  12.     __reqsk_free(req);  
  13.     return child;  
  14. }  


这里还有一个inet_csk_wait_for_connect,它是用来在accept队列为空的情况下,休眠掉一段时间 (这里每个socket都有一个等待队列的(等待队列的用法请google,我这里就不阐述了).这里是每个调用的进程都会声明一个wait队列,然后将它连接到主的socket的等待队列链表中,然后休眠,等到唤醒. 

Java代码   收藏代码
  1. static int inet_csk_wait_for_connect(struct sock *sk, long timeo)  
  2. {  
  3.     struct inet_connection_sock *icsk = inet_csk(sk);  
  4. ///定义一个waitqueue.  
  5.     DEFINE_WAIT(wait);  
  6.     int err;  
  7. ..................................................  
  8.     for (;;) {  
  9. ///这里也就是把当前的进程的等待队列挂入sk中的sk_sleep队列,sk也就是主的那个socket.  
  10.         prepare_to_wait_exclusive(sk->sk_sleep, &wait,  
  11.                       TASK_INTERRUPTIBLE);  
  12.         release_sock(sk);  
  13. ///再次判断是否为空.  
  14.         if (reqsk_queue_empty(&icsk->icsk_accept_queue))  
  15. ///这个函数里面会休眠timeo时间(调用schedule让出cpu),或者被当accept队列有数据时唤醒(我们前面也有介绍这个)主的等待队列链表.,  
  16.             timeo = schedule_timeout(timeo);  
  17.         lock_sock(sk);  
  18.         err = 0;  
  19. ///非空则跳出.  
  20.         if (!reqsk_queue_empty(&icsk->icsk_accept_queue))  
  21.             break;  
  22.         err = -EINVAL;  
  23.         if (sk->sk_state != TCP_LISTEN)  
  24.             break;  
  25.         err = sock_intr_errno(timeo);  
  26.         if (signal_pending(current))  
  27.             break;  
  28. ///设置错误号.  
  29.         err = -EAGAIN;  
  30. ///时间为0则直接退出.  
  31.         if (!timeo)  
  32.             break;  
  33.     }  
  34. ///这里也就会从sk_sleep中remove掉当前的wait队列.  
  35.     finish_wait(sk->sk_sleep, &wait);  
  36.     return err;  
  37. }  


然后来看inet_csk_accept的源码,这里有个阻塞和非阻塞的问题.非阻塞的话会直接返回的,就算accept队列为空.这个时侯设置errno为-EAGAIN. 

Java代码   收藏代码
  1. struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)  
  2. {  
  3.     struct inet_connection_sock *icsk = inet_csk(sk);  
  4.     struct sock *newsk;  
  5.     int error;  
  6.   
  7.     lock_sock(sk);  
  8.   
  9.     /* We need to make sure that this socket is listening, 
  10.      * and that it has something pending. 
  11.      */  
  12.     error = -EINVAL;  
  13. ///sk也就是主socket,他的状态我们前面也讲过会一直是TCP_LISTEN.  
  14.     if (sk->sk_state != TCP_LISTEN)  
  15.         goto out_err;  
  16.   
  17. ///然后判断accept队列是否为空  
  18.     if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {  
  19. ///如果是O_NONBLOCK,则返回0,此时下面的inet_csk_wait_for_connect也就会立即返回.  
  20.         long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);  
  21.   
  22.         /* If this is a non blocking socket don't sleep */  
  23.         error = -EAGAIN;  
  24.         if (!timeo)  
  25.             goto out_err;  
  26. ///休眠或者立即返回.  
  27.         error = inet_csk_wait_for_connect(sk, timeo);  
  28.         if (error)  
  29.             goto out_err;  
  30.     }  
  31. ///得到sock并从accept队列中remove.  
  32.     newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);  
  33.     WARN_ON(newsk->sk_state == TCP_SYN_RECV);  
  34. out:  
  35.     release_sock(sk);  
  36.     return newsk;  
  37. out_err:  
  38.     newsk = NULL;  
  39.     *err = error;  
  40.     goto out;  
  41. }  


最后来大概分析下connect的实现.它的具体流程是: 

1 由fd得到socket,并且将地址复制到内核空间 

2 调用inet_stream_connect进行主要的处理. 

这里要注意connect也有个阻塞和非阻塞的区别,阻塞的话调用inet_wait_for_connect休眠,等待握手完成,否则直接返回. 

Java代码   收藏代码
  1. asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr,  
  2.                 int addrlen)  
  3. {  
  4.     struct socket *sock;  
  5.     struct sockaddr_storage address;  
  6.     int err, fput_needed;  
  7. ///得到socket.  
  8.     sock = sockfd_lookup_light(fd, &err, &fput_needed);  
  9.     if (!sock)  
  10.         goto out;  
  11. ///拷贝地址.  
  12.     err = move_addr_to_kernel(uservaddr, addrlen, (struct sockaddr *)&address);  
  13.     if (err < 0)  
  14.         goto out_put;  
  15.   
  16.     err =  
  17.         security_socket_connect(sock, (struct sockaddr *)&address, addrlen);  
  18.     if (err)  
  19.         goto out_put;  
  20. ///调用处理函数.  
  21.     err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,  
  22.                  sock->file->f_flags);  
  23. out_put:  
  24.     fput_light(sock->file, fput_needed);  
  25. out:  
  26.     return err;  
  27. }  



然后来看inet_stream_connect,他的主要工作是: 

1 判断socket的状态.只有当为SS_UNCONNECTED也就是非连接状态时才调用tcp_v4_connect来进行连接处理. 

2 判断tcp的状态sk_state只能为TCPF_SYN_SENT或者TCPF_SYN_RECV,才进入相关处理. 

3 如果状态合适并且socket为阻塞模式则调用inet_wait_for_connect进入休眠等待握手完成,否则直接返回,并设置错误号为EINPROGRESS. 

Java代码   收藏代码
  1. int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,  
  2.             int addr_len, int flags)  
  3. {  
  4.     struct sock *sk = sock->sk;  
  5.     int err;  
  6.     long timeo;  
  7.   
  8.     lock_sock(sk);  
  9. ............................................  
  10.   
  11.     switch (sock->state) {  
  12.     default:  
  13.         err = -EINVAL;  
  14.         goto out;  
  15.     case SS_CONNECTED:  
  16.         err = -EISCONN;  
  17.         goto out;  
  18.     case SS_CONNECTING:  
  19.         err = -EALREADY;  
  20.         /* Fall out of switch with err, set for this state */  
  21.         break;  
  22.     case SS_UNCONNECTED:  
  23.         err = -EISCONN;  
  24.         if (sk->sk_state != TCP_CLOSE)  
  25.             goto out;  
  26. ///调用tcp_v4_connect来处理连接.主要是发送syn.  
  27.         err = sk->sk_prot->connect(sk, uaddr, addr_len);  
  28.         if (err < 0)  
  29.             goto out;  
  30. ///设置状态.  
  31.         sock->state = SS_CONNECTING;  
  32. ///设置错误号.  
  33.         err = -EINPROGRESS;  
  34.         break;  
  35.     }  
  36. ///和上面的处理一样,如果非阻塞返回0,否则返回timeo.  
  37.     timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);  
  38.   
  39.     if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {  
  40. ///如果非阻塞则直接返回.否则进入休眠等待三次握手完成并唤醒他.(这个函数和上面的inet_csk_wait_for_connect函数实现很类似,因此这里就不分析了)  
  41.         if (!timeo || !inet_wait_for_connect(sk, timeo))  
  42.             goto out;  
  43.   
  44.         err = sock_intr_errno(timeo);  
  45.         if (signal_pending(current))  
  46.             goto out;  
  47.     }  
  48.   
  49.     /* Connection was closed by RST, timeout, ICMP error 
  50.      * or another process disconnected us. 
  51.      */  
  52.     if (sk->sk_state == TCP_CLOSE)  
  53.         goto sock_error;  
  54. ///设置socket状态.为已连接.  
  55.     sock->state = SS_CONNECTED;  
  56.     err = 0;  
  57. out:  
  58.     release_sock(sk);  
  59.     return err;  
  60.   
  61. sock_error:  
  62.     err = sock_error(sk) ? : -ECONNABORTED;  
  63.     sock->state = SS_UNCONNECTED;  
  64.     if (sk->sk_prot->disconnect(sk, flags))  
  65.         sock->state = SS_DISCONNECTING;  
  66.     goto out;  
  67. }  


tcp_v4_connect的源码就不分析了,我这里只大概的介绍下他的流程: 

1 判断地址的一些合法性. 

2 调用ip_route_connect来查找出去的路由(包括查找临时端口等等). 

3 设置sock的状态为TCP_SYN_SENT,并调用inet_hash_connect来查找一个临时端口(也就是我们出去的端口),并加入到对应的hash链表(具体操作和get_port很相似). 

4 调用tcp_connect来完成最终的操作.这个函数主要用来初始化将要发送的syn包(包括窗口大小isn等等),然后将这个sk_buffer加入到socket的写队列.最终调用tcp_transmit_skb传输到3层.再往下的操作就可以看我前面的blog了. 

最后来看下3次握手的客户端的状态变化,还是看tcp_rcv_state_process函数,这里我们进来的socket假设就是TCP_SYN_SENT状态,也就是在等待syn和ack分节: 

Java代码   收藏代码
  1. int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,  
  2.               struct tcphdr *th, unsigned len)  
  3. {  
  4. ..........................................  
  5.   
  6.     switch (sk->sk_state) {  
  7.     case TCP_CLOSE:  
  8.         goto discard;  
  9.   
  10.     case TCP_LISTEN:  
  11.         ..................................  
  12.   
  13.     case TCP_SYN_SENT:  
  14. ///进入对应的状态机处理函数.  
  15.         queued = tcp_rcv_synsent_state_process(sk, skb, th, len);  
  16.         if (queued >= 0)  
  17.             return queued;  
  18.   
  19.         /* Do step6 onward by hand. */  
  20.         tcp_urg(sk, skb, th);  
  21.         __kfree_skb(skb);  
  22.         tcp_data_snd_check(sk);  
  23.         return 0;  
  24.     }  



然后来看tcp_rcv_synsent_state_process中的状态变化: 


Java代码   收藏代码
  1. static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,  
  2.                      struct tcphdr *th, unsigned len)  
  3. {  
  4. ..................  
  5.   
  6.     if (th->ack) {  
  7. ....................................  
  8. ///如果是rst分节,则进行相关处理,  
  9.         if (th->rst) {  
  10.             tcp_reset(sk);  
  11.             goto discard;  
  12.         }  
  13. ///如果过来的ack分节没有syn分节则直接丢掉这个包,然后返回.  
  14.         if (!th->syn)  
  15.             goto discard_and_undo;  
  16.   
  17. ..................................................  
  18. ///如果校验都通过则设置状态为TCP_ESTABLISHED,下面就会发送最后一个ack分节.  
  19.         tcp_set_state(sk, TCP_ESTABLISHED);  
  20.   
  21.         .......................................  
  22.     }  
  23.   
  24. ....................................................  
  25.   
  26.     if (th->syn) {  
  27. ///如果只有syn分节,则此时设置状态为TCP_SYN_RECV.  
  28.         tcp_set_state(sk, TCP_SYN_RECV);  
  29.   
  30. ...................................  
  31. ///发送ack分节给对方.  
  32.         tcp_send_synack(sk);  
  33.         goto discard;  
  34. #endif  
  35.     }  
  36. ...................  
  37. }  


这里如果只接受到syn,则三次握手还没完成,我们还在等待最后一个ack,因此此时有数据报的话,会再次落入tcp_rcv_state_process函数: 

Java代码   收藏代码
  1. if (th->ack) {  
  2. ///是否这个ack可被接受.  
  3.         int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH);  
  4.   
  5.         switch (sk->sk_state) {  
  6.         case TCP_SYN_RECV:  
  7.             if (acceptable) {  
  8.   
  9.                 tp->copied_seq = tp->rcv_nxt;  
  10.                 smp_mb();  
  11. ///设置为TCP_ESTABLISHED,三次握手完成.  
  12.                 tcp_set_state(sk, TCP_ESTABLISHED);  
  13.                 sk->sk_state_change(sk);  
  14. ///唤醒休眠在connect的队列.  
  15.                 if (sk->sk_socket)  
  16.                     sk_wake_async(sk,  
  17.                               SOCK_WAKE_IO, POLL_OUT);  
  18.   
  19.             ........................................  
  20.             } else {  
  21.                 return 1;  
  22.             }  
  23.             break;  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值