Linux内核角度分析服务器Listen细节
Listen功能简述
编写服务器程序时,在Linux中需要调用Listen系统调用,如下所示,Listen系统调用的主要功能就是根据传入的backlog参数创建连接队列,并将套接字的状态迁移至LISTEN状态,最后将监听sock注册到TCP全局的监听套接字哈希表。
Listen系统调用-函数执行流程
系统调用调用的函数执行如下所示:
其中sockfd_lookup_light函数根据fd描述符得到struct socket结构体,并找到当前系统设定的最大可监听连接数somaxconn ,PROC文件系统中somaxconn默认为128,意味着单个套接口队列的长度,可最大监听128个连接 ,如下所示:
somaxconn与Listen系统调用传入的参数backlog进行比较,若当前传入的参数backlog大于somaxconn则使用somaxconn,即backlog最大值不能超过somaxconn。该系统调用核心是执行:sock->ops->listen(sock,backlog) ;也就是说找到服务器的socket后,通过它的协议操作表结构struct proto_ops执行其listen钩子函数,proto_ops协议操作表结构的挂入是在socket创建过程根据协议类型进行设置的,TCP实际挂入的是inet_stream_ops操作表结构,listen在inet_stream_ops表中的赋值如下所示:
【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)


故sock->ops->listen针对于TCP而言,继续调用inet_listen函数:
inet_listen函数首先是对套接字类型、状态进行检查,类型必须是流式套接字且状态必须是close或者listen状态:
inet_listen函数核心的继续调用inet_csk_start_listen函数:
inet_csk_listen_start函数通过reqsk_queue_alloc创建连接队列,队列结构体如下,队列的最大长度是sk_max_ack_backlog,也就是用户传入的backlog参数值,队列的长度计数是sk_ack_backlog。
其中request_sock结构体是请求队列的节点如下所示,*dl_next将所有的accept请求串起来。
struct request_sock_queue和struct request_sock的关系如下:

inet_csk_listen_start调用的分配并初始化连接队列的函数reqsk_queue_alloc如下所示,其中可以看到queue->rskq_accept_head初始化为NULL
inet_csk_listen_start函数中另一个核心内容就是调用哈希函数:
sk->sk_prot->hash(sk)将监听sock注册到TCP全局的监听套接字哈希表,对于TCP对应的协议栈,hash函数是inet_hash:
继续调用__inet_hash:
关于:
struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
通过图解,如下所示:

最终得到struct inet_hashinfo:
内核将监听队列分为32个哈希桶bucket:listening_hash[INET_LHTABLE_SIZE],保存处于监听状态的TCP套接口哈希链表,每个哈希桶由独立的保护锁和链表,此结构通过减小锁的粒度,增加并行处理的可能,每个哈希桶如下所示:
哈希桶的选择由函数inet_sk_listen_hashfn的返回值决定
inet_sk_listen_hash函数使用数据包的目的端口号(本地监听端口号)计算的hash值为索引得到具体的哈希桶,如下所示:
内核为处于LISTEN状态的socket分配了大小为32的哈希桶,监听的端口号经过哈希算法运算打散到这些哈希桶中(如果开启了IPV6,并且启用了端口重用,将此套接口添加在监听套接口桶的链表末尾;否则,添加到链表头部,如下代码所示)
如下图所示,哈希链表的组织方式:

当收到客户端的 SYN 握手报文以后,会根据目标端口号的哈希值计算出哈希冲突链表,然后遍历这条哈希链表得到对应的socket。
