Linux tcp/ip 源码分析 - connection termination

时间:2022-06-23
本文章向大家介绍Linux tcp/ip 源码分析 - connection termination,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前两篇文章中我们讲到,shutdown和close方法会发送fin消息给对方,开始tcp连接的关闭流程,现在我们从源码角度看下tcp连接关闭的具体过程,以及中间发送的消息和涉及到的各种状态。

假设客户端先调用了shutdown方法,发起了tcp连接关闭请求,由之前的文章我们可以知道,此时客户端会从TCP_ESTABLISHED状态切换到TCP_FIN_WAIT1状态,shutdown方法同时会发送fin消息给服务端。

下面看下服务端如何处理fin消息的。

由之前的文章可知,ip层在收到消息之后,会通过回调tcp_v4_rcv方法将消息转给tcp层。

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

该方法会先根据ip、端口等信息,找到对应的sk,再调用tcp_v4_do_rcv方法,继续处理fin消息。

// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
  ...
  if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
    ...
    tcp_rcv_established(sk, skb, tcp_hdr(skb));
    return 0;
  }
  ...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);

由于此时服务端的sk->sk_state处于TCP_ESTABLISHED状态,所以skb的处理流程会进入到tcp_rcv_established方法。

// net/ipv4/tcp_input.c
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
       const struct tcphdr *th)
{
  ...
  tcp_data_queue(sk, skb);
  ...
  tcp_ack_snd_check(sk);
  return;
  ...
}
EXPORT_SYMBOL(tcp_rcv_established);

该方法先调用tcp_data_queue处理fin消息,再调用tcp_ack_snd_check方法

// net/ipv4/tcp_input.c
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
  ...
  if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
    ...
    if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
      tcp_fin(sk);
    ...
    return;
  }
  ...
}

由上可见,fin消息的处理流程会进入到tcp_fin方法。

// net/ipv4/tcp_input.c
void tcp_fin(struct sock *sk)
{
  ...
  sk->sk_shutdown |= RCV_SHUTDOWN;
  sock_set_flag(sk, SOCK_DONE);

  switch (sk->sk_state) {
  ...
  case TCP_ESTABLISHED:
    /* Move to CLOSE_WAIT */
    tcp_set_state(sk, TCP_CLOSE_WAIT);
    ...
    break;
  ...
  }
  ...
}

方法描述

1. 标记sk->sk_shutdown字段包含RCV_SHUTDOWN。

由上两篇文章可以知道,shutdown和close方法在发送fin消息之前,会先标记sk->sk_shutdown字段包含SEND_SHUTDOWN,即发送fin消息后,客户端是SEND_SHUTDOWN,服务端是RCV_SHUTDOWN。

2. 设置sk的flag为SOCK_DONE。

3. 由于此时服务端的sk状态还是TCP_ESTABLISHED,所以该方法会调用tcp_set_state方法,将sk->sk_state状态设置为TCP_CLOSE_WAIT,即等待应用层关闭tcp连接。

tcp_fin方法处理完之后,上面的tcp_ack_snd_check方法会发送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 (!sock_owned_by_user(sk)) {
    ret = tcp_v4_do_rcv(sk, skb);
  } else if (tcp_add_backlog(sk, skb)) {
    ...
  }
  ...
}

该方法会先根据ip、端口等信息找到对应的sk,再调用tcp_v4_do_rcv方法,对这个sk继续进行ack消息的逻辑处理。

// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
  ...
  if (tcp_rcv_state_process(sk, skb)) {
    ...
  }
  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)
{
  ...
  switch (sk->sk_state) {
  ...
  case TCP_FIN_WAIT1: {
    ...
    tcp_set_state(sk, TCP_FIN_WAIT2);
    sk->sk_shutdown |= SEND_SHUTDOWN;
    ...
  }
  ...
  }
  ...
  return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);

由于客户端的sk此时处于TCP_FIN_WAIT1状态,当收到ack消息后,该方法会将sk的状态修改为TCP_FIN_WAIT2。

