第12章 网络通信:连接的建立

在互联网时代,网络通信编程已经是一个程序员必不可少的技能之一。几乎所有的产品都会涉及网络操作或访问。在Linux编程环境中,系统提供了socket套接字为程序员提供统一的网络编程接口。本书将对socket套接字进行详细的分析,由于篇幅较多,所以将内容分为三章来讲述。本章主要讲解与连接相关的分析,包括socketbindconnectlistenaccept系统调用及相关的源码追踪。这里假设读者有一定的Linux网络编程基础,所以对于系统调用的解释都是点到为止,只针对不常见或容易忽视的问题进行详细说明。

12.1 socket文件描述符

socket翻译成中文是插座、插槽的意思,而在网络编程中,其被翻译为“套接字”。Linux环境下,我们经常说“一切皆文件”。因此套接字也被视为一种文件描述符。首先,来看看如何使用socket系统调用创建一个套接字,代码如下:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
 
int socket(int domain, int type, int protocol);

其中的参数解释如下:

  • domain:用于指示协议族名字,如AF_INET为IPv4。
  • type:用于指示类型,如基于流通信的SOCK_STREAM
  • protocol:用于指示对于这种socket的具体协议类型。一般情况下,使用前两个参数限定后,只会存在一种协议类型对应该情况。这时,可以将protocol设置为0。但是在某些情况下,会存在多个协议类型,这时就必须指定具体的协议类型。

成功创建socket后,会返回一个文件描述符。失败时,该接口返回-1。

那么对于Linux内核来说,如何知道一个文件描述符是一个套接字,还是一个普通文件呢?其实这个问题也可以扩展到,内核如何知道一个文件描述符的具体类型,如何调用实际类型的操作函数呢?这仍然是VFS的魔力。

在第1章中,我们了解了文件描述符fd与内核文件结构struct file之间的关系,后者是内核用于管理文件的真正结构,其中的成员变量file->f_op为VFS支持的所有文件操作。VFS层无须关心该文件file的实际类型,它会直接调用file->f_op中的操作函数(这样的处理,与面向对象语言中的多态是类似的)。

对于套接字来说,只要在创建套接字时,将file->f_op设置为正确的套接字操作函数即可。该操作是在socket->sock_map_fd->sock_alloc_file中完成的,代码如下:

static int sock_alloc_file(struct socket *sock, struct file **f, int flags)
{
    ......
    ......
    /*    申请一个struct file,并将socket_file_ops作为参数来传递.
        在alloc_file中,会将socket_file_ops赋给file->f_op.
    */
    file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
                      &socket_file_ops);
    ......
    ......
    /* 让sock->file指向file,完成sock和file的关联 */
    sock->file = file;
    file->f_flags = O_RDWR | (flags & O_NONBLOCK);
    file->f_pos = 0;
    file->private_data = sock;
    *f = file;
    return fd;
}

尽管Linux内核是使用C语言编写的,但是其应用了很多面向对象的设计思想。以这里的file为例,内核利用f_op(对象操作函数指针集合)指向具体对象的操作函数集合。这样一来,对于VFS来说,就只须关心struct file,而无须关心具体的对象类型了,它会在处理过程中,调用正确的处理函数。

12.2 绑定IP地址

在成功创建套接字后,该套接字仅仅是一个文件描述符,并没有任何地址与之关联。使用该socket发送数据包时,由于该socket没有任何IP地址,内核会根据策略自动选择一个地址。但是,在某些情况下,我们需要手工指定socket使用哪个IP地址进行发送。这时,就需要使用bind系统调用了。

12.2.1 bind的使用

bind系统调用的接口定义如下:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
 
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

其中的参数解释如下:

  • sockfd:表示要绑定地址的套接字描述符。
  • addr:表示绑定到套接字的地址。
  • addrlen:表示绑定的地址长度。

返回值0表示成功,-1则表示错误。

因为Linux的套接字是针对多种协议族的,而每个协议族都可以有不同的地址类型。所以Linux套接字关于地址的系统调用,统一使用了一个公共结构体,并要求调用者将实际地址参数进行强制类型转换,以此来避免编译警告。

struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
}

因为每个协议族的地址类型各不相同,所以需要通过参数addrlen来告诉内核这个地址的实际大小。

说明

struct sockaddr数据类型会在socket涉及地址的所有接口中出现。这是因为套接字接口要支持所有的协议族,所以涉及地址的地方都使用了一个统一的地址结构struct sockaddr

下面是一个简单示例:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
#define LOOPBACK_ADDR 0x7F000001
#define LISTEN_PORT

在上面的示例中,我们创建了一个TCP套接字,并将回环地址127.0.0.1和端口1234绑定到这个套接字上。运行这个程序,然后通过netstat检查监听端口:

[fgao@ubuntu ~]#netstat -ant
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address  Foreign Address        State
tcp        0      0    127.0.0.1:1234      0.0.0.0:*          LISTEN

从上面的输出可以看到,创建的套接字已经成功地绑定了指定的地址和端口。

12.2.2 bind的源码分析

bind源码入口位于net/socket.c中,如下所示:

SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;
 
    /* 由文件描述符获取struct socket.前文已经介绍了fd与struct socket的关联关系. */
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        /* umyaddr是用户空间地址,这里将其复制到内核空间address变量中 */
        err = move_addr_to_kernel(umyaddr, addrlen, (struct sockaddr *)&address);
        if (err >= 0) {
            /* 对bind动作进行安全性检查 */
            err = security_socket_bind(sock,
                                       (struct sockaddr *)&address,
                                       addrlen);
            if (!err) {
                /* 调用对应协议的bind动作 */
                err = sock->ops->bind(sock,
                                      (struct sockaddr *)
                                      &address, addrlen);
            }
        }
        fput_light(sock->

在bind的调用中,根据不同的协议调用不同的实现函数(Linux的内核代码中,大量使用了这种面向对象的设计思路)。对于AF_INET协议族来说,无论是面向连接的SOCK_STREAM类型,还是SOCK_DGRAM协议类型,其实现函数均是inet_bind。下面来看一下inet_bind的具体实现:

int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
    struct sock *sk = sock->sk;
    struct in
    /*    
        如果具体协议实现了bind函数,则调用协议的bind函数.
        AF_INET协议族中,只有IPPROTO_ICMP和
        IPPROTO_IP实现了自己的bind函数,
        IPPROTO_TCP和IPPROTO_UDP都使用AF_INET通用的函数,即
        这个inet_bind.
    */
    if (sk->sk_prot->bind) {
        err = sk->sk_prot->bind(sk, uaddr, addr_len);
        goto out;
    }
    err = -EINVAL;
    /* 检查地址长度 */
    if (addr_len < sizeof(struct sockaddr_in))
        goto out;
    if (addr->sin_family != AF_INET) {
        /* 本来要求地址的协议族要与sock相同,必须为AF_INET,但是这里有个兼容性问题.允许协
             议族为AF_UNSPEC并且地址为INADDR_ANY的任意地址 */
        err = -EAFNOSUPPORT;
        if (addr->sin_family != AF_UNSPEC ||
            addr->sin_addr.s_addr != htonl(INADDR_ANY))
            goto out;
    }
    /* 判断地址类型 */
    chk_addr_ret = inet_addr_type(sock_net(sk), addr->sin_addr.s_addr);
    err = -EADDRNOTAVAIL;
    /*
        sysctl_ip_nonlocal_bind系统控制开关,允许bind非本地IP;inet->freebind为一个
        socket选项,允许该socket bind任意IP;在上面这些变量均不成立时,指定地址又不是任意的
        本地地址INADDR_ANY,地址类型又不是本地地址类型,多播或广播时,则bind失败.
    */
    if (!sysctl_ip_nonlocal_bind &&
        !(inet->freebind || inet->transparent) &&
        addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
        chk_addr_ret != RTN_LOCAL &&
    
    PROT_SOCK(1024),则需要检查用户是否有权限创建知名端口
    */
    if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))
        goto out;
 
    lock_sock(sk);
    err = -EINVAL;
    /* 确保套接字不会被bind两次 */
    if (sk->sk_state != TCP_CLOSE || inet->inet_num)
        goto out_release_sock;
    /* 使用参数设置套接字的接收和发送地址 */
    inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
    /* 如果参数地址是多播或广播类型,则重置发送源地址为0,表示在发送时,使用的是设备地址 */
    if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
        inet->inet_saddr = 0;  /* Use device */
    /*    
        调用协议自定义的操作函数get_port,判断该端口是否可以使用.
        虽然这里是一个查询的动作,但是却会有修改的动作.
        当该端口可以使用时,会让inet_sk(sk)->inet_num = snum;
        这样做,是因为查询动作已经获得了锁.在确定可以使用该端口时,直接修
        改inet_num,这样既可以保证设置端口的原子性,同时还可以提高性能
    */
    if (sk->sk_prot->get_port(sk, snum)) {
        inet->inet_saddr = inet->inet_rcv_saddr = 0;
        err = -EADDRINUSE;
        goto out_release_sock;
    }
    /* 如果设置了bind地址,则置上相应的标志 */
    if (inet->inet_rcv_saddr)
        sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
    /* 如果设置了源端口,则设置相应的标志 */
    if (snum)
        sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
    /* 设置inet_sport,其为网络序 */
    inet->inet_sport = htons(inet->inet_num);
    /* 重置目的地址和端口 */
    inet->inet_daddr = 0;
    inet->inet_dport = 0;
    /* 重置该套接字的路由信息 */
    sk_dst_reset(sk);
    err = 0;
 
