Linux tcp/ip 源码分析 - three way handshake

在上一篇文章中我们讲到,connect方法会发送syn消息给服务端,之后客户端会进入TCP_SYN_SENT状态。

这就是tcp三次握手的第一步。

接下来我们看下,服务端收到syn消息后的处理逻辑。

首先,ip层在收到消息后,会调用tcp_v4_rcv方法将消息转给tcp层。

// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
  ...
  const struct iphdr *iph;
  const struct tcphdr *th;
  ...
  struct sock *sk;
  int ret;
  ...
  th = (const struct tcphdr *)skb->data;
  iph = ip_hdr(skb);
  ...
  sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
             th->dest, sdif, &refcounted);
  ...
  if (sk->sk_state == TCP_LISTEN) {
    ret = tcp_v4_do_rcv(sk, skb);
    goto put_and_return;
  }
  ...
  return ret;
  ...
}

方法描述

1. 调用__inet_lookup_skb方法,根据ip、端口等信息在全局的hashtable中找到对应的sock。

2. 此时sk->sk_state应该是TCP_LISTEN状态,所以会继续调用tcp_v4_do_rcv方法。

// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
  struct sock *rsk;
  ...
  if (tcp_rcv_state_process(sk, skb)) {
    ...
    goto reset;
  }
  return 0;
  ...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);

该方法会继续调用tcp_rcv_state_process方法。

// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
  ...
  struct inet_connection_sock *icsk = inet_csk(sk);
  const struct tcphdr *th = tcp_hdr(skb);
  ...
  switch (sk->sk_state) {
  ...
  case TCP_LISTEN:
    ...
    if (th->syn) {
      ...
      acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
      ...
      return 0;
    }
  ...
  }
  ...
}
EXPORT_SYMBOL(tcp_rcv_state_process);

因为客户端发来的是syn消息,而服务端此时是TCP_LISTEN状态,所以该方法最终会调用icsk->icsk_af_ops->conn_request(sk, skb)方法。

由第一篇文章我们可以知道,icsk->icsk_af_ops字段的值为&ipv4_specific,所以icsk->icsk_af_ops->conn_request指向的方法为tcp_v4_conn_request。

// net/ipv4/tcp_ipv4.c
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
  ...
  return tcp_conn_request(&tcp_request_sock_ops,
        &tcp_request_sock_ipv4_ops, sk, skb);
  ...
}
EXPORT_SYMBOL(tcp_v4_conn_request);

该方法又调用了tcp_conn_request方法,其中前两个参数为两个结构体,内容如下

// net/ipv4/tcp_ipv4.c
struct request_sock_ops tcp_request_sock_ops __read_mostly = {
  .family    =  PF_INET,
  .obj_size  =  sizeof(struct tcp_request_sock),
  .rtx_syn_ack  =  tcp_rtx_synack,
  .send_ack  =  tcp_v4_reqsk_send_ack,
  .destructor  =  tcp_v4_reqsk_destructor,
  .send_reset  =  tcp_v4_send_reset,
  .syn_ack_timeout =  tcp_syn_ack_timeout,
};

static const struct tcp_request_sock_ops tcp_request_sock_ipv4_ops = {
  .mss_clamp  =  TCP_MSS_DEFAULT,
#ifdef CONFIG_TCP_MD5SIG
  .req_md5_lookup  =  tcp_v4_md5_lookup,
  .calc_md5_hash  =  tcp_v4_md5_hash_skb,
#endif
  .init_req  =  tcp_v4_init_req,
#ifdef CONFIG_SYN_COOKIES
  .cookie_init_seq =  cookie_v4_init_sequence,
#endif
  .route_req  =  tcp_v4_route_req,
  .init_seq  =  tcp_v4_init_seq,
  .init_ts_off  =  tcp_v4_init_ts_off,
  .send_synack  =  tcp_v4_send_synack,
};

tcp_conn_request方法如下

