Linux内核TCP建立连接阶段服务器端socket状态变化以及在哈希表中的转移流程详解

TCP提供可靠的连接服务,通过三次握手来建立连接,建立连接阶段客户端和服务器端对于MSS,窗口大小等值进行协商,各种字段进行初始化,为可靠传输数据建立了基础。
服务器端建立连接流程:
1. 创建socket
2. 将socket与地址以及端口号进行绑定
3. 对此socket进行监听
4. accept阻塞
5. 获取到客户端发来的SYN请求,解析请求,若满足条件则构建SYN+ACK段发回客户端
6. 获取到客户端发来的ACK应答,若ACK段满足条件,则进行相应的初始化,提醒系统连接已建立,可以发送数据
本文主要介绍服务器端TCP建立连接阶段socket状态的变化过程,以及socket在哈希表中的转移流程。

建立连接过程中涉及到的socket状态

    TCP_CLOSED    /*初始状态*/
    TCP_ESTABLISHED    /*表示链接已经建立*/
    TCP_SYN_RECV    /*接收到了SYN报文,是三次握手的中间态*/
    TCP_LISTEN    /*表示服务器端的这个socket处于监听状态,可以接受连接*/

哈希表

Linux内核中为TCP建立连接维护的哈希表如下所示:

/*include/net/inet_hashtables.h*/
struct inet_hashinfo {
    /* This is for sockets with full identity only.  Sockets here will
     * always be without wildcards and will have the following invariant:
     *
     *          TCP_ESTABLISHED <= sk->sk_state < TCP_CLOSE
     *
     */
    struct inet_ehash_bucket    *ehash;  /*管理已建立连接的tcp_sock*/
    spinlock_t          *ehash_locks;
    unsigned int            ehash_mask;
    unsigned int            ehash_locks_mask;

    /* Ok, let's try this, I give up, we do need a local binding
     * TCP hash as well as the others for fast bind/connect.
     */
    struct inet_bind_hashbucket *bhash;  /*管理端口的哈希表*/

    unsigned int            bhash_size;  /*端口哈希表的大小*/
    /* 4 bytes hole on 64 bit */

    struct kmem_cache       *bind_bucket_cachep;  /*哈希表结构告诉缓存*/

    /* All the above members are written once at bootup and
     * never written again _or_ are predominantly read-access.
     *
     * Now align to a new cache line as all the following members
     * might be often dirty.
     */
    /* All sockets in TCP_LISTEN state will be in here.  This is the only
     * table where wildcard'd TCP sockets can exist.  Hash function here
     * is just local port number.
     */
    /*listen状态的tcp_sock*/
    struct inet_listen_hashbucket   listening_hash[INET_LHTABLE_SIZE]
                    ____cacheline_aligned_in_smp;
};

1. 创建socket

/*
domain: 通信协议族,对于TCP而言其值为AF_INET(IPv4协议)或AF_INET6(IPv6协议)
type: 通信语义,TCP对应的是SOCK_STREAM,UDP对应的是SOCK_DGREAM
protocol: 指定socket所使用的协议,IPPROTO_IP(值为0)或IPPROTO_TCP(值为6)代表TCP协议。
*/
int socket(int domain, int type, int protocol);

socket创建系统调用过程请移步socket系统调用 - (jianshu.com),成功创建socket会返回给用户态socket的文件描述符,后续可以用此文件描述符来代表socket进行后续操作,此时sock->state = TCP_CLOSE。

创建sock

2. 绑定socket,bind()

/*
sockfd: 要绑定地址的套接字文件描述符。
addr: 绑定套接字的地址,此结构体中有端口号。
addrlen: 存放地址信息的结构体大小,即sizeof(struct sockaddr)。
*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind系统调用是来给创建的socket和sockaddr进行绑定,sockaddr中包含本机接受的地址和端口号,bind系统调用和创建socket系统调用相似。IP地址的绑定过程比较简单,直接把地址赋值给内核的相关的数据结构,端口绑定过程涉及到哈希表,这里重点分析端口绑定。
ipv6为例,bind最终会调用inet6_bind函数来处理,函数中端口绑定的主要处理为sk->sk_prot->get_prot(sk, snum),v4和v6中注册get_port函数都是为inet_csk_get_port,用户态bind系统调用回传进来端口号,若是用户指定端口号,则根据此端口号在bhash表找是否有此端口号的sock,若是端口号没有被占用,则将此sock放入bhash表中;若是用户没有指定端口号(即将端口号设为0),则自动给sock分配一个端口号,之后将sock放入bhash表中。


bind

3. 监听socket,listen()

/*
sockfd: 套接字的文件描述符,即socket()系统调用返回的fd。
backlog:保存客户端请求的队列长度。
*/
int listen(int sockfd, int backlog);

listen系统调用是来对此socket进行监听并设置backlog,首先设置sock->sk_max_ack_backlog = backlog,指定socket能接受的客户端请求的最大队列长度,然后通过sk->sk_prot->get_prot(sk, snum)来查看此sock是否已经被绑定,若是被绑定则将sock从bhash表中取出来,设置sock->state = TCP_LISTEN,然后将sock放入lhash表中。


listen

4.accept阻塞

/*
sockfd: 套接字的文件描述符,即socket()系统调用返回的fd。
addr: 绑定套接字的地址,此结构体中有端口号。
addrlen: 存放地址信息的结构体大小,即sizeof(struct sockaddr)。
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);  

accept系统调用是用来查看是否有已经建立好连接的socket,如果没有则阻塞accept进程,一旦有已经建立连接的socket,即icsk->icsk_accept_queue != NULL,则被唤醒,通知用户态和内核态此socket可以进行数据传输。


accept

socket在listen成功之后就代表可以接受客户端发来的SYN请求来建立链接了,accept会阻塞来查看是否有已经建立连接完成的socket。

5. 接收客户端传来的SYN请求

服务器端收到skb包之后会检测包是否到本机,通过网络层检测之后会传输到传输层,此时会取出skb中的端口号以及net信息,通过端口号和net信息,从lhash表中取出sock,此sock是正在监听的sock,此时会新建一个建立连接请求块request_sock *req = inet_reqsk_alloc(),通过在skb中获取的客户端传来的信息和本机的相关信息,进行协商MSS,确定窗口大小等工作,然后对req的各字段进行填充,将req转换的sock的state(即req_to_sk(req)) = TCP_NEW_SYN_RECV,之后将req_to_sk(req)放入ehash表中,最终构造skb发出,关于skb的内容请移步Linux内核中sk_buff结构详解 - (jianshu.com)。

接收SYN请求

6. 接收ACK应答

发送出去SYN+ACK段之后客户端进行校验,满足条件后会返回ACK确认段,服务器端收到含ACK段的skb,通过网络层检测之后会传输到传输层,此时会取出skb中的源地址、目的地址、源端口、目的端口进行hash,然后根据hash值从ehash表中找到sock,将skb中的ack等字段和sock中的相应的字段进行对比,若都满足条件,则会创建一个子传输控制块sock *child,child是sock克隆得来的,修改child中的指定字段,将child的state设置为TCP_SYN_RECV,然后会将sock从ehash表中删除,将child加入到ehash表中,child则为后续传输数据用到的数据传输控制块。最终将req加到icsk->icsk_accept_queue中,设置child状态为TCP_ESTABLISHED,唤醒accept,通知用户态和内核态此socket已经完成建立连接,可以发送数据。


接收ACK应答

你可能感兴趣的:(Linux内核TCP建立连接阶段服务器端socket状态变化以及在哈希表中的转移流程详解)