out_release_sock:
    release_sock(sk);
out:
    return err;
}

说明

无论是APUE还是man手册,在讲解bind的时候都有点问题,或有偏差,或不够详尽。从上面的源码我们知道,通过使用系统控制开关sysctl_ip_nonlocal_bind或套接字选项可以让套接字bind一个非本机地址。但APUE却说套接字只能绑定本机的有效地址——当然这也是由于APUE距现在的时间太久了,而man手册都没有提及非本机地址的事情。

12.3 客户端连接过程

12.3.1 connect的使用

connect的原型为:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
 
int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);

其中的参数解释如下:

  • int sockfd:套接字描述符。
  • const struct sockaddr *addr:要连接的地址。
  • socklen_t addrlen:要连接的地址长度。

返回值0表示成功,-1表示失败。

connect的用途是使用指定的套接字去连接对于TCP套接字来说,connect实际上是要真正地进行三次握手,所以其默认是一个阻塞操作。那么是否可以写一个非阻塞的TCP connect代码呢?这是一个合格的网络开发工程师的基本功,具体的实现可以参看UNPv1的实现。更重要是要理解其原理,这样才能在需要的时候,信手拈来。

12.4 服务器端监听与连接

12.3.2 connect的源码分析

connect的源码入口位于socket.c,代码如下:

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,       int, addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;
 
    /* 通过文件描述符获取 socket 结构 */
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
       goto out;
    /* 将用户空间地址复制到内核空间变量 address 中 */
    err = move_addr_to_kernel(uservaddr, addrlen, (struct sockaddr *)&address);
    if (err < 0)
       goto out_put;
    /* 安全性检查 */
    err = security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
    if (err)
       goto out_put;
    /* 与 bind 类似,调用与协议族对应的 connect 操作函数 */
    err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
                sock->file->f_flags);
out_put:
   fput_light(sock->file, fput_needed);
out:
   return err;
}

对于 AF_INET 协议族来说,面向连接的协议类型是 SOCK_STREAM,其连接函数为 inet_stream_connect,而非面向连接的协议类型 SOCK_DGRAM,其连接函数为 inet_dgram_connect。这很合理,因为从 connect 的功能实现上看,两者的实现效果完全不同。

让我们先从简单的 inet_dgram_connect 入手。

int inet_dgram_connect(struct socket *sock, struct sockaddr * uaddr,
            int addr_len, int flags)
{
    struct sock *sk = sock->sk;
    /* 长度合法性检查 */
    if (addr_len < sizeof(uaddr->sa_family))
       return -EINVAL;
    /* 如果协议族为 AF_UNSPEC,则先执行 disconnect */
    if (uaddr->sa_family == AF_UNSPEC)
       return sk->sk_prot->disconnect(sk, flags);
    /* 如果该套接字没有指定源端口,并且系统自动绑定端口失败,则返回错误 */
    if (!inet_sk(sk)->inet_num && inet_autobind(sk))
       return -EAGAIN;
    /* 调用具体协议的 connect 实现函数 */
    return sk->sk_prot->connect(sk, (struct sockaddr *)uaddr, addr_len);
}

udp_prot 是 UDP 协议中所有自定义操作函数的集合。其 connect 的实现函数为 ip4_datagram_connect

int ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    struct inet_sock *inet = inet_sk(sk);
    struct sockaddr_in *usin = (struct sockaddr_in *) uaddr;
    struct flowi4 *fl4;
    struct rtable *rt;
    int oif;
    __be32 saddr;
    int err;
 
    if (addr_len < sizeof(*usin))
       return -EINVAL;
    /* 检查是否为 AF_INET 协议族 */
    if (usin->sin_family != AF_INET)
       return -EAFNOSUPPORT;
    /* 因为 connect 会改变目的地址,所有 socket 中保存的路由缓存已经无用,必须重置. */
    sk_dst_reset(sk);
    lock_sock(sk);
    /* 得到套接字绑定的发送接口 */
    oif = sk->sk_bound_dev_if;
    saddr = inet->inet_saddr;
    /* 在目的地址是多播地址的情况下,
       如果该套接字没有绑定网卡,则出口网卡为设置的多播网卡索引;
       如果该套接字没有绑定源 IP,则使用设置的多播源地址; */
    if (ipv4_is_multicast(usin->sin_addr.s_addr)) {
        if (!oif)
           oif = inet->mc_index;
        if (!saddr)
           saddr = inet->mc_addr;
    }
    /* 执行路由查找 */
    fl4 = &inet->cork.fl.u.ip4;
    rt = ip_route_connect(fl4, usin->sin_addr.s_addr, saddr,
                RT_CONN_FLAGS(sk), oif,
                sk->sk_protocol,
                inet->inet_sport, usin->sin_port, sk);
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        if (err == -ENETUNREACH)
            IP_INC_STATS_BH(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
        goto out;
    }
    /* 如果是广播地址且未设置广播标志,则返回错误 */
    if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST)) {
       ip_rt_put(rt);
       err = -EACCES;
       goto out;
    }
    /* 如果套接字没有设置发送地址或接收地址,则使用路由结果 */
    if (!inet->inet_saddr)
       inet->inet_saddr = fl4->saddr;  /* Update source address */
    if (!inet->inet_rcv_saddr) {
       inet->inet_rcv_saddr = fl4->saddr;
       if (sk->sk_prot->rehash)
           sk->sk_prot->rehash(sk);
    }
    inet->inet_daddr = fl4->daddr;
    inet->inet_dport = usin->sin_port;
    sk->sk_state = TCP_ESTABLISHED;
    inet->inet_id = jiffies;
    /* 重新设置路由信息 */
    sk_dst_set(sk, &rt->dst);
    err = 0;
out:
    release_sock(sk);
    return err;
}

由于功能比较简单,所以 UDP 的 connect 实现源码也一目了然,可以看到,只是设置了目的 IP、端口和路由信息。下面对比一下 TCP 的 connect 实现,其实现比 UDP 要复杂得多,代码如下:

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
          int addr_len, int flags)
{
    /* 从 socket 结构获得 sock 结构,后者是内核真正用于管理网络层的套接字结构 */
    struct sock *sk = sock->sk;
    int err;
    long timeo;
 
    /* 地址长度检查 */
    if (addr_len < sizeof(uaddr->sa_family))
       return -EINVAL;
 
    lock_sock(sk);
    /* 对 AF_UNSPEC 兼容性处理 */
    if (uaddr->sa_family == AF_UNSPEC) {
       err = sk->sk_prot->disconnect(sk, flags);
       sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
       goto out;
    }
 
    /* STREAM 协议是有连接状态的,所以需要对套接字进行状态检查 */
    switch (sock->state) {
    default:
        err = -EINVAL;
        goto out;
    /* 若连接已经建立,则返回错误 */
    case SS_CONNECTED:
        err = -EISCONN;
        goto out;
    /* 若连接正在进行中,则返回错误 */
    case SS_CONNECTING:
        err = -EALREADY;
        /* Fall out of switch with err, set for this state */
        break;
    /* 当前为未连接状态 */
    case SS_UNCONNECTED:
       err = -EISCONN;
       /* sock 的状态是未连接,但是套接字的 sk_state 却不是关闭状态,
          此时无法进行连接 */
       if (sk->sk_state != TCP_CLOSE)
          goto out;
        /* 既然需要产生连接,那么每种具体的协议肯定都有自己的实现.
           所以这会调用具体协议的实现函数. */
        err = sk->sk_prot->connect(sk, uaddr, addr_len);
        if (err < 0)
           goto out;
        /* 将 sock 状态更改为正在连接中 */
        sock->state = SS_CONNECTING;
        /* Just entered SS_CONNECTING state; the only
         * difference is that return value in non-blocking
         * case is EINPROGRESS,表示正在连接中.
           当 connect 为非阻塞时,就会返回这个错误
         */
        err = -EINPROGRESS;
        break;
    }
 
    /* 检查是否需要连接超时.若设置了非阻塞标志,则 timeo 为假.
       若设置了阻塞标志,则 timeo 为真. */
    timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
    /* 当前连接状态为正在连接的状态(即刚发送了 syn 或收到 syn) */
    if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
       /* 如果没有超时标志或连接超时或失败,则返回 */
       if (!timeo || !inet_wait_for_connect(sk, timeo))
          goto out;
       /* 判断是否由于信号导致等待连接退出 */
       err = sock_intr_errno(timeo);
       /* 如果有未处理的信号,则返回失败,表示信号中断了连接 */
       if (signal_pending(current))
          goto out;
    }
    /* 连接被关闭了.原因可能是对端 RST,超时等 */
    if (sk->sk_state == TCP_CLOSE)
       goto sock_error;
    /* 至此,连接成功 */
    sock->state = SS_CONNECTED;
    err = 0;
