Linux TCP/IP协议栈之Socket的实现分析(socket listen)

简介: sys_listen对面向连接的协议,在调用 bind(2)后,进一步调用 listen(2),让套接字进入监听状态: int listen(int sockfd, int backlog);backlog 表示新建连接请求时,最大的未处理的积压请求数。
sys_listen

对面向连接的协议,在调用 bind(2)后,进一步调用 listen(2),让套接字进入监听状态:
int listen(int sockfd, int backlog);

backlog 表示新建连接请求时,最大的未处理的积压请求数。
 
这里说到让套接字进入某种状态,也就是说,涉及到套接字的状态变迁,前面 create 和bind 时,也遇到过相应的代码。
 
sock和 sk 都有相应的状态字段,先来看 sock 的:
typedef enum {
        SS_FREE = 0,                        /*  套接字未分配  */
        SS_UNCONNECTED,              /*  套接字未连接  */
        SS_CONNECTING,                  /*  套接字正在处理连接 */
        SS_CONNECTED,                   /*  套接字已连接 */
        SS_DISCONNECTING              /*  套接字正在处理关闭连接 */
}socket_state;
 
在创建套接字时,被初始化为SS_UNCONNECTED
 
对于面向连接模式的SOCK_TREAM来讲,这样描述状态显然是不够的。这样在sk中, 使用sk_state维护了一个有限状态机
来描述套接字的状态:
enum {
  TCP_ESTABLISHED = 1,
  TCP_SYN_SENT,
  TCP_SYN_RECV,
  TCP_FIN_WAIT1,
  TCP_FIN_WAIT2,
  TCP_TIME_WAIT,
  TCP_CLOSE,
  TCP_CLOSE_WAIT,
  TCP_LAST_ACK,
  TCP_LISTEN,
  TCP_CLOSING,       /* now a valid state */
 
  TCP_MAX_STATES /* Leave at the end! */
};
 