// net/ipv4/tcp_input.c
int tcp_conn_request(struct request_sock_ops *rsk_ops,
         const struct tcp_request_sock_ops *af_ops,
         struct sock *sk, struct sk_buff *skb)
{
  ...
  struct request_sock *req;
  ...
  req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
  ...
  tcp_rsk(req)->af_specific = af_ops;
  ...
  if (fastopen_sk) {
    ...
  } else {
    ...
    if (!want_cookie)
      inet_csk_reqsk_queue_hash_add(sk, req,
        tcp_timeout_init((struct sock *)req));
    af_ops->send_synack(sk, dst, &fl, req, &foc,
            !want_cookie ? TCP_SYNACK_NORMAL :
               TCP_SYNACK_COOKIE);
    ...
  }
  ...
  return 0;
  ...
}
EXPORT_SYMBOL(tcp_conn_request);

方法描述

1. 调用inet_reqsk_alloc方法,分配并初始化一个struct request_sock实例,用于存储连接请求数据。

2. 将tcp_rsk(req)->af_specific的值设置为af_ops,即&tcp_request_sock_ipv4_ops。

3. 调用inet_csk_reqsk_queue_hash_add方法,将连接请求实例req添加到全局hashtable中。

4. 调用af_ops->send_synack发送synack消息给客户端。

我们再来看下inet_reqsk_alloc方法

// net/ipv4/tcp_input.c
struct request_sock *inet_reqsk_alloc(const struct request_sock_ops *ops,
              struct sock *sk_listener,
              bool attach_listener)
{
  struct request_sock *req = reqsk_alloc(ops, sk_listener,
                 attach_listener);

  if (req) {
    struct inet_request_sock *ireq = inet_rsk(req);
    ...
    ireq->ireq_state = TCP_NEW_SYN_RECV;
    ...
  }

  return req;
}
EXPORT_SYMBOL(inet_reqsk_alloc);

该方法中要注意的就是,ireq->ireq_state的值被设置为TCP_NEW_SYN_RECV,这个后面会用到。

该方法有调用了reqsk_alloc方法,继续看下。

// include/net/request_sock.h
static inline struct request_sock *
reqsk_alloc(const struct request_sock_ops *ops, struct sock *sk_listener,
      bool attach_listener)
{
  struct request_sock *req;

  req = kmem_cache_alloc(ops->slab, GFP_ATOMIC | __GFP_NOWARN);
  ...
  if (attach_listener) {
    ...
    req->rsk_listener = sk_listener;
  }
  req->rsk_ops = ops;
  req_to_sk(req)->sk_prot = sk_listener->sk_prot;
  ...
  return req;
}

该方法需要注意的是

1. req->rsk_listener字段被设置为sk_listener,即listen sock。

2. req->rsk_ops被设置为ops,即&tcp_request_sock_ops。

到这里tcp_conn_request方法就算分析结束了。

至此,tcp三次握手的第二步结束。

我们再来看下客户端在收到synack消息时是如何处理的。

还是回到tcp_v4_rcv方法,此时sock的状态为TCP_SYN_SENT。

// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
  ...
  const struct tcphdr *th;
  struct sock *sk;
  ...
  th = (const struct tcphdr *)skb->data;
  ...
  sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
             th->dest, sdif, &refcounted);
  ...
  ret = 0;
  if (!sock_owned_by_user(sk)) {
    ret = tcp_v4_do_rcv(sk, skb);
  } else if (tcp_add_backlog(sk, skb)) {
    ...
  }
  ...
  return ret;
  ...
}

方法描述

1. 调用__inet_lookup_skb方法,根据ip、端口等信息,从全局hashtable中找到对应的sock。

2. 调用tcp_v4_do_rcv方法,继续处理。

// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
  struct sock *rsk;
  ...
  if (tcp_rcv_state_process(sk, skb)) {
    ...
    goto reset;
  }
  return 0;
  ...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);

该方法又调用了tcp_rcv_state_process方法。

// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
  ...
  const struct tcphdr *th = tcp_hdr(skb);
  ...
  switch (sk->sk_state) {
  ...
  case TCP_SYN_SENT:
    ...
    queued = tcp_rcv_synsent_state_process(sk, skb, th);
    ...
    return 0;
  }
  ...
}
EXPORT_SYMBOL(tcp_rcv_state_process);

因为客户端此时的状态为TCP_SYN_SENT,所以该方法最终会调用tcp_rcv_synsent_state_process方法。