out:
    release_sock(sk);
    return err;
sock_error:
   err = sock_error(sk) ? : -ECONNABORTED;
   sock->state = SS_UNCONNECTED;
   if (sk->sk_prot->disconnect)
        sk->sk_prot->disconnect(sk, 0);
   goto out;
}

接下来,就需要进入 TCP 协议自定义的 connect 函数 tcp_v4_connect 了,代码如下:

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
    struct inet_sock *inet = inet_sk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    struct ip_options_rcu *inet_opt;
    struct flowi4 *fl4;
    struct rtable *rt;
    int err;
    __be32 saddr = 0, nexthop, daddr;
    __be16 orig_sport, orig_dport;
 
    /* 地址长度检查 */
    if (addr_len < sizeof(struct sockaddr_in))
       return -EINVAL;
    /* 协议族类型检查 */
    if (usin->sin_family != AF_INET)
       return -EAFNOSUPPORT;
    /* 设置下一跳和目的地址 */
    nexthop = daddr = usin->sin_addr.s_addr;
    /* 获得 IP 选项 */
    inet_opt = rcu_dereference_protected(inet->inet_opt,
                         sock_owned_by_user(sk));
    /* 如果有严格源路由的 IP 选项 */
    if (inet_opt && inet_opt->opt.srr) {
       /* 若地址为 0,则返回错误 */
       if (!daddr)
          return -EINVAL;
       /* 因为严格源路由的 IP 选项,所以下一跳要设置为选项中的第一跳地址 */
       nexthop = inet_opt->opt.faddr;
    }
    /* 设置源端口和目的端口 */
    orig_sport = inet->inet_sport;
    orig_dport = usin->sin_port;
    fl4 = &inet->cork.fl.u.ip4;
    /* 查找路由 */
    rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
                RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
                IPPROTO_TCP,
                orig_sport, orig_dport, sk);
    if (IS_ERR(rt)) {
       err = PTR_ERR(rt);
       if (err == -ENETUNREACH)
          IP_INC_STATS_BH(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
       return err;
    }
    /* 如果路由结果是多播或广播地址,则返回错误 */
    if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
       ip_rt_put(rt);
       return -ENETUNREACH;
    }
    /* 如果没有 IP 选项或没有设置严格路由,那么目的地址即为路由结果的目的地址 */
    if (!inet_opt || !inet_opt->opt.srr)
       daddr = fl4->daddr;
    /* 如果没有设置源地址,则使用路由结果的源地址 */
    if (!inet->inet_saddr)
       inet->inet_saddr = fl4->saddr;
    /* 套接字的接收地址即为源地址 */
    inet->inet_rcv_saddr = inet->inet_saddr;
    /* 若保存的 TCP 选项有时间戳,并且目的地址与要连接的地址不同,
       则需要重置时间戳及相关变量 */
    if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {
       /* Reset inherited state */
       tp->rx_opt.ts_recent       = 0;
       tp->rx_opt.ts_recent_stamp = 0;
       tp->write_seq              = 0;
    }
    /* 如果系统启用了 TIME_WAIT 回收机制,并且当前没有时间戳信息,
       则尝试从对端 peer 中获得时间戳信息 */
    if (tcp_death_row.sysctl_tw_recycle &&
       !tp->rx_opt.ts_recent_stamp && fl4->daddr == daddr) {
       struct inet_peer *peer = rt_get_peer(rt, fl4->daddr);
       /* 如果找到对端 peer */
       if (peer) {
           inet_peer_refcheck(peer);
           /* 如果对端保存的时间戳信息还没有过期 */
            if ((u32)get_seconds() - peer->tcp_ts_stamp <= TCP_PAWS_MSL) {
                /* 利用对端保存的时间戳信息初始化当前套接字的时间戳选项 */
                tp->rx_opt.ts_recent_stamp = peer->tcp_ts_stamp;
                tp->rx_opt.ts_recent = peer->tcp_ts;
           }
       }
    }
    /* 设置目的端口和地址 */
    inet->inet_dport = usin->sin_port;
    inet->inet_daddr = daddr;
    /* 设置 IP 头的选项长度 */
    inet_csk(sk)->icsk_ext_hdr_len = 0;
    if (inet_opt)
        inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;
    /* 初始化 MSS */
    tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;
    /* 设置 TCP 的状态为 SYN_SENT,即发送了 syn 包 */
    tcp_set_state(sk, TCP_SYN_SENT);
    /* 将套接字加入到 hash 表中,并分配源端口 */
    err = inet_hash_connect(&tcp_death_row, sk);
    if (err)
        goto failure;
    /* 检查源端口或目的端口是否发生了变化,如果发生了变化则重新查找路由 */
    rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
                inet->inet_sport, inet->inet_dport, sk);
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        rt = NULL;
        goto failure;
    }
    /* 设置 GSO 功能 */
    sk->sk_gso_type = SKB_GSO_TCPV4;
    sk_setup_caps(sk, &rt->dst);
    /* 如果没有设置初始的序列号,则根据双方地址,随机生成端口 */
    if (!tp->write_seq)
       tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,
                                                  inet->inet_daddr,
                                                  inet->inet_sport,
                                                  usin->sin_port);
    /* 设置 inet_id,该 ID 用于生成 IP 报文的 ID 值 */
    inet->inet_id = tp->write_seq ^ jiffies;
    /* 一切准备工作完毕,tcp_connect 生成 SYN 报文并发送 */
    err = tcp_connect(sk);
    rt = NULL;
    if (err)
       goto failure;
    return 0;
 
failure:
   /* This unhashes the socket and releases the local port, if necessary */
   tcp_set_state(sk, TCP_CLOSE);
   ip_rt_put(rt);
   sk->sk_route_caps = 0;
   inet->inet_dport = 0;
   return err;
}

下面来分析 tcp_connect,看看内核是如何至此,TCP的连接过程已经分析完毕,其中涉及的某些过程会在后面进行具体分析。

12.4 服务器端连接过程

12.4.1 listen的使用

服务器端用 listen 来监听端口,其原型为:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
 
int listen(int sockfd, int backlog);

其中的参数解释如下:

  • 参数 int sockfd:成功创建的 TCP 套接字。
  • 参数 int backlog:定义 TCP 未处理连接的队列长度。该队列虽然已经完成了三次握手,但服务器端还没有执行 accept 的连接。APUE 中说,backlog 只是一个提示,具体的数值实际上是由系统来决定的。后面会通过学习内核源码来确定这一点。

函数的返回值为 0,表示成功;-1 表示失败。

12.4.2 listen的源码分析

listen的源码入口位于socket.c,代码如下:

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    struct socket *sock;
    int err, fput_needed;
    int somaxconn;
    /* 从文件描述符得到socket结构 */
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
       /* 得到系统设置的最大未处理连接队列长度 */
       somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
       /* 如果用户指定的参数backlog大于系统最大值,则使用系统最大值 */
       if ((unsigned)backlog > somaxconn)
           backlog = somaxconn;
        /* 进行安全性检查 */
        err = security_socket_listen(sock, backlog);
        /* 通过检查后,就调用指定协议族的listen实现函数 */
        if (!err)
           err = sock->ops->listen(sock, backlog);
        fput_light(sock->file, fput_needed);
    }
    return err;
}

AF_INET协议族的listen实现函数为inet_listen,代码如下:

int inet_listen(struct socket *sock, int backlog)
{
    struct sock *sk = sock->sk;
    unsigned char old_state;
    int err;
    lock_sock(sk);
    err = -EINVAL;
    /* 如果套接字状态不是未连接 */
    if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
       goto out;
    /* 得到之前的TCP连接状态 */
    old_state = sk->sk_state;
    /* 如果之前的状态不是关闭或监听,则返回错误 */
    if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
       goto out;
    /*
        经过前面的状态过滤,这里只可能是关闭或监听状态.
        如果当前已经是监听状态了,那么我们只须改变backlog的值;
        如果是关闭状态,则需要真正地启动监听操作.
    */
    if (old_state != TCP_LISTEN) {
        err = inet_csk_listen_start(sk, backlog);
        if (err)
            goto out;
    }
    /* 更新backlog的值 */
    sk->sk_max_ack_backlog = backlog;
    err = 0;
out:
    release_sock(sk);
    return err;
}

