当前位置: 首页 > 科技观察

SeeSocket(TCP)ListenandConnectionQueuefromLinuxSourceCode

时间:2023-03-16 18:03:25 科技观察

SeeSocket(TCP)ListenandConnectionQueuefromLinuxSourceCode这是一件令人兴奋的事情。今天,我将从Linux源码(基于Linux3.10内核)的角度来看一下server端的Socket在监听的时候是干什么的。当然因为listen的backlog参数和半连接哈希表和全连接队列有关,在这篇博客中也一起讲了。服务器端Socket需要Listen。众所周知,Server端Socket的建立需要四个步骤:socket、bind、listen、accept。今天我将重点介绍Listen步骤。代码如下:call_err==-1){fprintf(stdout,"bindererror!\n");exit(1);}//这里是我们今天的重点listencall_err=listen(sockfd_server,MAX_BACK_LOG);if(call_err==-1){fprintf(stdout,"listenererror!\n");exit(1);}}首先,我们通过socket系统调用创建一个socket,它指定了SOCK_STREAM,最后一个参数为0,即我们创建一个socket通常是拥有TCPSocket。这里我们直接给出了TCPSocket对应的ops,也就是操作函数。想知道上图中的结构是怎么来的,可以看作者之前的博客:https://my.oschina.net/alchemystar/blog/1791017Listen系统调用OK,现在我们直接进入Listen系统称呼。#include//成功返回0,错误返回-1,错误码设置在errnointlisten(intsockfd,intbacklog);注意这里的listen调用是通过glibc的INLINE_SYSCALL安装的,其修正返回值只有0和-1两个选项,并在errno中设置错误码的绝对值。这里的backlog是一个非常重要的参数。如果设置不当,就是一个非常隐蔽的坑。对于java开发者来说,基本都是使用现成的框架,而java本身默认的backlog设置大小只有50,这会造成一些细微的现象,本文会进行说明。接下来我们进入Linux内核源码栈listen|->INLINE_SYSCALL(listen...)|->SYSCALL_DEFINE2(listen,int,fd,int,backlog)/*检查对应的描述符fd是否存在,不存在,return-BADF|->sockfd_lookup_light/*限制传递过来的backlog最大值不超过/proc/sys/net/core/somaxconn|->if((unsignedint)backlog>somaxconn)backlog=somaxconn|->sock->ops->listen(sock,backlog)<=>inet_listen值得注意的是,Kernel对我们传入的backlog值做了调整,使其无法在内核参数设置中>somaxconn。inet_listen是inet_listen接下来的核心调用程序。intinet_listen(structsocket*sock,intbacklog){/*Really,ifthesocketisalreadyinlistenstate*wecanonlyallowthebacklogtobeadjusted.*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);elseif((sysctl_tcp_fastopen&TFO_SERVER_WO_SOCKOPT2)!=0)err=fastopen_init_queue(sk,((uint)sysctl_tcp_fastopen)>>16);elseerr=0;if(err)gotoout;}if(old_state!=TCP_LISTEN){err=inet_csk_listen_start(sk,backlog);}sk->sk_max_ack_backlog=backlog;...}从这段代码来看,第一个有意思的是listen系统调用可以重复调用!第二次调用只能修改其积压队列的长度(虽然感觉没必要)。首先我们看一下fastopen以外的逻辑(fastopen会在开篇详细讨论)。这是最后一次inet_csk_listen_start调用。intinet_csk_listen_start(structsock*sk,constintnr_table_entries){...//这里的nr_table_entries是调整后的backlog//但是在这个函数中,nr_table_entries=min(backlog,sysctl_max_syn_backlog)的逻辑会进一步intrc=reqsk_queue_alloc(&icsk->icsk_accept_queue,nr_table_entries);......inet_csk_delack_init(sk);//设置socket为监听状态sk->sk_state=TCP_LISTEN;//查看端口号if(!sk->sk_prot->get_port(sk,inet->inet_num)){//清除dstcachesk_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);//端口已经被占用,返回错误码-EADDRINUSEreturn-EADDRINUSE;}这里最重要的调用是sk->sk_prot->hash(sk),也就是inet_hash,将当前sock链接到全局listen哈希表中,这样在SY时就可以找到对应的listensockN个数据包到达。如下图所示:如图所示,如果启用了SO_REUSEPORT,不同的Socket可以监听(监听)同一个端口,这样就可以在内核中创建连接的负载均衡。Nginx1.9.1版本启用后,压测性能提升三倍!半连接队列哈希表和全连接队列在我开头看的资料中有提到。tcp连接队列有两个,一个是sync_queue,一个是accept_queue。但是笔者仔细阅读了源码,并不是这样的。其实sync_queue其实就是一个哈希表(syn_table)。另一个队列是icsk_accept_queue。所以在本文中,称之为reqsk_queue(request_socket_queue的简称)。在这里,笔者首先给出了三次握手时这两个队列出现的时机。如下图所示:当然,除了上面提到的qlen和sk_ack_backlog这两个计数器之外,还有一个qlen_young,它的作用如下:qlen_young:记录一个SYN刚刚到达,还没有被SYN_ACK重传重传定时器socks通过SYN_ACK还没有完成三次握手的次数如下图所示:关于SYN_ACK的重传定时器,内核中的代码如下:staticvoidtcp_synack_timer(structsock*sk){inet_csk_reqsk_queue_prune(sk,TCP_SYNQ_INTERVAL,TCP_TIMEOUT_INIT,TCP_RTO_MAX);}当半连接队列不为空时,此计时器以200毫秒(TCP_SYNQ_INTERVAL)的间隔运行。限于篇幅,笔者在此不再多述。为什么会有半连接队列?因为根据TCP协议的特点,会存在半连接等网络攻击,即不断发送SYN包,却从不响应SYN_ACK。如果发送一个SYN包让Kernel创建一个消耗很多的sock,那么就很容易内存溢出。因此,在三次握手成功之前,内核只是分配了一个占用内存非常小的request_sock来防止这种攻击,并配合syn_cookie机制来尝试抵御这种半连接攻击的风险。半连接哈希表和全连接队列的限制由于全连接队列存放的是普通的socks,占用内存较多,所以Kernel为其增加了最大长度限制。这个限制是:以下三个中的最小值:1.listen系统调用中传入的backlog2./proc/sys/inet/ipv4/tcp_max_syn_backlog3./proc/sys/net/core/somaxconn为min(backlog,tcp_ma_syn_backlog,somaxcon)如果超过了这个somaxconn,就会被内核丢弃,如下图所示:在这种情况下,丢弃连接时会出现一个奇怪的现象。当不设置tcp_abort_on_overflow时,client端无法感知,会导致peerconnection只在第一次调用时掉线。那么,这种情况下如何让client感知,我们可以设置tcp_abort_on_overflowecho'1'>tcp_abort_on_overflow设置,如下图:当然,最直接的方法就是增加backlog!listen(fd,2048)echo'2048'>/proc/sys/inet/ipv4/tcp_max_syn_backlogecho'2048'>/proc/sys/net/core/somaxconnbacklog对半连接队列的影响这个backlog对半连接队列也有影响,如图如下代码:/*TWbuckets转换为openrequests,没有*limitations,theyconserveresourcesandpeeris*evidentlyrealone.*///启用SYNcookie时,如果半连接队列长度超过backlog,则发送cookie//否则丢弃(inet_csk_reqsk_queue_is_full(sk)&&!isn){want_cookie=tcp_syn_flood_action(sk,skb,"TCP");if(!want_cookie)gotodrop;}/*接受后备日志已满。/当满connectionqueue已满,如果有young_ack,则直接discardif(sk_acceptq_is_full(sk)&&inet_csk_reqsk_queue_young(sk)>1){NET_INC_STATS_BH(sock_net(sk),LINUX_MIB_LISTENOVERFLOWS);gotodrop;}我们在dmesg中经常看到的PossibleSYNfloodingonport8080是因为半连接队列已满,Kernel发送cookie检查并得出TCP是一种古老而流行的协议的结论。经过几十年的演变,其设计已经相当复杂。结果,当出现问题时,变得难以分析。这时候,他妈的阅读源码就很有必要了!而作者也是无意中写了这篇博客并详细阅读了源码,找到了最近一个怪问题的根源。因为。这个怪问题的分析过程会在近期写出来分享给大家。本文转载自微信公众号《Bug解决之路》,可通过以下二维码关注。转载本文请联系BUG解决公众号。