本文转载自微信公众号《内功修炼之养成》,作者张燕飞allen。转载本文请联系内功修炼发展公众号。大家好,我是飞哥。飞哥来北京9年多,最近终于拿到了北京电动车牌。写10,000字所涉及的艰难过程可能不足以完成。不管怎么说,新能源也是车,终于有了能开的车。这几天买车卖车(洋牌)很忙。但是再忙,硬核文章还是停不下来!大家都知道,在创建服务器程序时,需要先监听,然后再接收客户端请求。例如,下面这段代码我们再熟悉不过了。intmain(intargc,charconst*argv[]){intfd=socket(AF_INET,SOCK_STREAM,0);bind(fd,...);listen(fd,128);accept(fd,...);所以今天我们我们想一个问题,为什么需要监听接收连接?或者换句话说,listen在内部执行的时候到底做了什么?如果你也想弄清楚listen里面的这些秘密,就请关注我吧!1.创建套接字服务器要做的第一件事是创建套接字。具体来说,通过调用socket函数。当执行socket函数时,从用户层的角度我们可以看到返回了一个文件描述符fd。但在内核中实际上是一组内核对象的组合,大致结构如下。下面简单理解一下这个结构。后面在源码中看到函数指针调用的时候,需要回过头来看。2、内核执行listen2.1listen系统调用。我在net/socket.c下找到了listen系统调用的源码。//file:net/socket.cSYSCALL_DEFINE2(listen,int,fd,int,backlog){//根据fd查找socket内核对象sock=sockfd_lookup_light(fd,&err,&fput_needed);if(sock){//获取内核parametersnet.core.somaxconnsomaxconn=sock_net(sock->sk)->core.sysctl_somaxconn;if((unsignedint)backlog>somaxconn)backlog=somaxconn;//调用协议栈注册的listen函数err=sock->ops->listen(sock,backlog);......}用户态的socket文件描述符只是一个整数,内核不能直接使用。所以这个函数的第一行代码就是根据用户传入的文件描述符找到对应的socket内核对象。然后获取系统中net.core.somaxconn内核参数的值,与用户传入的backlog进行比较,然后取一个最小值传递给下一步。因此,虽然listen允许我们传入backlog(这个值与半连接队列和全连接队列有关)。但是如果用户传入一个大于net.core.somaxconn的值就不行了。然后通过调用sock->ops->listen进入协议栈的listen函数。2.2协议栈listen这里需要用到第一节中的socket内核对象结构图,通过它可以看出sock->ops->listen实际上是执行了inet_listen。//file:net/ipv4/af_inet.cintinet_listen(structsocket*sock,intbacklog){//还没有进入监听状态(notlistenedyet)if(old_state!=TCP_LISTEN){//开始监听err=inet_csk_listen_start(sk,backlog);}//设置全连接队列长度sk->sk_max_ack_backlog=backlog;}这里我们先看底线,sk->sk_max_ack_backlog是全连接队列的最大长度。所以这里我们知道一个关键的技术点,服务器的全连接队列长度是监听时传入的backlog和net.core.somaxconn中较小的值。如果线上遇到全连接队列溢出问题,想增加队列长度,可能需要同时考虑backlog和listen时传入的net.core.somaxconn。回头看看inet_csk_listen_start函数。//file:net/ipv4/inet_connection_sock.cintinet_csk_listen_start(structsock*sk,constintnr_table_entries){structinet_connection_sock*icsk=inet_csk(sk);//icsk->icsk_accept_queue为接收队列,详见2.3节//接收队列内核对象申请及初始化见2.4节intrc=reqsk_queue_alloc(&icsk->icsk_accept_queue,nr_table_entries);......}函数开头将structsock对象强制转换为inet_connection_sock,命名为icsk。这里简单说明一下为什么可以这样转换,因为inet_connection_sock包含sock。tcp_sock、inet_connection_sock、inet_sock、sock是层层嵌套的关系,类似于面向对象中的继承概念。对于TCP套接字,sock对象实际上是一个tcp_sock。因此,TCP中的sock对象可以转换为tcp_sock、inet_connection_sock、inet_sock,以备随时使用。在下一行中,reqsk_queue_alloc实际上包含了两件重要的事情。一是接收队列数据结构的定义。其次是接收队列的申请和初始化。这两块比较重要,我们将分别在2.3节和2.4节介绍。2.3接收队列的定义icsk->icsk_accept_queue定义在inet_connection_sock下,是一个request_sock_queue类型的对象。它是内核用来接收客户端请求的主要数据结构。我们平时说的全连接队列和半连接队列都是在这个数据结构中实现的。我们来看具体的代码。//file:include/net/inet_connection_sock.hstructinet_connection_sock{/*inet_sockhastobethefirstmember!*/structinet_sockicsk_inet;structrequest_sock_queueicsk_accept_queue;...}我们再找request_sock_queue的定义,如下。//file:include/net/request_sock.hstructrequest_sock_queue{//全连接队列structrequest_sock*rskq_accept_head;structrequest_sock*rskq_accept_tail;//半连接队列structrequest_sock*listen_opt;...};对于全连接队列,在中不需要做复杂的查找工作,接受时只接受先进先出。所以全连接队列是通过rskq_accept_head和rskq_accept_tail以链表的形式进行管理的。半连接队列相关的数据对象是listen_opt,类型为listen_sock。//file:structlisten_sock{u8max_qlen_log;u32nr_table_entries;......structrequest_sock*syn_table[0];};由于服务器端需要在第三次握手时快速找出第一次握手保留的request_sock对象,所以其实,用一个哈希表来管理,就是structrequest_sock*syn_table[0]。max_qlen_log和nr_table_entries都与半连接队列的长度有关。2.4接收队列申请及初始化了解完全/半连接队列数据结构,让我们回到inet_csk_listen_start函数。调用reqsk_queue_alloc申请并初始化重要对象icsk_accept_queue。//file:net/ipv4/inet_connection_sock.cintinet_csk_listen_start(structsock*sk,constintnr_table_entries){...intrc=reqsk_queue_alloc(&icsk->icsk_accept_queue,nr_table_entries);...}接收队列的request_sock_queue内核对象完成在reqsk_queue_alloc函数创建和初始化。其中包括内存申请、半连接队列长度的计算、全连接队列头部的初始化等等。我们进入其源码://file:net/core/request_sock.cintreqsk_queue_alloc(structrequest_sock_queue*queue,unsignedintnr_table_entries){size_tlopt_size=sizeof(structlisten_sock);structlisten_sock*lopt;//计算半连接队列长度nr_table_entries=min_t(u3,nr_table_entries,sysctl_max_syn_backlog);nr_table_entries=...//为listen_sock对象申请内存,其中包括半连接队列lopt_size+=nr_table_entries*sizeof(structrequest_sock*);if(lopt_size>PAGE_SIZE)lopt=vzalloc(lopt_size);elselopt=kzalloc(lopt_size,GFP_KERNEL);//全连接队列头初始化queue->rskq_accept_head=NULL;//半连接队列设置lopt->nr_table_entries=nr_table_entries;queue->listen_opt=lopt;...}开头定义了一个structlisten_sock指针。这个listen_sock就是我们通常所说的半连接队列。接下来计算半连接队列的长度。计算出实际大小后,开始申请内存。最后将全连接队列头queue->rskq_accept_head设置为NULL,将半连接队列挂在接收队列queue上。这里有一个细节需要注意。半连接队列上的每个元素都分配了一个指针大小(sizeof(structrequest_sock*))。这实际上是一个哈希表。真正半连接的request_sock对象是在握手过程中分配的,计算出Hash值后挂在Hash表上。2.5半连接队列长度的计算在上一节中,我们提到在reqsk_queue_alloc函数中计算半连接队列的长度。由于这有点复杂,我们将在单独的部分中进行讨论。//file:net/core/request_sock.cintreqsk_queue_alloc(structrequest_sock_queue*queue,unsignedintnr_table_entries){//计算半连接队列的长度nr_table_entries=min_t(u32,nr_table_entries,sysctl_max_syn_backlog);nr_table_entries=max_t(u32,riers_table,8_table)=roundup_pow_of_two(nr_table_entries+1);//为了效率,不记录nr_table_entries//而是记录2的几个次方等于nr_table_entriesfor(lopt->max_qlen_log=3;(1<