接下来进入inet_csk_listen_start,代码如下:

int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
    struct inet_sock *inet = inet_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);
    /* 为连接请求队列分配空间 */
    int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);
    if (rc != 0)
       return rc;
    /* 初始化工作 */
    sk->sk_max_ack_backlog = 0;
    sk->sk_ack_backlog = 0;
    inet_csk_delack_init(sk);
    /*
        虽然这里是先将连接的状态设为了监听状态,看似有一个竞争时间窗口.但实际上只有在get_port
        成功以后,该套接字才被加入到哈希表中—从系统的角度看,套接字加入到哈希表中后,才会真正
        处于监听状态,可以接受连接请求了.因此实际上并没有竞争发生
    */
    sk->sk_state = TCP_LISTEN;
    /* 使用get_port进行端口绑定 */
    if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
       /* 设置源端口 */
       inet->inet_sport = htons(inet->inet_num);
       /* 清除路由缓存 */
       sk_dst_reset(sk);
       /* 将套接字加入到哈希表中,这时才可以接受新连接 */
       sk->sk_prot->hash(sk);
       return 0;
    }
    /* 绑定端口失败,则设置连接未关闭状态 */
    sk->sk_state = TCP_CLOSE;
    /* 释放连接请求队列空间 */
    __reqsk_queue_destroy(&icsk->icsk_accept_queue);
    return -EADDRINUSE;
}

现在服务器端已经处于监听状态,可以接收客户端的连接请求了。同时,通过源码跟踪,也可以发现在第二个参数不超过系统限制的最大值的情况下,内核已直接使用其值作为已连接队列的长度了。

12.4.3 accept的使用

accept用于从指定套接字的连接队列中取出第一个连接,并返回一个新的套接字用于与客户端进行通信,示例代码如下:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr,
socklen_t *addrlen, int flags);

其中的参数解释如下:

  • int sockfd:处于监听状态的套接字。
  • struct sockaddr *addr:用于保存对端的地址信息。
  • socklen_t *addrlen:是一个输入输出值。调用者将其初始化为addr缓存的大小,accept返回时,会将其设置为addr的大小。
  • int flags:是新引入的系统调用accept4的标志位;目前支持SOCK_NONBLOCKSOCK_CLOEXEC

关于返回值,若执行成功,则返回一个非负的文件描述符;若失败则返回-1。

NOTE

若不关心对端地址信息,则可以将addraddrlen设置为NULL

12.4.4 accept的源码分析

accept的源码入口位于文件socket.c,代码如下:

SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
        int __user *, upeer_addrlen)
{
    return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}

进入sys_accept4,代码如下:

SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
        int __user *, upeer_addrlen, int, flags)
{
    struct socket *sock, *newsock;
    struct file *newfile;
    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
       return -EINVAL;
    /* 保证设置的非阻塞标志SOCK_NONBLOCK与O_NONBLOCK相同 */
    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
       flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
    /* 通过文件描述符获得socket结构 */
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
       goto out;
    err = -ENFILE;
    /* 申请一个新的socket结构 */
    newsock = sock_alloc();
    if (!newsock)
       goto out_put;
    /* 新的socket的类型和操作函数与监听的socket一致 */
    newsock->type = sock->type;
    newsock->ops = sock->ops;
    /* 
        这里必须增加该套接字模块的引用计数.这是因为这个套接字模块可能不是Linux内核内置的,
        为了保证在套接字的使用过程中,该模块不会被意外卸载,所以,在创建套接字时,需要增加相应
        的模块计数
    */
    __module_get(newsock->ops->owner);
    /* 为新的socket类型,申请一个新的文件描述符 */
    newfd = sock_alloc_file(newsock, &newfile, flags);
    if (unlikely(newfd < 0)) {
       err = newfd;
       sock_release(newsock);
       goto out_put;
    }
    /* 对accept操作进行安全性检查 */
    err = security_socket_accept(sock, newsock);
    if (err)
       goto out_fd;
    /* 执行协议族的accept操作函数 */
    err = sock->ops->accept(sock, newsock, sock->file->f_flags);
    if (err < 0)
       goto out_fd;
    /* 用户想获得对端地址 */
    if (upeer_sockaddr) {
       /* 获得对端地址 */
       if (newsock->ops->getname(newsock, (struct sockaddr *)&address,
                         &len, 2) < 0) {
          err = -ECONNABORTED;
          goto out_fd;
        }  
        err = move_addr_to_user((struct sockaddr *)&address,
               len, upeer_sockaddr, upeer_addrlen);
        if (err < 0)
           goto out_fd;
    }
    /* 将文件描述符newfd和文件管理结构newfile安装到文件表中 */
    fd_install(newfd, newfile);
    /* 此时,已保证accept成功执行,将newfd赋给err,并在后面返回err */
    err = newfd;
out_put:
    fput_light(sock->file, fput_needed);
out:
    return err;
out_fd:
    fput(newfile);
    put_unused_fd(newfd);
    goto out_put;
}

对于AF_INET协议族,accept的实现函数为inet_accept,代码如下:

int inet_accept(struct socket *sock, struct socket *newsock, int flags)
{
    struct sock *sk1 = sock->sk;
    int err = -EINVAL;
    /* 调用具体协议的accept操作,并得到新的sock结构 */
    struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);
    if (!sk2)
       goto do_err;
    /* 锁住新的sk2 */
    lock_sock(sk2);
    /* 记录RFS信息 */
    sock_rps_record_flow(sk2);
    WARN_ON(!((1 << sk2->sk_state) &
       (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_CLOSE)));
    /* 将新的sock与调用者传递的socket关联起来 */
    sock_graft(sk2, newsock);
    /* 设置socket为连接状态 */
    newsock->state = SS_CONNECTED;
    err = 0;
    /* 释放sk2的控制权 */
    release_sock(sk2);
do_err:
    return err;
}

对于TCP协议来说,其accept实现函数如下:

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct sock *newsk;
    int error;
    /* 获得sk的控制权 */
    lock_sock(sk);
    error = -EINVAL;
    /* sk,即TCP连接,若不是监听状态则报错 */
    if (sk->sk_state != TCP_LISTEN)
       goto out_err;
    if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {
       /* 已连接队列为空 */
       /* 得到sk的超时时间 */
       long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
       /* If this is a non blocking socket don't sleep */
       error = -EAGAIN;
       /* 如果超时为0,即非阻塞,则报错退出 */
       if (!timeo)
          goto out_err;
       /* 以timeo为超时时间,等待一个新的连接 */
       error = inet_csk_wait_for_connect(sk, timeo);
       if (error)
          goto out_err;
    }
    /* 得到新的sock */
    newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);
    WARN_ON(newsk->sk_state == TCP_SYN_RECV);
out:
    /* 释放sk的控制权,并返回新建连接newsk */
    release_sock(sk);
    return newsk;
out_err:
    newsk = NULL;
    *err = error;
    goto out;
}

12.5 TCP三次握手的实现分析

前面两节分别从客户端和服务器端的系统调用的角度,来分析和学习TCP的连接过程。本节将从TCP三次握手的数据包交互过程,来研究TCP连接的建立。如果不熟悉TCP握手的三个数据包,则请自行阅读相关材料。

三次握手的过程如图12-1所示。

![图12-1 TCP三次握手的过程](image placeholder - 实际上是一张TCP三次握手示意图)

12.5.1 SYN包的发送

SYN包是指客户端主动建立一个TCP新连接的第一个包,其TCP标志为SYN,表示同步TCP的序列号。SYN包的发送是在tcp_connect函数中完成的,下面对SYN包的构建做进一步分析。在tcp_connect函数中,通过调用tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN)来完成SYN包的构建,示例代码如下:

static void tcp_init_nondata_skb(struct sk_buff *skb, u32 seq, u8 flags)
{
    /* 设置为CHECKSUM_PARTIAL,表示需要计算TCP校验和 */
    skb->ip_summed = CHECKSUM_PARTIAL;
    /* 初始化校验和信息 */
    skb->csum = 0;
    /* TCP_SKB_CB是一个宏,用于将skb->cb转换为TCP的控制块 */
    /* 设置TCP首部标志位 */
    TCP_SKB_CB(skb)->tcp_flags = flags;
    /* 重置控制块的SACK标志位 */
    TCP_SKB_CB(skb)->sacked = 0;
    /* 初始化skb的GSO */
    skb_shinfo(skb)->gso_segs = 1;
    skb_shinfo(skb)->gso_size = 0;
    skb_shinfo(skb)->gso_type = 0;
    /* 设置TCP的序列号 */
    TCP_SKB_CB(skb)->seq = seq;
    /* 如果是SYN或FIN包,则增加TCP序列号 */
    if (flags & (TCPHDR_SYN | TCPHDR_FIN))
        seq++;
    /* 设置结束序列号 */
    TCP_SKB_CB(skb)->end_seq = seq;
}