// net/ipv4/tcp_input.c
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
           const struct tcphdr *th)
{
  struct inet_connection_sock *icsk = inet_csk(sk);
  ...
  if (th->ack) {
    ...
    if (!th->syn)
      goto discard_and_undo;
    ...
    tcp_finish_connect(sk, skb);
    ...
    if (!sock_flag(sk, SOCK_DEAD)) {
      sk->sk_state_change(sk);
      ...
    }
    ...
    if (sk->sk_write_pending ||
        icsk->icsk_accept_queue.rskq_defer_accept ||
        icsk->icsk_ack.pingpong) {
      ...
    } else {
      tcp_send_ack(sk);
    }
    return -1;
  }
  ...
}

方法描述

1. 如果此ack消息里没有syn标志,则丢弃消息。

2. 调用tcp_finish_connect方法,将sk->sk_state设置为TCP_ESTABLISHED,表示tcp建立连接成功。

// net/ipv4/tcp_input.c
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
  ...
  tcp_set_state(sk, TCP_ESTABLISHED);
  ...
}

3. 调用sk->sk_state_change方法,通知sock状态发生变化。

在上一篇文章中我们讲到,如果socket没设置nonblock状态,则当connect时,会阻塞等待状态变化,而这里的sk->sk_state_change方法,正是用于告知等待线程sock状态变化了,可以从阻塞状态退出了。

4. 调用tcp_send_ack方法发送三次握手的最终的ack消息。

我们再来看下,服务端在收到这个ack消息后是如何处理的。

还是看tcp_v4_rcv方法

// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
  ...
  const struct tcphdr *th;
  ...
  struct sock *sk;
  ...
  th = (const struct tcphdr *)skb->data;
  ...
  sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
             th->dest, sdif, &refcounted);
  ...
  if (sk->sk_state == TCP_NEW_SYN_RECV) {
    struct request_sock *req = inet_reqsk(sk);
    struct sock *nsk;

    sk = req->rsk_listener;
    ...
    nsk = NULL;
    if (!tcp_filter(sk, skb)) {
      ...
      nsk = tcp_check_req(sk, skb, req, false);
    }
    ...
    if (nsk == sk) {
      ...
    } else if (tcp_child_process(sk, nsk, skb)) {
      ...
    } else {
      ...
      return 0;
    }
  }
  ...
}

方法描述

1. 调用__inet_lookup_skb方法,根据ip、端口等信息找到对应的sock。

2. 因为sk->sk_state为TCP_NEW_SYN_RECV,所以将sk转成struct request_sock类型,并赋值给req。

3. 将sk值设置为req->rsk_listener,即listen sock。

4. 调用tcp_check_req方法,创建一个新的struct sock实例newsk,把newsk->sk_state状态设置为TCP_SYN_RECV,之后把struct request_sock实例中的数据赋值到newsk中,再之后把newsk放到全局的hashtable中,最后把newsk放到listen sock的icsk_accept_queue队列中,这样后面的accept方法就能从这个队列获取这个sock了。

由于涉及到的代码太多,这里就不贴代码了,只说下相关方法

// net/ipv4/tcp_ipv4.c tcp_v4_syn_recv_sock // net/ipv4/inet_connection_sock.c inet_csk_complete_hashdance

5. 调用tcp_child_process将nsk->sk_state设置为TCP_ESTABLISHED,表示连接建立成功。

// net/ipv4/tcp_minisocks.c
int tcp_child_process(struct sock *parent, struct sock *child,
          struct sk_buff *skb)
{
  int ret = 0;
  int state = child->sk_state;
  ...
  if (!sock_owned_by_user(child)) {
    ret = tcp_rcv_state_process(child, skb);
    ...
  } else {
    ...
  }
  ...
  return ret;
}
EXPORT_SYMBOL(tcp_child_process);

该方法调用了tcp_rcv_state_process方法,此时参数child的sk_state为TCP_SYN_RECV。

// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
  ...
  switch (sk->sk_state) {
  case TCP_SYN_RECV:
    ...
    tcp_set_state(sk, TCP_ESTABLISHED);
    sk->sk_state_change(sk);
    ...
    break;
  ...
  }
  ...
  return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);

6. return 0 给上层,表示成功。

至此,tcp三次握手全部结束,tcp连接建立成功。

完。

原文发布于微信公众号 - Linux内核及JVM底层相关技术研究(ytcode)

原文发表时间:2019-03-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

编辑于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券