您当前的位置: 首页 >  网络

phymat.nico

暂无认证

  • 0浏览

    0关注

    1967博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Linux内核分析 - 网络[十六]:TCP三次握手

phymat.nico 发布时间:2017-12-26 00:05:43 ,浏览量:0

  内核:2.6.34       TCP是应用最广泛的传输层协议,其提供了面向连接的、可靠的字节流服务,但也正是因为这些特性,使得TCP较之UDP异常复杂,还是分两部分[创建与使用]来进行分析。这篇主要包括TCP的创建及三次握手的过程。

      编程时一般用如下语句创建TCP Socket:

[cpp] view plain copy
  1. socket(AF_INET, SOCK_DGRAM, IPPROTO_TCP)  

      由此开始分析,调用接口[net/socket.c]: SYSCALL_DEFINE3(socket)       其中执行两步关键操作:sock_create()与sock_map_fd()

[cpp] view plain copy
  1. retval = sock_create(family, type, protocol, &sock);  
  2. if (retval create()。sock_alloc()分配了sock内存空间并初始化inode;pf->create()初始化了sk。

    [cpp] view plain copy
    1. sock = sock_alloc();  
    2. sock->type = type;  
    3. ……  
    4. pf = rcu_dereference(net_families[family]);  
    5. ……  
    6. pf->create(net, sock, protocol, kern);  

    sock_alloc()       分配空间,通过new_inode()分配了节点(包括socket),然后通过SOCKET_I宏获得sock,实际上inode和sock是在new_inode()中一起分配的,结构体叫作sock_alloc。

    [cpp] view plain copy
    1. inode = new_inode(sock_mnt->mnt_sb);  
    2. sock = SOCKET_I(inode);  

          设置inode的参数,并返回sock。

    [cpp] view plain copy
    1. inode->i_mode = S_IFSOCK | S_IRWXUGO;  
    2. inode->i_uid = current_fsuid();  
    3. inode->i_gid = current_fsgid();  
    4. return sock;  

          继续往下看具体的创建过程:new_inode(),在分配后,会设置i_ino和i_state的值。

    [cpp] view plain copy
    1. struct inode *new_inode(struct super_block *sb)  
    2. {  
    3.  ……  
    4.  inode = alloc_inode(sb);  
    5.  if (inode) {  
    6.   spin_lock(&inode_lock);  
    7.   __inode_add_to_lists(sb, NULL, inode);  
    8.   inode->i_ino = ++last_ino;  
    9.   inode->i_state = 0;  
    10.   spin_unlock(&inode_lock);  
    11.  }  
    12.  return inode;  
    13. }  

          其中的alloc_inode() -> sb->s_op->alloc_inode(),sb是sock_mnt->mnt_sb,所以alloc_inode()指向的是sockfs的操作函数sock_alloc_inode。

    [cpp] view plain copy
    1. static const struct super_operations sockfs_ops = {  
    2.  .alloc_inode = sock_alloc_inode,  
    3.  .destroy_inode =sock_destroy_inode,  
    4.  .statfs = simple_statfs,  
    5. };  

          sock_alloc_inode()中通过kmem_cache_alloc()分配了struct socket_alloc结构体大小的空间,而struct socket_alloc结构体定义如下,但只返回了inode,实际上socket和inode都已经分配了空间,在之后就可以通过container_of取到socket。

    [cpp] view plain copy
    1. static struct inode *sock_alloc_inode(struct super_block *sb)  
    2. {  
    3.  struct socket_alloc *ei;  
    4.  ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);  
    5.  …..  
    6.  return &ei->vfs_inode;  
    7. }  
    8. struct socket_alloc {  
    9.  struct socket socket;  
    10.  struct inode vfs_inode;  
    11. };  
    12.   
    13. net_families[AF_INET]:  
    14. static const struct net_proto_family inet_family_ops = {  
    15.  .family = PF_INET,  
    16.  .create = inet_create,  
    17.  .owner = THIS_MODULE,  
    18. };  

    err = pf->create(net, sock, protocol, kern); ==> inet_create()       这段代码就是从inetsw[]中取到适合的协议类型answer,sock->type就是传入socket()函数的type参数SOCK_DGRAM,最终取得结果answer->ops==inet_stream_ops,从上面这段代码还可以看出以下问题:       socket(AF_INET, SOCK_RAW, IPPROTO_IP)这样是不合法的,因为SOCK_RAW没有默认的协议类型;同样socket(AF_INET, SOCK_DGRAM, IPPROTO_IP)与socket(AF_INET, SOCK_DGRAM, IPPROTO_TCP)是一样的,因为TCP的默认协议类型是IPPTOTO_TCP;SOCK_STREAM与IPPROTO_UDP同上。

    [cpp] view plain copy
    1. sock->state = SS_UNCONNECTED;  
    2. list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {  
    3.  err = 0;  
    4.  /* Check the non-wild match. */  
    5.  if (protocol == answer->protocol) {  
    6.   if (protocol != IPPROTO_IP)  
    7.    break;  
    8.  } else {  
    9.   /* Check for the two wild cases. */  
    10.   if (IPPROTO_IP == protocol) {  
    11.    protocol = answer->protocol;  
    12.    break;  
    13.   }  
    14.   if (IPPROTO_IP == answer->protocol)  
    15.    break;  
    16.  }  
    17.  err = -EPROTONOSUPPORT;  
    18. }  

          sock->ops指向inet_stream_ops,然后创建sk,sk->proto指向tcp_prot,注意这里分配的大小是struct tcp_sock,而不仅仅是struct sock大小

    [cpp] view plain copy
    1. sock->ops = answer->ops;  
    2. answer_prot = answer->prot;  
    3. ……  
    4. sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);  

          然后设置inet的一些参数,这里直接将sk类型转换为inet,因为在sk_alloc()中分配的是struct tcp_sock结构大小,返回的是struct sock,利用了第一个成员的特性,三者之间的关系如下图:

    [cpp] view plain copy
    1. inet = inet_sk(sk);  
    2. ……  
    3. inet->inet_id = 0;  
    4. sock_init_data(sock, sk);  

          其中有些设置是比较重要的,如

    [cpp] view plain copy
    1. sk->sk_state = TCP_CLOSE;  
    2. sk_set_socket(sk, sock);  
    3. sk->sk_protocol = protocol;  
    4. sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;  

            创建socket后,接下来的流程会因为客户端或服务器的不同而有所差异,下面着重于分析建立连接的三次握手过程。典型的客户端流程: connect() -> send() -> recv()       典型的服务器流程: bind() -> listen() -> accept() -> recv() -> send()

     

    客户端流程 *发送SYN报文,向服务器发起tcp连接       connect(fd, servaddr, addrlen);        -> SYSCALL_DEFINE3()         -> sock->ops->connect() == inet_stream_connect (sock->ops即inet_stream_ops)        -> tcp_v4_connect()       查找到达[daddr, dport]的路由项,路由项的查找与更新与”路由表”章节所述一样。要注意的是由于是作为客户端调用,创建socket后调用connect,因而saddr, sport都是0,同样在未查找路由前,要走的出接口oif也是不知道的,因此也是0。在查找完路由表后(注意不是路由缓存),可以得知出接口,但并未存储到sk中。因此插入的路由缓存是特别要注意的:它的键值与实际值是不相同的,这个不同点就在于oif与saddr,键值是[saddr=0, sport=0, daddr, dport, oif=0],而缓存项值是[saddr, sport=0, daddr, dport, oif]。

    [cpp] view plain copy
    1. tmp = ip_route_connect(&rt, nexthop, inet->inet_saddr,  
    2.       RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,  
    3.       IPPROTO_TCP,  
    4.       inet->inet_sport, usin->sin_port, sk, 1);  
    5. if (tmp inet_saddr)  
    6.  inet->inet_saddr = rt_rt_src;   
    7. inet->inet_rcv_addr = inet->inet_saddr;  
    8. ……  
    9. inet->inet_dport = usin->sin_port;  
    10. inet->inet_daddr = daddr;  

          状态从CLOSING转到TCP_SYN_SENT,也就是我们熟知的TCP的状态转移图。

    [cpp] view plain copy
    1. tcp_set_state(sk, TCP_SYN_SENT);  

          插入到bind链表中

    [cpp] view plain copy
    1. err = inet_hash_connect(&tcp_death_row, sk); //== > __inet_hash_connect()  

          当snum==0时,表明此时源端口没有指定,此时会随机选择一个空闲端口作为此次连接的源端口。low和high分别表示可用端口的下限和上限,remaining表示可用端口的数,注意这里的可用只是指端口可以用作源端口,其中部分端口可能已经作为其它socket的端口号在使用了,所以要循环1~remaining,直到查找到空闲的源端口。

    [cpp] view plain copy
    1. if (!snum) {  
    2.  inet_get_local_port_range(&low, &high);  
    3.  remaining = (high - low) + 1;  
    4.  ……  
    5.  for (i = 1; i bhash_size)];  
    6.  ……  
    7.  inet_bind_bucket_for_each(tb, node, &head->chain) {  
    8.   if (net_eq(ib_net(tb), net) && tb->port == port) {  
    9.    if (tb->fastreuse >= 0)  
    10.     goto next_port;  
    11.    WARN_ON(hlist_empty(&tb->owners));  
    12.    if (!check_established(death_row, sk, port, &tw))  
    13.     goto ok;  
    14.    goto next_port;  
    15.   }  
    16.  }  
    17.   
    18.  tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep, net, head, port);  
    19.  ……  
    20.  next_port:  
    21.   spin_unlock(&head->lock);  
    22. }  
    23.   
    24. ok:  
    25.  ……  
    26. inet_bind_hash(sk, tb, port);  
    27.  ……  
    28.  goto out;  

          在获取到合适的源端口号后,会重建路由项来进行更新:

    [cpp] view plain copy
    1. err = ip_route_newports(&rt, IPPROTO_TCP, inet->inet_sport, inet->inet_dport, sk);  

          函数比较简单,在获取sport前已经查找过一次路由表,并插入了key=[saddr=0, sport=0, daddr, dport, oif=0]的路由缓存项;现在获取到了sport,调用ip_route_output_flow()再次更新路由缓存表,它会添加key=[saddr=0, sport, daddr, dport, oif=0]的路由缓存项。这里可以看出一个策略选择,查询路由表->获取sport->查询路由表,为什么不是获取sport->查询路由表的原因可能是效率的问题。

    [cpp] view plain copy
    1. if (sport != (*rp)->fl.fl_ip_sport ||  
    2.     dport != (*rp)->fl.fl_ip_dport) {  
    3.  struct flowi fl;  
    4.   
    5.  memcpy(&fl, &(*rp)->fl, sizeof(fl));  
    6.  fl.fl_ip_sport = sport;  
    7.  fl.fl_ip_dport = dport;  
    8.  fl.proto = protocol;  
    9.  ip_rt_put(*rp);  
    10.  *rp = NULL;  
    11.  security_sk_classify_flow(sk, &fl);  
    12.  return ip_route_output_flow(sock_net(sk), rp, &fl, sk, 0);  
    13. }  

          write_seq相当于第一次发送TCP报文的ISN,如果为0,则通过计算获取初始值,否则延用上次的值。在获取完源端口号,并查询过路由表后,TCP正式发送SYN报文,注意在这之前TCP状态已经更新成了TCP_SYN_SENT,而在函数最后才调用tcp_connect(sk)发送SYN报文,这中间是有时差的。

    [cpp] view plain copy
    1. if (!tp->write_seq)  
    2.  tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,  
    3.          inet->inet_daddr,  
    4.          inet->inet_sport,  
    5.          usin->sin_port);  
    6. inet->inet_id = tp->write_seq ^ jiffies;  
    7. err = tcp_connect(sk);  

    tcp_connect() 发送SYN报文       几步重要的代码如下,tcp_connect_init()中设置了tp->rcv_nxt=0,tcp_transmit_skb()负责发送报文,其中seq=tcb->seq=tp->write_seq,ack_seq=tp->rcv_nxt。

    [cpp] view plain copy
    1. tcp_connect_init(sk);  
    2. tp->snd_nxt = tp->write_seq;  
    3. ……  
    4. tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);  

      *收到服务端的SYN+ACK,发送ACK tcp_rcv_synsent_state_process()       此时已接收到对方的ACK,状态变迁到TCP_ESTABLISHED。最后发送对方SYN的ACK报文。

    [cpp] view plain copy
    1. tcp_set_state(sk, TCP_ESTABLISHED);  
    2. tcp_send_ack(sk);  

      服务端流程 *bind() -> inet_bind()       bind操作的主要作用是将创建的socket与给定的地址相绑定,这样创建的服务才能公开的让外部调用。当然对于socket服务器的创建来说,这一步不是必须的,在listen()时如果没有绑定地址,系统会选择一个随机可用地址作为服务器地址。 一个socket地址分为ip和port,inet->inet_saddr赋值了传入的ip,snum是传入的port,对于端口,要检查它是否已被占用,这是由sk->sk_prot->get_port()完成的(这个函数前面已经分析过,在传入port时它检查是否被占用;传入port=0时它选择未用的端口)。如果没有被占用,inet->inet_sport被赋值port,因为是服务监听端,不需要远端地址,inet_daddr和inet_dport都置0。 注意bind操作不会改变socket的状态,仍为创建时的TCP_CLOSE。

    [cpp] view plain copy
    1. snum = ntohs(addr->sin_port);  
    2. ……  
    3. inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;  
    4. if (sk->sk_prot->get_port(sk, snum)) {  
    5.  inet->inet_saddr = inet->inet_rcv_saddr = 0;  
    6.  err = -EADDRINUSE;  
    7.  goto out_release_sock;  
    8. }  
    9. ……  
    10. inet->inet_sport = htons(inet->inet_num);  
    11. inet->inet_daddr = 0;  
    12. inet->inet_dport = 0;  

      listen() -> inet_listen()       listen操作开始服务器的监听,此时服务就可以接受到外部连接了。在开始监听前,要检查状态是否正确,sock->state==SS_UNCONNECTED确保仍是未连接的socket,sock->type==SOCK_STREAM确保是TCP协议,old_state确保此时状态是TCP_CLOSE或TCP_LISTEN,在其它状态下进行listen都是错误的。

    [cpp] view plain copy
    1. if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)  
    2.  goto out;  
    3. old_state = sk->sk_state;  
    4. if (!((1  sys_accept4() -> inet_accept() -> inet_csk_accept()       accept()实际要做的事件并不多,它的作用是返回一个已经建立连接的socket(即经过了三次握手),这个过程是异步的,accept()并不亲自去处理三次握手过程,而只是监听icsk_accept_queue队列,当有socket经过了三次握手,它就会被加到icsk_accept_queue中,所以accept要做的就是等待队列中插入socket,然后被唤醒并返回这个socket。而三次握手的过程完全是协议栈本身去完成的。换句话说,协议栈相当于写者,将socket写入队列,accept()相当于读者,将socket从队列读出。这个过程从listen就已开始,所以即使不调用accept(),客户仍可以和服务器建立连接,但由于没有处理,队列很快会被占满。

      [cpp] view plain copy
      1. if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {  
      2.  long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);  
      3.  ……  
      4.  error = inet_csk_wait_for_connect(sk, timeo);  
      5.  ……  
      6. }  
      7.   
      8. newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);  

            协议栈向队列中加入socket的过程就是完成三次握手的过程,客户端通过向已知的listen fd发起连接请求,对于到来的每个连接,都会创建一个新的sock,当它经历了TCP_SYN_RCV -> TCP_ESTABLISHED后,就会被添加到icsk_accept_queue中,而监听的socket状态始终为TCP_LISTEN,保证连接的建立不会影响socket的接收。

      *接收客户端发来的SYN,发送SYN+ACK tcp_v4_do_rcv()       tcp_v4_do_rcv()是TCP模块接收的入口函数,客户端发起请求的对象是listen fd,所以sk->sk_state == TCP_LISTEN,调用tcp_v4_hnd_req()来检查是否处于半连接,只要三次握手没有完成,这样的连接就称为半连接,具体而言就是收到了SYN,但还没有收到ACK的连接,所以对于这个查找函数,如果是SYN报文,则会返回listen的socket(连接尚未创建);如果是ACK报文,则会返回SYN报文处理中插入的半连接socket。其中存储这些半连接的数据结构是syn_table,它在listen()调用时被创建,大小由sys_ctl_max_syn_backlog和listen()传入的队列长度决定。 此时是收到SYN报文,tcp_v4_hnd_req()返回的仍是sk,调用tcp_rcv_state_process()来接收SYN报文,并发送SYN+ACK报文,同时向syn_table中插入一项表明此次连接的sk。

      [cpp] view plain copy
      1. if (sk->sk_state == TCP_LISTEN) {  
      2.  struct sock *nsk = tcp_v4_hnd_req(sk, skb);  
      3.  if (!nsk)  
      4.   goto discard;  
      5.  if (nsk != sk) {  
      6.   if (tcp_child_process(sk, nsk, skb)) {  
      7.    rsk = nsk;  
      8.    goto reset;  
      9.   }  
      10.   return 0;  
      11.  }  
      12. }  
      13. TCP_CHECK_TIMER(sk);  
      14. if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {  
      15.  rsk = sk;  
      16.  goto reset;  
      17. }  

            tcp_rcv_state_process()处理各个状态上socket的情况。下面是处于TCP_LISTEN的代码段,处于TCP_LISTEN的socket不会再向其它状态变迁,它负责监听,并在连接建立时创建新的socket。实际上,当收到第一个SYN报文时,会执行这段代码,conn_request() => tcp_v4_conn_request。

      [cpp] view plain copy
      1. case TCP_LISTEN:  
      2. ……  
      3.  if (th->syn) {  
      4.   if (icsk->icsk_af_ops->conn_request(sk, skb) sk_state == TCP_LISTEN) {  
      5.  struct sock *nsk = tcp_v4_hnd_req(sk, skb);  
      6.  if (!nsk)  
      7.   goto discard;  
      8.  if (nsk != sk) {  
      9.   if (tcp_child_process(sk, nsk, skb)) {  
      10.    rsk = nsk;  
      11.    goto reset;  
      12.   }  
      13.   return 0;  
      14.  }  
      15. }  

      tcp_v4_hnd_req()       之前已经分析过,inet_csk_search_req()会在syn_table中找到req,此时进入tcp_check_req()

      [cpp] view plain copy
      1. struct request_sock *req = inet_csk_search_req(sk, &prev, th->source, iph->saddr, iph->daddr);  
      2. if (req)  
      3.  return tcp_check_req(sk, skb, req, prev);  

      tcp_check_req()       syn_recv_sock() -> tcp_v4_syn_recv_sock()会创建一个新的sock并返回,创建的sock状态被直接设置为TCP_SYN_RECV,然后因为此时socket已经建立,将它添加到icsk_accept_queue中。       状态TCP_SYN_RECV的设置可能比较奇怪,按照TCP的状态转移图,在服务端收到SYN报文后变迁为TCP_SYN_RECV,但看到在实现中收到ACK后才有了状态TCP_SYN_RECV,并且马上会变为TCP_ESTABLISHED,所以这个状态变得无足轻重。这样做的原因是listen和accept返回的socket是不同的,而只有真正连接建立时才会创建这个新的socket,在收到SYN报文时新的socket还没有建立,就无从谈状态变迁了。这里同样是一个平衡的存在,你也可以在收到SYN时创建一个新的socket,代价就是无用的socket大大增加了。

      [cpp] view plain copy
      1. child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);  
      2. if (child == NULL)  
      3.  goto listen_overflow;  
      4. inet_csk_reqsk_queue_unlink(sk, req, prev);  
      5. inet_csk_reqsk_queue_removed(sk, req);  
      6. inet_csk_reqsk_queue_add(sk, req, child);  

      tcp_child_process()       如果此时sock: child被用户进程锁住了,那么就先添加到backlog中__sk_add_backlog(),待解锁时再处理backlog上的sock;如果此时没有被锁住,则先调用tcp_rcv_state_process()进行处理,处理完后,如果child状态到达TCP_ESTABLISHED,则表明其已就绪,调用sk_data_ready()唤醒等待在isck_accept_queue上的函数accept()。

      [cpp] view plain copy
      1. if (!sock_owned_by_user(child)) {  
      2.  ret = tcp_rcv_state_process(child, skb, tcp_hdr(skb), skb->len);  
      3.  if (state == TCP_SYN_RECV && child->sk_state != state)  
      4.   parent->sk_data_ready(parent, 0);  
      5. } else {  
      6.  __sk_add_backlog(child, skb);  
      7. }  

            tcp_rcv_state_process()处理各个状态上socket的情况。下面是处于TCP_SYN_RECV的代码段,注意此时传入函数的sk已经是新创建的sock了(在tcp_v4_hnd_req()中),并且状态是TCP_SYN_RECV,而不再是listen socket,在收到ACK后,sk状态变迁为TCP_ESTABLISHED,而在tcp_v4_hnd_req()中也已将sk插入到了icsk_accept_queue上,此时它就已经完全就绪了,回到tcp_child_process()便可执行sk_data_ready()。

      [cpp] view plain copy
      1. case TCP_SYN_RECV:  
      2.  if (acceptable) {  
      3.   ……  
      4.   tcp_set_state(sk, TCP_ESTABLISHED);  
      5.   sk->sk_state_change(sk);  
      6.   ……  
      7.   tp->snd_una = TCP_SKB_CB(skb)->ack_seq;  
      8.   tp->snd_wnd = ntohs(th->window) seq);   
      9.   ……  
      10. }  

            最后总结三次握手的过程

      • 本文已收录于以下专栏:
      • Linux内核协议栈
关注
打赏
1659628745
查看更多评论
立即登录/注册

微信扫码登录

0.0951s