从源码中可以发现,这个函数只是设置TCP控制块的序列号和标志,并没有真正构建TCP数据包。那么,让我们回过头来看tcp_connect,但是在tcp_init_nondata_skbtcp_transmit_skb之间再没有任何与构建数据包相关的代码了。那么也只剩下一个可能,即在tcp_transmit_skb中实现数据包的构建,这样也合乎道理。tcp_transmit_skb作为TCP发送函数的入口,统一实现了TCP数据包的构建。

下面是其中用于构造TCP数据包的相关代码:

/* 为TCP报文头部保存空间 */
skb_push(skb, tcp_header_size);
/* 重置TCP报文头指针 */
skb_reset_transport_header(skb);
/* 设置skb的所有者为sk,同时增加sk的写缓存的使用统计 */
skb_set_owner_w(skb, sk);
/* 得到TCP报文头的内存指针,开始构建TCP的报文头部 */
/* 这里设置了源端口、目的端口、序列号、确认序列号、包长及标志位 */
th = tcp_hdr(skb);
th->source        = inet->inet_sport;
th->dest          = inet->inet_dport;
th->seq           = htonl(tcb->seq);
th->ack_seq       = htonl(tp->rcv_nxt);
*((__be32 *)th) = htonl((tcb->tcp_flags & 0x0F) << 12);  /* 设置标志位 */
/* SYN包,则TCP窗口不会被扩展 */
if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) {
   /* RFC1323: The window in SYN & SYN/ACK segments
    * is never scaled.
    */
   th->window    = htons(min(tp->rcv_wnd, 65535U));
}
/* TCP的校验和与紧急指针为0 */
th->check        = 0;
th->urg_ptr      = 0;
/* 检查是否需要设置紧急指针 */
if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {
   if (before(tp->snd_up, tcb->seq + 0x10000)) {
       th->urg_ptr       th->urg_ptr = htons(tp->snd_up - tcb->seq);
       th->urg = 1;
    } else {
       th->urg = 0;
    }
/* TCP选项部分 */
tcp_options_write((__be32 *)(th + 1), tp, &opts);
/* 计算TCP的校验和 */
icsk->icsk_af_ops->send_check(sk, skb);
/* 完成了TCP数据包的构建,将数据包交给IP层 */
err = icsk->icsk_af_ops->queue_xmit(skb, &inet->cork.fl);

从上面的代码中,我们可以进一步领会Linux内核协议栈的数据包传输机制——每一层都专注于自己的工作。对于TCP传输层来说,只须负责在skb中构建自己的首部,然后将skb数据包传递给IP层做进一步的处理即可。

12.5.2 接收SYN包,发送SYN+ACK包

为了跟踪SYN包的接收流程,首先进入内核并接收发给本机数据包的入口 ip_local_deliver_finish,代码如下:

static int ip_local_deliver_finish(struct sk_buff *skb)
{
    /* 得到设备的网络空间 */
    struct net *net = dev_net(skb->dev);
    /* 取走网络层报文头部 */
    __skb_pull(skb, ip_hdrlen(skb));
    /* 重置传输层报文头部 */
    skb_reset_transport_header(skb);
    rcu_read_lock();
    {
        /* 得到传输层协议 */
        int protocol = ip_hdr(skb)->protocol;
        int hash, raw;
        const struct net_protocol *ipprot;
    resubmit:
        /* 将数据包传递给对应的原始套接字 */
        raw = raw_local_deliver(skb, protocol);
        /* 得到协议表的桶索引 */
        hash = protocol & (MAX_INET_PROTOS - 1);
        /* 得到注册协议 */
        ipprot = rcu_dereference(inet_protos[hash]);
        if (ipprot != NULL) {
            int ret;
            if (!net_eq(net, &init_net) && !ipprot->netns_ok) {
                /* xfrm策略检查 */
                if (!ipprot->no_policy) {
                    if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                        kfree_skb(skb);
                        goto out;
                    }
                }
                ret = ipprot->handler(skb);
                if (ret < 0) {
                    protocol = -ret;
                    goto resubmit;
                }
                IP_INC_STATS_BH(net, ...);
            }
            if (!raw) {
                if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
                    /* 没有任何已注册的网络协议可以处理这个数据包,因此回复 ICMP proto unreachable */
                    IP_INC_STATS_BH(net, IPSTATS_MIB_INUNKNOWNPROTOS);
                    icmp_send(skb, ICMP_DEST_UNREACH, ICMP...);
                }
            }
        }
    }
}

TCP协议在系统初始化时,会将对应的处理函数注册到 inet_protos 上,接下来进入TCP的处理函数 tcp_v4_rcv 中,代码如下:

int tcp_v4_rcv(struct sk_buff *skb)
{
    const struct iphdr *iph;
    const struct tcphdr *th;
    struct sock *sk;
    int ret;
    struct net *net = dev_net(skb->dev);
    /* 如果数据包类型不是发给本机的,drop掉 */
    if (skb->pkt_type != PACKET_HOST)
        goto discard_it;
    /* Count it even if it's bad */
    TCP_INC_STATS_BH(net, TCP_MIB_INSEGS);
    /* 数据包至少还有一个TCP报文头部长度 */
    if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
        goto discard_it;
    /* 得到TCP报文头部 */
    th = tcp_hdr(skb);
    /* 检查TCP的数据偏移,至少要比头部大 */
    if (th->doff < sizeof(struct tcphdr) / 4)
        goto bad_packet;
    /* 检查数据段长度 */
    if (!pskb_may_pull(skb, th->doff * 4))
        goto discard_it;
    /* 计算校验和 */
    if (!skb_csum_unnecessary(skb) && tcp_v4_checksum_init(skb))
        goto bad_packet;
    /* 重新得到TCP头部.因为前面的代码可能会重新申请skb. */
    th = tcp_hdr(skb);
    /* 得到IP头部 */
    iph = ip_hdr(skb);
    /* 设置TCP控制块的序列号,结束序列号,确认序列号等 */
    TCP_SKB_CB(skb)->seq = ntohl(th->seq);
    TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin + skb->len - th->doff * 4);
    TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
 
    /* 查找对应的sock结构.
       这里先对已经连接的sock进行查找,然后对监听的sock进行查找 */
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
    if (!sk)
        goto no_tcp_socket;
 
process:
    /* 如果sock处于TIME_WAIT状态,则跳转到do_time_wait */
    if (sk->sk_state == TCP_TIME_WAIT)
        goto do_time_wait;
    /* 如果数据包的TTL小于设置的TTL阀值,则丢弃 */
    if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) {
        NET_INC_STATS_BH(net, LINUX_MIB_TCPMINTTLDROP);
        goto discard_and_relse;
    }
    /* xfrm策略失败 */
    if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
        goto discard_and_relse;
    /* 虽然函数的名字为reset重置,但实际上是释放了netfilter的相关资源 */
    nf_reset(skb);
    /* 执行socket过滤器 */
    if (sk_filter(sk, skb))
        goto discard_and_relse;
    /* 重置数据包的网卡信息 */
    skb->dev = NULL;
    /* 锁住sock,获得控制权 */
    bh_lock_sock_nested(sk);
    ret = 0;
    if (!sock_owned_by_user(sk)) {
        /* 如果用户进程没有再使用这个sock */
        /* 由DMA来做数据包拷贝,实现TCP receive offload */
#ifdef CONFIG_NET_DMA
        struct tcp_sock *tp = tcp_sk(sk);
        if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)
            tp->ucopy.dma_chan = dma_find_channel(...);
#endif
        /* 将数据包放入prequeue中 */
        if (!tcp_prequeue(sk, skb)) {
            /* 如果放到prequeue中失败,则只能即时处理该数据包 */
            ret = tcp_v4_do_rcv(sk, skb);
        }
    } else if (unlikely(sk_add_backlog(sk, skb))) {
        /* else if的时候,意味着用户进程正在使用这个套接字.
           那么就把数据包保存到backlog中. */
        bh_unlock_sock(sk);
        NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
        goto discard_and_relse;
    }
    bh_unlock_sock(sk);
    sock_put(sk);
    return ret;
 
no_tcp_socket:
    /* 若没有找到对应的TCP套接字,并且xfrm策略检测失败,则丢弃数据包 */
    if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
        goto discard_it;
    /* 检查是否数据包长度出错,或者是校验和出错 */
    if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
bad_packet:
        TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
    } else {
        /* 若数据包未出错,但也没有匹配的套接字,则发送TCP RESET */
        tcp_v4_send_reset(NULL, skb);
    }
 