至此,由客户端到服务端的tcp流就已经被完全关闭。

当服务端应用层调用了shutdown或close方法后,根据前两篇文章我们可以知道,shutdown或close方法会先调用tcp_close_state方法,将服务端sk的状态由TCP_CLOSE_WAIT修改为TCP_LAST_ACK,然后再调用tcp_send_fin方法,发送fin消息给客户端。

继续看下客户端fin消息的处理流程,还是从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 (!sock_owned_by_user(sk)) {
    ret = tcp_v4_do_rcv(sk, skb);
  } else if (tcp_add_backlog(sk, skb)) {
    ...
  }
  ...
}

处理流程和之前差不多,最终还是调用tcp_v4_do_rcv方法。

// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
  ...
  if (tcp_rcv_state_process(sk, skb)) {
    ...
  }
  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)
{
  ...
  switch (sk->sk_state) {
  ...
  case TCP_FIN_WAIT2:
    ...
    /* Fall through */
  case TCP_ESTABLISHED:
    tcp_data_queue(sk, skb);
    ...
    break;
  }
  ...
  return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);

由于此时客户端的sk的状态为TCP_FIN_WAIT2,该方法最终会调用tcp_data_queue继续处理fin消息。

// net/ipv4/tcp_input.c
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
  struct tcp_sock *tp = tcp_sk(sk);
  ...
  if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
    ...
    if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
      tcp_fin(sk);
    ...
    return;
  }
  ...
}

该方法又调用了tcp_fin方法。

// net/ipv4/tcp_input.c
void tcp_fin(struct sock *sk)
{
  ...
  switch (sk->sk_state) {
  ...
  case TCP_FIN_WAIT2:
    /* Received a FIN -- send ACK and enter TIME_WAIT. */
    tcp_send_ack(sk);
    tcp_time_wait(sk, TCP_TIME_WAIT, 0);
    break;
  ...
  }
  ...
}

由于此时客户端的sk是TCP_FIN_WAIT2状态,所以会执行如上代码

1. 调用tcp_send_ack方法,发送ack给服务端。

2. 调用tcp_time_wait方法,先将sk状态设置为TCP_TIME_WAIT,再开启TIME_WAIT定时,超时后销毁这个sk。

当sk处于TCP_TIME_WAIT状态时,会一直占用对应的ip和端口,防止其他连接再次使用,从而出现错误。

由上可见,谁先发起的tcp连接关闭请求,谁最终就会进入到TIME_WAIT状态,在写服务器端代码时,这个是要注意的。

现在剩下最后一步,即服务端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 (!sock_owned_by_user(sk)) {
    ret = tcp_v4_do_rcv(sk, skb);
  } else if (tcp_add_backlog(sk, skb)) {
    ...
  }
  ...
}

和之前一样,该方法最终还是会调用tcp_v4_do_rcv方法。

// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
  ...
  if (tcp_rcv_state_process(sk, skb)) {
    ...
  }
  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 tcp_sock *tp = tcp_sk(sk);
  ...
  switch (sk->sk_state) {
  ...
  case TCP_LAST_ACK:
    if (tp->snd_una == tp->write_seq) {
      ...
      tcp_done(sk);
      goto discard;
    }
    break;
  }
  ...
  return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);

由于此时服务器的sk状态为TCP_LAST_ACK,所以该方法最终会调用tcp_done方法。

// net/ipv4/tcp.c
void tcp_done(struct sock *sk)
{
  ...
  tcp_set_state(sk, TCP_CLOSE);
  ...
  sk->sk_shutdown = SHUTDOWN_MASK;
  ...
}
EXPORT_SYMBOL_GPL(tcp_done);

该方法会先将sk状态设置为TCP_CLOSE,之后再设置sk->sk_shutdown字段的值为SHUTDOWN_MASK,即SEND_SHUTDONW & RCV_SHUTDOWN。

至此,服务端的sk就已经完全关闭。

客户端的sk等TIME_WAIT状态的定时超时之后,也会自动关闭。

这样,tcp连接的关闭流程就完整了。