从Linux源码看Socket的listen及连接队列

今天就从linux源码的角度看下Server端的Socket在进行listen的时候到底做了哪些事情(基于Linux 3.10内核),当然由于listen的backlog参数和半连接hash表以及全连接队列都相关,在这里也一块讲了 。
Server端Socket需要Listen众所周知,一个Server端Socket的建立,需要socket、bind、listen、accept四个步骤 。今天笔者就聚焦于Listen这个步骤 。

从Linux源码看Socket的listen及连接队列

文章插图
 
代码如下:
void start_server(){// server fdint sockfd_server;// accept fdint sockfd;int call_err;struct sockaddr_in sock_addr;......call_err=bind(sockfd_server,(struct sockaddr*)(&sock_addr),sizeof(sock_addr));if(call_err == -1){fprintf(stdout,"bind error!n");exit(1);}// 这边就是我们今天的聚焦点listencall_err=listen(sockfd_server,MAX_BACK_LOG);if(call_err == -1){fprintf(stdout,"listen error!n");exit(1);}}首先我们通过socket系统调用创建了一个socket,其中指定了SOCK_STREAM,而且最后一个参数为0,也就是建立了一个通常所有的TCP Socket 。在这里,我们直接给出TCP Socket所对应的ops也就是操作函数 。
从Linux源码看Socket的listen及连接队列

文章插图
 
 Listen系统调用好了,现在我们直接进入Listen系统调用吧 。
#include <sys/socket.h>// 成功返回0,错误返回-1,同时错误码设置在errnoint listen(int sockfd, int backlog);注意,这边的listen调用是被glibc的INLINE_SYSCALL装过一层,其将返回值修正为只有0和-1这两个选择,同时将错误码的绝对值设置在errno内 。这里面的backlog是个非常重要的参数,如果设置不好,是个很隐蔽的坑 。
对于JAVA开发者而言,基本用的现成的框架,而java本身默认的backlog设置大小只有50 。这就会引起一些微妙的现象,这个在本文中会进行讲解 。
从Linux源码看Socket的listen及连接队列

文章插图
 
接下来,我们就进入Linux内核源码栈吧
listen |->INLINE_SYSCALL(listen......)|->SYSCALL_DEFINE2(listen, int, fd, int, backlog)/* 检测对应的描述符fd是否存在,不存在,返回-BADF|->sockfd_lookup_light/* 限定传过来的backlog最大值不超出 /proc/sys/net/core/somaxconn|->if ((unsigned int)backlog > somaxconn) backlog = somaxconn|->sock->ops->listen(sock, backlog) <=> inet_listen值得注意的是,Kernel对于我们传进来的backlog值做了一次调整,让其无法>内核参数设置中的somaxconn 。
需要C/C++ Linux高级服务器架构师学习资料后台私信“资料”(包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)
从Linux源码看Socket的listen及连接队列

文章插图
 
inet_listen接下来就是核心调用程序inet_listen了 。
int inet_listen(struct socket *sock, int backlog){ /* Really, if the socket is already in listen state* we can only allow the backlog to be adjusted.*if ((sysctl_tcp_fastopen & TFO_SERVER_ENABLE) != 0 &&inet_csk(sk)->icsk_accept_queue.fastopenq == NULL) {fastopen的逻辑if ((sysctl_tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) != 0)err = fastopen_init_queue(sk, backlog);else if ((sysctl_tcp_fastopen &TFO_SERVER_WO_SOCKOPT2) != 0)err = fastopen_init_queue(sk,((uint)sysctl_tcp_fastopen) >> 16);elseerr = 0;if (err)goto out;} if(old_state != TCP_LISTEN) {err = inet_csk_listen_start(sk, backlog); } sk->sk_max_ack_backlog =backlog; ......}从这段代码中,第一个有意思的地方就是,listen这个系统调用可以重复调用!第二次调用的时候仅仅只能修改其backlog队列长度(虽然感觉没啥必要) 。
从Linux源码看Socket的listen及连接队列

文章插图
 
首先,我们看下除fastopen之外的逻辑(fastopen以后开单章详细讨论) 。也就是最后的inet_csk_listen_start调用 。
int inet_csk_listen_start(struct sock *sk, const int nr_table_entries){ ...... // 这里的nr_table_entries即为调整过后的backlog // 但是在此函数内部会进一步将nr_table_entries = min(backlog,sysctl_max_syn_backlog)这个逻辑 int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries); ...... inet_csk_delack_init(sk); // 设置socket为listen状态 sk->sk_state = TCP_LISTEN; // 检查端口号 if (!sk->sk_prot->get_port(sk, inet->inet_num)){// 清除掉dst cachesk_dst_reset(sk);// 将当前sock链入listening_hash// 这样,当SYN到来的时候就能通过__inet_lookup_listen函数找到这个listen中的socksk->sk_prot->hash(sk); } sk->sk_state = TCP_CLOSE; __reqsk_queue_destroy(&icsk->icsk_accept_queue); // 端口已经被占用,返回错误码-EADDRINUSE return -EADDRINUSE;}


推荐阅读