discard_it:
    /* Discard frame. */
    kfree_skb(skb);
    return 0;
 
discard_and_relse:
    sock_put(sk);
    goto discard_it;
 
do_time_wait:
    /* TIME_WAIT状态的数据包处理,在此省略了这些代码分析 */
    ...
}

根据上面的分析可知,对于SYN包的处理,还需要进入函数 tcp_v4_do_rcv,代码如下:

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    struct sock *rsk;
#ifdef CONFIG_TCP_MD5SIG
    /* 进行TCP的MD5检查,是TCP的一个安全检查 */
    if (tcp_v4_inbound_md5_hash(sk, skb))
        goto discard;
#endif
    if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
        /* 处理已连接的数据包流程 */
        /* 如果数据包与套接字的rxhash不同,则重置套接字的rxhash */
        sock_rps_save_rxhash(sk, skb);
        /* 进入已连接TCP的处理函数 */
        if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
            rsk = sk;
            goto reset;
        }
        return 0;
    }
    /* 运行到这里,则表明该TCP为非连接状态 */
    /* 检查数据包长度和TCP校验值 */
    if (skb->len < tcp_hdrlen(skb) || tcp_checksum_complete(skb))
        goto csum_err;
 
    if (sk->sk_state == TCP_LISTEN) {
        /* 连接处于监听状态,这是我们要跟踪的流程 */
        /*
         * 处理连接请求,即处理SYN包.作为第一个SYN包请求,服务端只有
         * 一个监听sock,所以这里返回的nsk实际上就是sk.
         * 这里就不跟踪tcp_v4_hnd_req了.
         */
        struct sock *nsk = tcp_v4_hnd_req(sk, skb);
        if (!nsk)
            goto discard;
        if (nsk != sk) {
            /* 为SYN请求生成了新的sock结构,自然需要重新做RFS hash */
            sock_rps_save_rxhash(nsk, skb);
            /* 处理子sock */
            if (tcp_child_process(sk, nsk, skb)) {
                rsk = nsk;
                goto reset;
            }
            return 0;
        }
    }
    /* 根据状态处理数据包 */
    if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
        rsk = sk;
        goto reset;
    }
    /* 省略其余的不相干的代码 */
    ...
}

进入 tcp_rcv_state_process,我们截取部分相关的代码:

switch (sk->sk_state) {
    /* sock处于监听状态 */
    case TCP_LISTEN:
        /* 监听状态不应收到ack包 */
        if (th->ack)
            return 1;
        /* 丢弃RST数据包 */
        if (th->rst)
            goto discard;
        /* 收到SYN请求包 */
        if (th->syn) {
            /* 若设置了FIN结束标志,则丢弃包 */
            if (th->fin)
                goto discard;
            /* 调用对应的处理连接请求的回调函数 */
            if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
                return 1;
            /* SYN包处理完毕,释放数据包 */
            kfree_skb(skb);
            return 0;
        }
}

对于IPv4的TCP来说,处理连接请求的函数是 tcp_v4_conn_request,代码如下:

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_extend_values tmp_ext;
    struct tcp_options_received tmp_opt;
    const u8 *hash_location;
    struct tcp_sock *tp = tcp_sk(sk);
    struct request_sock *req;
    struct inet_request_sock *ireq;
    struct dst_entry *dst = NULL;
    __be32 saddr = ip_hdr(skb)->saddr;
    __be32 daddr = ip_hdr(skb)->daddr;
    __u32 isn = TCP_SKB_CB(skb)->when;
    bool want_cookie = false;
 
    /* 处理广播或多播包,直接丢弃 */
    if (skb_rtable(skb)->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST))
        goto drop;
 
    /* TW buckets are converted to open requests without limitations, they consume only memory */
    if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
        /* 判断是否使用syn cookie,如不使用则丢弃该包 */
        want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
        if (!want_cookie)
            goto drop;
    }
 
    /* backlog队列已满,并且队列中已有足够多的最近未处理的连接请求.则丢弃该包 */
    if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
        goto drop;
 
    /* 申请一个请求sock */
    req = inet_reqsk_alloc(&tcp_request_sock_ops);
    if (!req)
        goto drop;
 
#ifdef CONFIG_TCP_MD5SIG
    tcp_rsk(req)->af_specific = &tcp_request_sock_ipv4_ops;
#endif
 
    /* 解析TCP选项 */
    tcp_clear_options(&tmp_opt);
    tmp_opt.mss_clamp = TCP_MSS_DEFAULT;
    tmp_opt.user_mss  = tp->rx_opt.user_mss;
    tcp_parse_options(skb, &tmp_opt, &hash_location, 0);
 
    /* 处理syn cookie */
    if (tmp_opt.cookie_plus > 0 &&
        tmp_opt.saw_tstamp &&
        !tp->rx_opt.cookie_out_never &&
        (sysctl_tcp_cookie_size > 0 ||
         (tp->cookie_va ...))) {
        /* 具体情况待补充 */
    }
 
    /* 如果需要syn cookie,但没有时间戳选项,则清除TCP选项 */
    if (want_cookie && !tmp_opt.saw_tstamp)
        tcp_clear_options(&tmp_opt);
 
    /* 初始化request sock */
    tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;
    tcp_openreq_init(req, &tmp_opt, skb);
    ireq = inet_rsk(req);
    ireq->loc_addr = daddr;
    ireq->rmt_addr = saddr;
 
    /* 如果不是syn cookie,或者有时间戳选项时,如果请求表示支持ECN,则服务端也设置ECN标志 */
    if (!want_cookie || tmp_opt.tstamp_ok)
        TCP_ECN_create_request(req, tcp_hdr(skb));
 
    if (want_cookie) {
        /* 若需要做syn cookie,则产生一个cookie序列号 */
        isn = cookie_v4_init_sequence(sk, skb, &req->mss);
        req->cookie_ts = tmp_opt.tstamp_ok;
    } else if (!isn) {
        struct inet_peer *peer = NULL;
        /*
         * 检查处于TimeWait状态的socket是否可以重用:
         *  1)该socket支持时间```c
    } else if (!isn) {
        struct inet_peer *peer = NULL;
        /*
         * TimeWait状态的socket是否可以重用:
         * 1)该socket支持时间戳选项。
         * 2)打开了TimeWait状态socket重用开关。
         * 3)通过查找路由,获得对端peer信息。
         * 4)当前时间与对端的上个时间戳间隔小于TCP_PAWS_MSL(60)秒,并且新请求时间小于对端
         *    上个时间戳TCP_PAWS_MSL(60)秒以上。
         * 当同时满足上面几个条件时,则认为该请求为非法请求
         */
        if (tmp_opt.saw_tstamp &&
            tcp_death_row.sysctl_tw_recycle &&
            (dst = inet_csk_route_req(sk, &fl4, req)) != NULL &&
            fl4.daddr == ...)  /* 此处条件不完整,原文如此 */
        /*
         * 在没有syn cookie的情况下,内核对syn flood做的简单防护:
         * 1)连接队列已经使用了四分之三以上。
         * 2)没有对端信息或对端没有时间戳。
         * 3)没有路由信息或没有路由的RTT时间。
         * 当同时满足以上条件时,表明队列已接近满队列,同时这个新连接可能还无法正常通信,那么
         * 就会放弃这个请求
         */
        else if (!sysctl_tcp_syncookies &&
                 (sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
                  (sysctl_max_syn_backlog >> 2))) {
            /* 原文此处缺少闭合括号和跳转 */
            goto drop;
        }
        isn = tcp_v4_init_sequence(skb);
    }
 
    /* 保存初始序列号和synack发送时间 */
    tcp_rsk(req)->snt_isn = isn;
    tcp_rsk(req)->snt_synack = tcp_time_stamp;
    /* 回复SYN+ACK数据包 */
    if (tcp_v4_send_synack(sk, dst, req,
                (struct request_values *)&tmp_ext) ||
        want_cookie)
        goto drop_and_free;
    /* 将request sock加入到哈希表中 */
    inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
    return 0;
 
drop_and_release:
    dst_release(dst);
drop_and_free:
    reqsk_free(req);
drop:
    return 0;
}

这就是服务端收到SYN包,并回复SYN+ACK的过程.

12.5.3 接收SYN+ACK数据包

客户端接收SYN+ACK数据包的流程与服务器端类似,都要经过 ip_local_deliver_finishtcp_v4_rcvtcp_v4_do_rcvtcp_rcv_state_process,这里会根据TCP的不同连接状态,进行不同的处理.对于此时的客户端来说,其连接状态为 TCP_SYN_SENT(即发送了SYN包),其代码如下:

case TCP_SYN_SENT:
    /* 处理SYN+ACK,完成三次握手 */
    queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
    if (queued >= 0)
        return queued;
    /* Do step6 onward by hand. */
    /* 处理urgent数据 */
    tcp_urg(sk, skb, th);
    __kfree_skb(skb);
    tcp_data_snd_check(sk);