还有一个相应的用来进行状态位运算的枚举结构:
enum {
  TCPF_ESTABLISHED = (1   TCPF_SYN_SENT = (1   TCPF_SYN_RECV = (1   TCPF_FIN_WAIT1 = (1   TCPF_FIN_WAIT2 = (1   TCPF_TIME_WAIT = (1   TCPF_CLOSE = (1   TCPF_CLOSE_WAIT = (1   TCPF_LAST_ACK = (1   TCPF_LISTEN = (1   TCPF_CLOSING = (1 };
 
值得一提的是,sk 的状态不等于 TCP的状态,虽然 sk 是面向协议栈,但它的状态并不能同 TCP状态一一直接划等号。
虽然这些状态值都用 TCP-XXX 来表式,但是只是因为 TCP协议状态非常复杂。sk 结构只是利用它的一个子集来抽像
描述而已。
 
同样地,操作码 SYS_LISTEN的任务会落到 sys_listen()函数身上:
/* Maximum queue length specifiable by listen.  */
#define SOMAXCONN        128
int sysctl_somaxconn = SOMAXCONN;
 
asmlinkage long sys_listen(int fd, int backlog)
{
        struct socket *sock;
        int err;
        
        if ((sock = sockfd_lookup(fd, &err)) != NULL) {
                if ((unsigned) backlog > sysctl_somaxconn)
                        backlog = sysctl_somaxconn;
 
                err = security_socket_listen(sock, backlog);
                if (err) {
                        sockfd_put(sock);
                        return err;
                }
 
                err=sock->ops->listen(sock, backlog);
                sockfd_put(sock);
        }
        return err;
}
 
同样地,函数会最终转向协议簇的 listen 函数,也就是 inet_listen():
 
/*
*        Move a socket into listening state.
*/
int inet_listen(struct socket *sock, int backlog)
{
        struct sock *sk = sock->sk;
        unsigned char old_state;
        int err;
 
        lock_sock(sk);
 
        err = -EINVAL;
 
        /* 在 listen 之前,sock 必须为未连接状态,且只有 SOCK_STREAM 类型,才有 listen(2)*/
        if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
                goto out;
 
        /*  临时保存状态机状态 */
        old_state = sk->sk_state;        

        /*  只有状态机处于 TCP_CLOSE  或者是 TCP_LISTEN  这两种状态时,才可能对其调用listen(2)
         *  这个判断证明了 listen(2)是可以重复调用地(当然是在转向 TCP_LISTEN 后没有再进行状态变迁*/
        if (!((1                 goto out;
 
        /*  如果接口已经处理 listen 状态,只修改其 max_backlog,否则先调用 tcp_listen_start,
         *  继续置协议的 listen 状态 */
        if (old_state != TCP_LISTEN) {
                err = tcp_listen_start(sk);
                if (err)
                        goto out;
        }
        sk->sk_max_ack_backlog = backlog;
        err = 0;
 
out:
        release_sock(sk);
        return err;
}
 
inet_listen 函数在确认 sock->state 和 sk->sk_state 状态后,会进一步调用 tcp_listen_start 函数,最且
最后设置 sk_max_ack_backlog  。
 
tcp 的 tcp_listen_start 函数,完成两个重要的功能,一个是初始化 sk 的一些相关成员变量,另一方
面是切换有限状态机的状态。 sk_max_ack_backlog表示监听时最大的 backlog 数量,它由用户空间
传递的参数决定。而 sk_ack_backlog表示当前的的 backlog数量。
 
当 tcp 服务器收到一个 syn 报文时,它表示了一个连接请求的到达。内核使用了一个 hash 表来维护这个连接请求表:
struct tcp_listen_opt
{
        u8                        max_qlen_log;        /* log_2 of maximal queued SYNs */
        int                        qlen;
        int                        qlen_young;
        int                        clock_hand;
        u32                      hash_rnd;
        struct open_request        *syn_table[TCP_SYNQ_HSIZE];
};
 
syn_table是open_request结构,就是连接请求表,连接表中的最大项,也就是最大允许的 syn 报文的数
量,由 max_qlen_log 来决定。当套接字进入 listen 状态,也就是说可以接收 syn 报文了,那么在此
之前,需要先初始化这个表: 
int tcp_listen_start(struct sock *sk)
{
        struct inet_sock *inet = inet_sk(sk);           //获取 inet结构指针
        struct tcp_sock *tp = tcp_sk(sk);                //获取协议指针
        struct tcp_listen_opt *lopt;
        
        //初始化 sk 相关成员变量
        sk->sk_max_ack_backlog = 0;
        sk->sk_ack_backlog = 0;
        
        tp->accept_queue = tp->accept_queue_tail = NULL;
        rwlock_init(&tp->syn_wait_lock);
        tcp_delack_init(tp);
        
        //初始化连接请求 hash 表
        lopt = kmalloc(sizeof(struct tcp_listen_opt), GFP_KERNEL);
        if (!lopt)
                return -ENOMEM;
 
        memset(lopt, 0, sizeof(struct tcp_listen_opt));

        //初始化 hash 表容量,最小为 6,其实际值由 sysctl_max_syn_backlog 决定
        for (lopt->max_qlen_log = 6; ; lopt->max_qlen_log++)
                if ((1 max_qlen_log) >= sysctl_max_syn_backlog)
                        break;
        get_random_bytes(&lopt->hash_rnd, 4);
 
        write_lock_bh(&tp->syn_wait_lock);
        tp->listen_opt = lopt;
        write_unlock_bh(&tp->syn_wait_lock);
 
        /* There is race window here: we announce ourselves listening,
         * but this transition is still not validated by get_port().
         * It is OK, because this socket enters to hash table only
         * after validation is complete.
         */
         /*  修改状态机状态,表示进入 listen 状态,根据作者注释,当宣告自己进入 listening 状态后,
          *  但是这个状态转换并没有得到 get_port 的确认。所以需要调用 get_port()函数。
          *  对于一点,暂时还没有完全搞明白,只有留待后面再来分析它 */
        sk->sk_state = TCP_LISTEN;
        if (!sk->sk_prot->get_port(sk, inet->num)) {
                inet->sport = htons(inet->num);
                sk_dst_reset(sk);
                sk->sk_prot->hash(sk);
 
                return 0;
        }
 
        sk->sk_state = TCP_CLOSE;
        write_lock_bh(&tp->syn_wait_lock);
        tp->listen_opt = NULL;
        write_unlock_bh(&tp->syn_wait_lock);
        kfree(lopt);
        return -EADDRINUSE;
}
 
在切换了有限状态机状态后,调用了
sk->sk_prot->hash(sk);
也就是 tcp_v4_hash()函数。这里涉到到另一个 hash 表:TCP监听 hash 表。
 
TCP监听 hash表
所谓 TCP 监听表,指的就内核维护“当前有哪些套接字在监听”的一个表,当一个数据包进入 TCP
栈的时候,内核查询这个表中对应的 sk,以找到相应的数据 结构。(因为 sk 是面向网络栈调用的,找到了sk,
就找到了 tcp_sock,就找到了 inet_sock,就找到了 sock,就找到了 fd……就到了组  织了)。
 
TCP所有的 hash 表都用了tcp_hashinfo来封装,前面分析 bind已见过它:
extern struct tcp_hashinfo {
        ……
        /* 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.
         */
        struct hlist_head __tcp_listening_hash[TCP_LHTABLE_SIZE];
 
        ……
        spinlock_t __tcp_portalloc_lock;
} tcp_hashinfo;
 
#define tcp_listening_hash (tcp_hashinfo.__tcp_listening_hash)
 
函数 tcp_v4_hash 将一个处理监听状态下的 sk 加入至这个 hash 表:
static void tcp_v4_hash(struct sock *sk)
{
        if (sk->sk_state != TCP_CLOSE) {
                local_bh_disable();                
                __tcp_v4_hash(sk, 1);
                local_bh_enable();
        }
}
 
因为__tcp_v4_hash 不只用于监听 hash 表,它也用于其它 hash 表,其第二个参数 listen_possible 为
真的时候,表示处理的是监听 hash表:
static __inline__ void __tcp_v4_hash(struct sock *sk, const int listen_possible)
{
        struct hlist_head *list;
        rwlock_t *lock;
 
        BUG_TRAP(sk_unhashed(sk));
        if (listen_possible && sk->sk_state == TCP_LISTEN) {
                list = &tcp_listening_hash[tcp_sk_listen_hashfn(sk)];
                lock = &tcp_lhash_lock;
                tcp_listen_wlock();
        } else {
                                ……
        }
        __sk_add_node(sk, list);
        sock_prot_inc_use(sk->sk_prot);
        write_unlock(lock);
        if (listen_possible && sk->sk_state == TCP_LISTEN)
                wake_up(&tcp_lhash_wait);
}
else 中的部份用于另一个 hash 表,暂时不管它。代表很简单,如果确认是处理的是监听 hash 表。
则先根据 sk计算一个 hash 值,在hash 桶中找到入口。再调用__sk_add_node 加入至该 hash 链。
 
tcp_sk_listen_hashfn()函数事实上是 tcp_lhashfn 的包裹,前面已经分析过了。
 
__sk_add_node()函数也就是一个简单的内核 hash处理函数 hlist_add_head()的包裹:
static __inline__ void __sk_add_node(struct sock *sk, struct hlist_head *list)
{
        hlist_add_head(&sk->sk_node, list);
}
 
小结
 
一个套接字的 listen,主要需要做的工作有以下几件:
1. 初始化 sk 相关的成员变量,最重要的是 listen_opt,也就是连接请求 hash 表。
2. 将 sk 的有限状态机转换为 TCP_LISTEN,即监听状态;
3. 将 sk 加入监听 hash表;
4. 设置允许的最大请求积压数,也就是 sk 的成员 sk_max_ack_backlog 的值。 
相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。     相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
目录
相关文章
|
1月前
|
安全 Linux iOS开发
Binary Ninja 5.1.8104 (macOS, Linux, Windows) - 反编译器、反汇编器、调试器和二进制分析平台
Binary Ninja 5.1.8104 (macOS, Linux, Windows) - 反编译器、反汇编器、调试器和二进制分析平台
305 53
Binary Ninja 5.1.8104 (macOS, Linux, Windows) - 反编译器、反汇编器、调试器和二进制分析平台
|
1月前
|
Linux API iOS开发
Binary Ninja 4.2.6455 (macOS, Linux, Windows) - 反编译器、反汇编器、调试器和二进制分析平台
Binary Ninja 4.2.6455 (macOS, Linux, Windows) - 反编译器、反汇编器、调试器和二进制分析平台
169 14
Binary Ninja 4.2.6455 (macOS, Linux, Windows) - 反编译器、反汇编器、调试器和二进制分析平台
|
2月前
|
数据管理 Linux iOS开发
Splunk Enterprise 9.4.5 (macOS, Linux, Windows) - 机器数据管理和分析
Splunk Enterprise 9.4.5 (macOS, Linux, Windows) - 机器数据管理和分析
117 0
|
Unix Linux iOS开发
Splunk Enterprise 10.0.0 (macOS, Linux, Windows) - 搜索、分析和可视化,数据全面洞察平台
Splunk Enterprise 10.0.0 (macOS, Linux, Windows) - 搜索、分析和可视化,数据全面洞察平台
103 0
|
6月前
|
Ubuntu Linux 网络安全
在Linux云服务器上限制特定IP进行SSH远程连接的设置
温馨提示,修改iptables规则时要格外小心,否则可能导致无法远程访问你的服务器。最好在掌握足够技术知识和理解清楚操作含义之后再进行。另外,在已经配置了防火墙的情况下,例如ufw(Ubuntu Firewall)或firewalld,需要按照相应的防火墙的规则来设置。
358 24
|
7月前
|
安全 Linux
阿里云linux服务器使用脚本通过安全组屏蔽异常海外访问ip
公网网站可能会遭受黑客攻击导致访问异常,使用此脚本可以屏蔽掉异常IP 恢复访问。也可自行设置定时任务定期检测屏蔽。
584 28
|
8月前
|
监控 Linux
Linux基础:文件和目录类命令分析。
总的来说,这些基础命令,像是Linux中藏匿的小矮人,每一次我们使用他们,他们就把我们的指令准确的传递给Linux,让我们的指令变为现实。所以,现在就开始你的Linux之旅,挥动你的命令之剑,探索这个充满神秘而又奇妙的世界吧!
163 19
|
7月前
|
人工智能 Kubernetes Ubuntu
linux配置IP
linux配置IP
677 1
|
9月前
|
缓存 网络协议 Linux
PCIe 以太网芯片 RTL8125B 的 spec 和 Linux driver 分析备忘
本文详细介绍了 Realtek RTL8125B PCIe 以太网芯片的规格以及在 Linux 中的驱动安装和配置方法。通过深入分析驱动源码,可以更好地理解其工作原理和优化方法。在实际应用中,合理配置和优化驱动程序可以显著提升网络性能和稳定性。希望本文能帮助您更好地使用和管理 RTL8125B,以满足各种网络应用需求。
941 33
|
9月前
|
数据管理 Linux iOS开发
Splunk Enterprise 9.4.1 (macOS, Linux, Windows) 发布 - 机器数据管理和分析
Splunk Enterprise 9.4.1 (macOS, Linux, Windows) 发布 - 机器数据管理和分析
219 0
Splunk Enterprise 9.4.1 (macOS, Linux, Windows) 发布 - 机器数据管理和分析