然后进入 tcp_rcv_synsent_state_process,代码如下:

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
                  const struct tcphdr *th, unsigned int len)
{
    const u8 *hash_location;
    struct tcp_options_received tmp;
 
    /* 解析TCP选项 */
    tcp_parse_options(skb, &tp->rx_opt, &hash_location, 0);
 
    /* 设置了ACK标志 */
    if (th->ack) {
        /* rfc793:
         * "If the state is SYN-SENT then
         *    first check the ACK bit
         *      If the ACK bit is set
         *      If SEG.ACK号非法,即不等于我们下次要发的序列号,则重置连接
         */
        if (TCP_SKB_CB(skb)->ack_seq != tp->snd_nxt)
            goto reset_and_undo;
 
        /* 判断TCP时间戳是否合法 */
        if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
            !between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp,
                tcp_time_stamp)) {
            NET_INC_STATS(sock_net(sk), LINUX_MIB_PAWSABANDONED);
            goto reset_and_undo;
        }
 
        /* ACK标志已经通过了检查。这时,如果设置了Reset位,则重置连接 */
        if (th->rst) {
            tcp_reset(sk);
            goto discard;
        }
 
        /* 如果没有设置SYN标志,则丢弃该包 */
        if (!th->syn)
            goto discard_and_undo;
 
        /* 如果TCP套接字设置了ECN标志,但是数据包没有设置ECE标志
         * (表示对端不支持TCP ECN显示拥塞通告),则清除掉本端的ECN标志)
         */
        TCP_ECN_rcv_synack(tp, th);
 
        /* 处理ack数据包,设置TCP发送窗口 */
        tp->snd_wl1 = TCP_SKB_CB(skb)->seq;
        tcp_ack(sk, skb, FLAG_SLOWPATH);
 
        /* Ok.. it's good. Set up sequence numbers and
         * move to established.
         */
 
        /* TCP窗口大小,是不考虑scale选项的 */
        tp->snd_wnd = ntohs(th->window);
        tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
 
        /* 如果没有windows scale选项 */
        if (!tp->rx_opt.wscale_ok) {
            /* 将接收端和接收端的窗口扩展选项设置为0 */
            tp->rx_opt.snd_wscale = tp->rx_opt.rcv_wscale = 0;
            /* 设置窗口的最大值 */
            tp->window_clamp = min(tp->window_clamp, 65535U);
        }
 
        /* 判断是否时间戳选项 */
        if (tp->rx_opt.saw_tstamp) {
            /* 设置时间戳选项 */
            tp->rx_opt.tstamp_ok       = 1;
            tp->tcp_header_len =
                sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
            tp->advmss        -= TCPOLEN_TSTAMP_ALIGNED;
        }
 
        /* 初始化 */
        tcp_mtup_init(sk);
        tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
        tcp_initialize_rcv_mss(sk);
 
        /* Remember, tcp_poll() does not lock socket! */
        tp->copied_seq = tp->rcv_nxt;
 
        if (cvp != NULL &&
            cvp->cookie_pair_size > 0 &&
            tp->rx_opt.cookie_plus > 0) {
            int cookie_size = ... /* 省略细节 */
        }
 
        tcp_set_state(sk, TCP_ESTABLISHED);
        security_inet_conn_established(sk, skb);
 
        /* Make sure socket is routed, for correct metrics. */
        /* 查找路由 */
        icsk->icsk_af_ops->rebuild_header(sk);
 
        /* 下面对路由的metric、TCP的阻塞控制和缓存等进行初始化 */
        tcp_init_metrics(sk);
        tcp_init_congestion_control(sk);
 
        /* Prevent spurious tcp_cwnd_restart() on first data packet. */
        tp->lsndtime = tcp_time_stamp;
 
        /* 若设置了keepalive,则初始化keepalive定时器 */
        if (sock_flag(sk, SOCK_KEEPOPEN))
            inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));
 
        /* 若发送方没有设置窗口扩展选项,则设置TCP快速路径预测标志 */
        if (!tp->rx_opt.snd_wscale)
            __tcp_fast_path_on(tp, tp->snd_wnd);
        else
            tp->pred_flags = 0;
 
        /* 如果sock的状态不是死亡状态 */
        if (!sock_flag(sk, SOCK_DEAD)) {
            /* 改变sock状态,唤醒等待进程 */
            sk->sk_state_change(sk);
            /* 若有异步等待队列,则给该进程发送异步事件 */
            sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
        }
 
        /*
         * 如果该套接字:
         * 1)有写操作等待。
         * 2)设置了延迟accept。
         * 3)没有设置快速ack。
         */
        if (sk->sk_write_pending ||
           icsk->icsk_accept_queue.rskq_defer_accept ||
           icsk->icsk_ack.pingpong) {
           /* 满足上面条件之一,则延时确认 */
           /* Save one ACK. Data will be ready after
            * several ticks, if write_pending is set.
            *
            * It may be deleted, but with this feature
            * we avoid doing two opens when doing a connect.
            */
           tcp_send_ack(sk);
        }
        return -1;
    }
 
    /* 到此,表示没有ACK标志 */
    if (th->rst) {
       /* 如果没有ACK只有RST,则丢弃该包 */
       goto discard_and_undo;
    }
 
    /* 时间戳检测 */
    if (tp->rx_opt.ts_recent_stamp && tp->rx_opt.saw_tstamp &&
        tcp_paws_reject(&tp->rx_opt, 0))
        goto discard_and_undo;
 
    if (th->syn) {
        /*
         * 若只有SYN标志,没有ACK,则可能是同时发出了多个SYN连接请求,
         * 甚至有可能是自己连接自己
         */
        /* 设置连接状态为收到SYN包 */
        tcp_set_state(sk, TCP_SYN_RECV);
 
        /*
         * 后面的代码,与之前收到SYN包的流程基本一致,在此就不做分析了
         */
        if (tp->rx_opt.saw_tstamp) {
            tp->rx_opt.tstamp_ok = 1;
            tcp_store_ts_recent(tp);
            tp->tcp_header_len =
                sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
        }
        /* ... 省略后续相同处理 */
    }
 
    /* 没有SYN也没有RST标志,则丢掉数据包后返回 */
discard_and_undo:
    /* 丢弃数据包 */
    tcp_clear_options(&tp->rx_opt);
    tp->rx_opt.mss_clamp = saved_clamp;
    goto discard;
 
reset_and_undo:
    /* 重置连接。与丢弃数据包的区别在于返回值。非0时,调用者会重置连接 */
    tcp_clear_options(&tp->rx_opt);
    tp->rx_opt.mss_clamp = saved_clamp;
    return 1;
}

关键流程梳理

  • 客户端在 TCP_SYN_SENT 状态下收到 SYN+ACK 包后,进入 tcp_rcv_synsent_state_process 处理.
  • 首先检查 ACK 的合法性(序列号匹配、时间戳有效),然后处理 RST、SYN 标志.
  • 若一切正常,会设置发送窗口、时间戳选项,更新路由与拥塞控制,最后将状态迁移至 TCP_ESTABLISHED.
  • 若只收到 SYN 而无 ACK,则会进入 TCP_SYN_RECV 状态(类似同时打开的情况).
  • 若收到 RST 或不合法的包,则重置或丢弃连接.

至此,客户端完成了三次握手的最后一步,连接正式建立.

12.5.4 接收ACK数据包,完成三次握手

在前文中,我们已经知道了发往本机的TCP数据包会进入tcp_v4_do_rcv.但因为此时还未真正地完成三次握手,所以TCP仍然是未连接状态,自然就会再次进入函数tcp_v4_hnd_req了.

static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
    struct tcphdr *th = tcp_hdr(skb);
    const struct iphdr *iph = ip_hdr(skb);
    struct sock *nsk;
 
    /*
     * SYN请求时,已经将对应的request_sock加入到了队列中,
     * 因此这次收到ACK答复时,是可以找到对应的request_sock的。
     * 另外需要注意的是,这个函数还会有一个输出值prev,
     * 其为返回值req前面的元素。之所以返回这个prev值,
     * 是为了在后面的tcp_check_req函数中,移除req时,
     * 不需要进行第二次查找。
     */
    struct request_sock *req = inet_csk_search_req(sk, &prev, th->source,
                                            iph->saddr, iph->daddr);
    if (req)
        return ...
    ……
}

下面进入tcp_check_req函数:

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
               struct request_sock *req,
               struct request_sock **prev)
{
    struct tcp_options_received tmp_opt;
    /*
     * saw_tstamp,因为时间戳选项依赖于每个数据包
     */
    tmp_opt.saw_tstamp = 0;
 
    if (th->doff > (sizeof(struct tcphdr)>>2)) {
        /* 若实际数据位置偏移量大于TCP固定报头长度,
           则表明该报文一定包含了TCP选项 */
        /* 解析TCP选项 */
        tcp_parse_options(skb, &tmp_opt, &hash_location, 0);
        /* 判断是否有时间戳选项 */
        if (tmp_opt.saw_tstamp) {
            /* 检查时间戳选项 */
            tmp_opt.ts_recent = req->ts_recent;
            /* We do not store true stamp, but it is not required,
             * it can be estimated (approximately)
             */
        }
    }
 
    /*
     * 若为重复的SYN包,则回复SYN+ACK
     */
    if (flg & TCP_FLAG_SYN) {
        req->rsk_ops->rtx_syn_ack(sk, req, NULL);
        return NULL;
    }
 
    /* 非法的ACK值 */
    if ((flg & TCP_FLAG_ACK) &&
        (TCP_SKB_CB(skb)->ack_seq !=
         tcp_rsk(req)->snt_isn + 1 + tcp_s_data_size(tcp_sk(sk))))
        return sk;
 
    /* 时间戳检查失败,或者序列号不在窗口内 */
    if (paws_reject ||
        !tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
            tcp_rsk(req)->rcv_isn + 1,
            tcp_rsk(req)->rcv_isn + 1 + req->rcv_wnd)) {
        /* 若需发送ACK确认 */
        if (!(flg & TCP_FLAG_RST))
            req->rsk_ops->send_ack(sk, skb, req);
        /* 若时间戳检测失败,则增加相应的计数 */
        if (paws_reject)
            NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSESTABREJECTED);
        return NULL;
    }
 
    /* In sequence, PAWS is OK. */
    /* 若数据包为有序数据 */
    if (tmp_opt.saw_tstamp &&
        !after(TCP_SKB_CB(skb)->seq, tcp_rsk(req)->rcv_isn + 1))
        req->ts_recent = tmp_opt.rcv_tsval;
 
    /* 若序列号在接收窗口之外,则去掉SYN标志 */
    if (TCP_SKB_CB(skb)->seq == tcp_rsk(req)->rcv_isn) {
        /* Truncate SYN, it is out of window starting
           at tcp_rsk(req)->rcv_isn + 1. */
        flg &= ~TCP_FLAG_SYN;
    }
 
    /* 检查SYN和RST标志,若都设置了,则将当前半连接从队列中清除 */
    if (flg & (TCP_FLAG_RST|TCP_FLAG_SYN)) {
        TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_ATTEMPTFAILS);
        goto embryonic_reset;
    }
 
    /* 若没有设置ACK,则丢弃该包 */
    if (!(flg & TCP_FLAG_ACK))
        return NULL;
 
    /* While TCP_DEFER_ACCEPT is active, drop bare ACK. */
    /* 如果设置了延迟接收,则丢弃单独的ACK包 */
    if (req->retrans < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&
        TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
        inet_rsk(req)->acked = 1;
        return NULL;
    }
 
    /* 记录收到SYN+ACK的时间 */
    if (tmp_opt.saw_tstamp && tmp_opt.rcv_tsecr)
        tcp_rsk(req)->snt_synack = tmp_opt.rcv_tsecr;
    else if (req->retrans) /* don't take RTT sample if retrans && ~TS */
        /* 不进行RTT采样 */
 
    /* TCP的三次握手已经完成。使用syn_recv_sock创建真正的套接字 */
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
    if (child == NULL)
        goto listen_overflow;
 
    /* 新的套接字已经创建,因此原来的request sock可以从队列中删除 */
    inet_csk_reqsk_queue_unlink(sk, req, prev);
    inet_csk_reqsk_queue_removed(sk, req);
    /* 将套接字加入到已连接的队列中 */
    inet_csk_reqsk_queue_add(sk, req, child);
    return child;
 
listen_overflow:
    if (!sysctl_tcp_abort_on_overflow) {
        inet_rsk(req)->acked = 1;
        return NULL;
    }
    /* 否则,连接被重置(略) */
}

这样,当tcp_check_req成功返回时,会返回一个新创建的sock结构.那么在tcp_v4_do_rcv中,就会进入tcp_child_process中.

int tcp_child_process(struct sock *parent, struct sock *child,
              struct sk_buff *skb)
{
    int ret = 0;
    int state = child->sk_state;
 
    /* 检查sock是否正在被用户进程使用 */
    if (!sock_owned_by_user(child)) {
        /* 用户进程没有占用sock的情况 */
        ret = tcp_rcv_state_process(child, skb, tcp_hdr(skb),
                     skb->len);
        /* Wakeup parent, send SIGIO */
        /* 唤醒阻塞在父sock的任务 */
        if (state == TCP_SYN_RECV && child->sk_state != state)
            parent->sk_data_ready(parent, 0);
    } else {
        /* 由于用户进程占用着sock,将数据包加入backlog,以后再处理 */
        __sk_add_backlog(child, skb);
    }
    bh_unlock_sock(child);
    sock_put(child);
    return ret;
}

这里我们考虑数据包被立刻处理的情况,即用户进程没有占用sock结构,那么这里数据还是会进入tcp_rcv_state_process的.根据前面的分析,tcp_rcv_state_process是根据套接字的状态来处理数据包的.而child是从父sock生成的,所以如果child的状态和父sock的状态一致,肯定是有问题的——因为父sock的状态是监听状态.那么child的状态是何时改变的呢?

让我们退回到创建child的函数tcp_v4_syn_recv_sock->tcp_create_openreq_child->inet_csk_clone中,代码如下:

struct sock *inet_csk_clone(struct sock *sk, const struct request_sock *req,
                const gfp_t priority)
{
    /* 克隆一个新的sock结构 */
    struct sock *newsk = sk_clone(sk, priority);
 
    if (newsk != NULL) {
        /* 开始克隆面向连接的sock信息 */
        struct inet_connection_sock *newicsk = inet_csk(newsk);
        /* 将新sock设置为TCP_SYN_RECV状态 */
        newsk->sk_state = TCP_SYN_RECV;
        newicsk->icsk_bind_hash = NULL;
        /* 后面是复制其他变量的代码,在此省略掉 */
    }
    return newsk;
}

这个函数的命名稍稍有些别扭,名字叫做clone(克隆),也就是说,所有的内容都应该保持一致.而这里在这个inet_csk_clone后,新sock的状态与父sock的状态并不一致.

下面来查看tcp_rcv_state_process处理TCP_SYN_RECV状态的代码:

case TCP_SYN_RECV:
    /*
     * acceptable是tcp_rcv_state_process在前面对ACK数据包进行的判断。
     */
    if (acceptable) {
        /* 这时已经完成了三次握手 */
 
        /* 初始化用户态未读数据的序列号是我们期待接收的下一个序列号 */
        tp->copied_seq = tp->rcv_nxt;
        smp_mb();
 
        /* 设置连接状态为已连接 */
        tcp_set_state(sk, TCP_ESTABLISHED);
 
        /*
         * sk_state_change为一个回调函数,默认为sock_def_wakeup,
         * 其会唤醒sleep在该socket的进程
         */
        sk->sk_state_change(sk);
 
        /*
         * 若该sock有对应的用户态socket,则执行异步I/O通知
         * 这里需要注意的是,对于我们目前的情况来说。
         * 子sock是从监听sock clone而来的,其中sk_sleep和sk_socket都是NULL。
         * 那么三次握手以后,阻塞在监听socket的进程是如何被唤醒的呢?
         * tcp_child_process在调用tcp_rcv_state_process后,会检查sock状态是否发生了变
         * 化。如果发生了变化,则会调用parent->sk_data_ready(parent, 0);
         * 这样,就可以将事件通知到阻塞在监听sock的进程了。
         */
        if (sk->sk_socket)
            sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
 
        /* 初始化未确认回复的序列号 */
        tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
        /* 初始化发送窗口 */
        tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale;
        tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
 
        /* 如果有时间戳选项,则MSS需要减去时间戳所占的大小 */
        if (tp->rx_opt.tstamp_ok)
            tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
 
        /* Make sure socket is routed, for correct metrics. */
        icsk->icsk_af_ops->rebuild_header(sk);
 
        tcp_init_metrics(sk);
        tcp_init_congestion_control(sk);
 
        /* Prevent spurious tcp_cwnd_restart() on first data packet. */
        tp->lsnd = tcp_jiffies32;
        /* ... 其他初始化 */
    }

目前为止,三次握手的源码分析已经结束了。其内部还有很多细节值得展开学习,但那就不是一两个章节所能完成的任务了。笔者只是抛砖引玉,给出一个脉络,关于剩下的细节大家可以自己通过阅读代码来完善。


Image Context:

  • [Image 4389 on Page 599]
  • [Image 3829 on Page 612]
  • [Image 4494 on Page 616]