WRITEAS憋着做 WAIT1状态理解

其实WRITEAS憋着做的问题并不复杂,但是又很多的朋友都不太了解WAIT1状态理解,因此呢,今天小编就来为大家分享WRITEAS憋着做的一些知识,希望可以帮助到大家,下面我们一起来看看这个问题的分析吧!

从websocket协议到tcp自定义协议,tcp分包与粘包,明文传输

TCP/IP协议栈深度解析丨实现单机百万连接丨优化三次握手、四次挥手

C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

近期遇到一个问题,简单点说,主机A上显示一条ESTABLISHED状态的TCP连接到主机B,而主机B上却没有任何关于主机A的连接信息,经查明,这是由于主机A和主机B的发送/接收缓冲区差异巨大,导致主机B进程退出后,主机A暂时憋住,主机B频繁发送零窗口探测,FIN_WAIT1状态超时,进而连接被销毁,然而主机A并不知情导致。

正好昨天也有人咨询另外一个类似的问题,那么就抽昨晚和今天早上的时间,写一篇总结吧。

不要觉得你对TCP的实现的代码烂熟于心了就能把控它的所有行为!不知道你有没有发现,目前市面上新上市的关于Linux内核协议栈的书可谓是汗牛充栋,然而无论作者是国内的还是国外,几乎都是碰到TCP就草草略过,反而对IP,ARP,DNS这些大书特书,Why?因为Linux内核里TCP的代码太乱太复杂了,很少有人能看明白80%以上的,即便真的有看过的,其中还包括只懂代码而不懂网络技术的,我就发现很多声称自己精通Linux内核TCP/IP源码,结果竟然不知道什么是默认路由…

所以我打算写一篇文章,趁着这个FIN_WAIT1问题,顺便表达一下我是如何学习网络技术,我是如何解决网络问题的方法论观点,都是形而上,个人看法:

本文聊聊TCP的FIN_WAIT1以及TCP假连接(死连接)问题。先看FIN_WAIT1。

首先还是从状态机入手,看看和FIN_WAIT1相关的状态机转换图:

我们只考虑常规的从ESTABLISHED状态的转换,很简单的一个单一状态转换:

ESTAB状态发送FIN即切换到FIN_WAIT1状态;

FIN_WAIT1状态下收到针对FIN的ACK即可离开FIN_WAIT1到达FIN_WAIT2.

看一下和上述状态机转换相关的简单时序图:

从状态图和时序图上,我们很明确地可以看到,FIN_WAIT1持续1个RTT左右的时间!这个时间段几乎不会被肉眼观察到,转瞬而即逝

我们之所以得到FIN_WAIT1持续1个RTT这个结论,基于两个假设,即:

两端TCP之间的链路是正常的,可达的。

OK,接下来我们来设计一个实验模拟异常的情况。准备实验拓扑如下:

host1和host2的系统内核版本(uname-r获取):

3.10.0-862.2.3.el7.x86_64

首先,我们看一下如果对端TCP针对FIN发送的ACK丢失,会发生什么。按照上述的时序图,正常应该是FIN_WAIT1将会永久持续。我们来验证一下。

【文章福利】:小编整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!~点击加入(832218493需要自取)

实验1:模拟ACK丢失

nc-l-p1234

host2上完成以下命令:

cat/dev/zero|nc1.1.1.11234

以上保证了host1和host2之间的TCP建立并且连接之间有持续的数据传输。接下来,在host2上执行下列动作:

iptables-AINPUT-ptcp--tcp-flagsACK,FINACKkillallnc

此时在host2上:

n[root@localhost~]#netstat-antp|grep1234tcp012291.1.1.2:393181.1.1.1:1234FIN_WAIT1-

连续上翻命令,这个FIN_WAIT1均不会消失,暂时符合我们的预期…出去抽根烟,刷会儿微博…回来后,发现这个FIN_WAIT1消失了!

它是如何消失的呢?这个时候,我们提取netstat数据,执行“netstat-st”,会发现:

TcpExt:...1connectionsabortedduetotimeout

多了一条timeout连接!

虽然说在协议上规范上看,TCP没有必要为链路或者说对端的不合常规的行为而买单,但是从现实角度,TCP的实现必须处理异常情况,TCP的实现必然要有所限制!。

我们知道,计算机是无法处理无限,无穷这种抽线的数学概念的,所有如果针对FIN的ACK迟迟不来,那么必然要有一个等待的极限,这个极限在Linux内核协议栈中由以下参数控制:

net.ipv4.tcp_orphan_retries#默认值是0!这里有坑...

这个参数表示如果一直都收不到针对FIN的ACK,那么在彻底销毁这个FIN_WAIT1的连接前,等待几轮RTO退避。

所谓的orphantcpconnection,意思就是说,在Linux进程层面,创建该连接的进程已经退出销毁了,然而在TCP协议层面,它依然在遵循TCP状态机的转换规则存在着。

注意,这个参数不是一个时间量,而是一个次数量。我们知道,TCP每一次超时,都会对下一次超时时间进行指数退避,这里的次数量就是要经过几次退避的时间。举一个例子,如果RTO是2ms,而tcp_orphan_retries的值是4,那么所计算出的FIN_WAIT1容忍时间就是:

还是看看Linux内核文档怎么说的吧

??ThisvalueinfluencesthetimeoutofalocallyclosedTCPconnection,whenRTOretransmissionsremainunacknowledged.

??Seetcp_retries2formoredetails.

??IfyourmachineisaloadedWEBserver,

??youshouldthinkaboutloweringthisvalue,suchsockets

??mayconsumesignificantresources.Cf.tcp_max_orphans.

让我们看看tcp_retries2,以获取数值的含义:

??ThisvalueinfluencesthetimeoutofanaliveTCPconnection,

??whenRTOretransmissionsremainunacknowledged.

??GivenavalueofN,ahypotheticalTCPconnectionfollowing

??exponentialbackoffwithaninitialRTOofTCP_RTO_MINwould

??retransmitNtimesbeforekillingtheconnectionatthe(N+1)thRTO.

??Thedefaultvalueof15yieldsahypotheticaltimeoutof924.6

??secondsandisalowerboundfortheeffectivetimeout.

??TCPwilleffectivelytimeoutatthefirstRTOwhichexceedsthehypotheticaltimeout.

??RFC1122recommendsatleast100secondsforthetimeout,

??whichcorrespondstoavalueofatleast8.

虽然说文档上默认值的建议是8,但是大多数的Linux发行版上其默认值都是0。更多详情,就自己看RFC和Linux源码吧。

有了这个参数保底,我们知道,即便是ACK永远不来,FIN_WAIT1状态也不会一直持续下去的,这有效避免了有针对性截获ACK或者不发送ACK而导致的DDoS,退一万步讲,即便是没有DDoS,这种做法也具有资源利用率的容错性,使得资源使用更加高效。

如果主动断开端调用了close关掉了进程,它会进入FIN_WAIT1状态,此时如果它再也收不到ACK,无论是针对pending在发送缓冲的数据还是FIN,它都会尝试重新发送,在收到ACK前会尝试N次退避,该N由tcp_orphan_retries参数控制。

接下来,我们来看一个更加复杂一点的问题,还是先从实验说起。

实验2:模拟对端TCP不收数据,接收窗口憋死

#模拟小接收缓存,使得憋住接收窗口更加容易sysctl-wnet.ipv4.tcp_rmem="163232"nc-l-p1234

host2上完成以下命令:

cat/dev/zero|nc1.1.1.11234sleep5#稍微等一下killallnc

此时,我们发现host2的TCP连接进入了FIN_WAIT1状态。然而抓包看的话,数据传输依然在进行:

n05:15:51.674630IP1.1.1.2.39318>1.1.1.1.1234:Flags[P.],seq305:321,ack1,win5840,options[nop,nop,TSval1210945ecr238593370],length1605:15:51.674690IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack321,win0,options[nop,nop,TSval238593471ecr1210945],length005:15:51.674759IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack321,win16,options[nop,nop,TSval238593471ecr1210945],length005:15:51.777774IP1.1.1.2.39318>1.1.1.1.1234:Flags[P.],seq321:325,ack1,win5840,options[nop,nop,TSval1211048ecr238593471],length405:15:51.777874IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack325,win16,options[nop,nop,TSval238593497ecr1211048],length005:15:52.182918IP1.1.1.2.39318>1.1.1.1.1234:Flags[P.],seq325:341,ack1,win5840,options[nop,nop,TSval1211453ecr238593497],length1605:15:52.182970IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack341,win0,options[nop,nop,TSval238593599ecr1211453],length005:15:52.183055IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack341,win16,options[nop,nop,TSval238593599ecr1211453],length005:15:52.592759IP1.1.1.2.39318>1.1.1.1.1234:Flags[P.],seq341:357,ack1,win5840,options[nop,nop,TSval1211863ecr238593599],length1605:15:52.592813IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack357,win0,options[nop,nop,TSval238593701ecr1211863],length005:15:52.592871IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack357,win16,options[nop,nop,TSval238593701ecr1211863],length005:15:52.695160IP1.1.1.2.39318>1.1.1.1.1234:Flags[P.],seq357:361,ack1,win5840,options[nop,nop,TSval1211965ecr238593701],length405:15:52.695276IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack361,win16,options[nop,nop,TSval238593727ecr1211965],length005:15:53.099612IP1.1.1.2.39318>1.1.1.1.1234:Flags[P.],seq361:377,ack1,win5840,options[nop,nop,TSval1212370ecr238593727],length1605:15:53.099641IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack377,win0,options[nop,nop,TSval238593828ecr1212370],length005:15:53.099671IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack377,win16,options[nop,nop,TSval238593828ecr1212370],length005:15:53.505028IP1.1.1.2.39318>1.1.1.1.1234:Flags[P.],seq377:393,ack1,win5840,options[nop,nop,TSval1212775ecr238593828],length1605:15:53.505081IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack393,win0,options[nop,nop,TSval238593929ecr1212775],length005:15:53.505138IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack393,win16,options[nop,nop,TSval238593929ecr1212775],length005:15:53.605923IP1.1.1.2.39318>1.1.1.1.1234:Flags[P.],seq393:397,ack1,win5840,options[nop,nop,TSval1212876ecr238593929],length4

这是显然的,这是因为收发两端巨大的缓存大小差异造成的,即便是host2发送端进程退出了,在退出前已经有大量数据pending到了TCP的发送缓冲区里面而脱离已经被销毁的进程了,FIN包当然是排在了缓冲区的末尾了。

TCP的状态机运行在缓存的上层,即只要把FIN包pending排队,就切换到了FIN_WAIT1,而不是说实际发送了FIN包才切换。

因此,我们可有的等了,数据传输依然在正常有序进行,针对小包的ACK源源不断从host1回来,这进一步促进host2发送未竟的数据包,直到所有缓冲区的数据全部发送完毕…

不管怎样,总是有个头儿,只要有结束,就不需要担心。我们可以简单得出一个结论:

如果主动断开端调用了close关掉了进程,它会进入FIN_WAIT1状态,如果接收端的接收窗口呈现打开状态,此时它的TCP发送队列中的数据包还是会像正常一样发往接收端,直到发送完,最后发送FIN包,收到FIN包ACK后进入FIN_WAIT2。

现在,我们进行实验的下一步,把host1上的接收进程nc的接收逻辑彻底憋死。很简单,host1上执行下面的命令即可:

killall-STOPnc

进程并没有退出,只是暂停了,nc进程上下文的recv不再执行,然而软中断上下文的TCP协议的处理依然在进行。

这个时候,抓包就会发现只剩下指数时间退避的零窗口探测包了:

#注意观察探测包发送时间的间隔05:15:56.444570IP1.1.1.2.39318>1.1.1.1.1234:Flags[.],ack1,win5840,options[nop,nop,TSval1215715ecr238594487],length005:15:56.444602IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack465,win0,options[nop,nop,TSval238594664ecr1214601],length005:15:57.757217IP1.1.1.2.39318>1.1.1.1.1234:Flags[.],ack1,win5840,options[nop,nop,TSval1217027ecr238594664],length005:15:57.757248IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack465,win0,options[nop,nop,TSval238594992ecr1214601],length005:16:00.283259IP1.1.1.2.39318>1.1.1.1.1234:Flags[.],ack1,win5840,options[nop,nop,TSval1219552ecr238594992],length005:16:00.283483IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack465,win0,options[nop,nop,TSval238595624ecr1214601],length005:16:05.234277IP1.1.1.2.39318>1.1.1.1.1234:Flags[.],ack1,win5840,options[nop,nop,TSval1224503ecr238595624],length005:16:05.234305IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack465,win0,options[nop,nop,TSval238596861ecr1214601],length005:16:15.032486IP1.1.1.2.39318>1.1.1.1.1234:Flags[.],ack1,win5840,options[nop,nop,TSval1234301ecr238596861],length005:16:15.032532IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack465,win0,options[nop,nop,TSval238599311ecr1214601],length005:16:34.629137IP1.1.1.2.39318>1.1.1.1.1234:Flags[.],ack1,win5840,options[nop,nop,TSval1253794ecr238599311],length005:16:34.629164IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack465,win0,options[nop,nop,TSval238604210ecr1214601],length005:17:13.757815IP1.1.1.2.39318>1.1.1.1.1234:Flags[.],ack1,win5840,options[nop,nop,TSval1292784ecr238604210],length005:17:13.757863IP1.1.1.1.1234>1.1.1.2.39318:Flags[.],ack465,win0,options[nop,nop,TSval238613992ecr1214601],length0

这个实验的现象和实验1的现象,仅有一个区别,那就是实验1是阻塞了ACK,而本实验则是FIN根本就还没有发送出去就进入了FIN_WAIT1,且针对RTO指数时间退避发送的零窗口探测的ACK持续到来,简单总结就是:

实验1没有ACK到来,实验2有ACK到来。

在实验结果之前,我们来看一段摘录,来自RFC1122:https://tools.ietf.org/html/rfc1122#page-92

4.2.2.17ProbingZeroWindows:RFC-793Section3.7,page42

??Probingofzero(offered)windowsMUSTbesupported.

??ATCPMAYkeepitsofferedreceivewindowclosed

??indefinitely.AslongasthereceivingTCPcontinuesto

??sendacknowledgmentsinresponsetotheprobesegments,the

??sendingTCPMUSTallowtheconnectiontostayopen.

??ItisextremelyimportanttorememberthatACK

??(acknowledgment)segmentsthatcontainnodataarenot

??reliablytransmittedbyTCP.Ifzerowindowprobingis

??notsupported,aconnectionmayhangforeverwhenan

??ACKsegmentthatre-opensthewindowislost.

??Thedelayinopeningazerowindowgenerallyoccurs

??whenthereceivingapplicationstopstakingdatafrom

??itsTCP.Forexample,consideraprinterdaemon

??application,stoppedbecausetheprinterranoutof

只要有ACK到来,连接就要保持,这会带来什么问题呢?确实会带来问题,但是在正视这些问题之前,Linux内核协议栈的实现者,也保持了缄默,我们来看一段实验主机host1和host2所用的标准内核主线版本3.10的内核源码,来自tcp_probe_timer函数内部的注释以及一小段代码:

n/**WARNING*RFC1122forbidsthis**Itdoesn'tAFAIK,becausewekilltheretransmittimer-AK**FIXME:Weoughtnottodoit,Solaris2.5actuallyhasfixing*thisbehaviourinSolarisdownasabugfix.[AC]**Letmetoexplain.icsk_probes_outiszeroedbyincomingACKs*eveniftheyadvertisezerowindow.Hence,connectioniskilledonly*ifwereceivednoACKsfornormalconnectiontimeout.Itisnotkilled*onlybecausewindowstayszeroforsometime,windowmaybezero*untilarmageddonandevenlater.Weareinfullaccordance*withRFCs,onlyprobetimercombinesbothretransmissiontimeout*andprobetimeoutinonebottle.--ANK*/...max_probes=sysctl_tcp_retries2;nif(sock_flag(sk,SOCK_DEAD)){//如果是orphan连接的话constintalive=((icsk->icsk_rto<<icsk->icsk_backoff)<TCP_RTO_MAX);//即获取tcp_orphan_retries参数,有微调,请详审。本实验参数默认值取0!max_probes=tcp_orphan_retries(sk,alive);nif(tcp_out_of_resources(sk,alive||icsk->icsk_probes_out<=max_probes))return;}//只有在icsk_probes_out,即未应答的probe次数超过探测最大容忍次数后,才会出错清理连接。if(icsk->icsk_probes_out>max_probes){tcp_write_err(sk);}else{/*Onlysendanotherprobeifwedidn'tclosethingsup.*/tcp_send_probe0(sk);}

是的,从上面那一段注释,我们看出了抱怨,一个FIN_WAIT1的连接可能会等到世界终结日之后,然而我们却只能“infullaccordancewithRFCs”!

这也许暗示了某种魔咒般的结果,即FIN_WAIT1将会一直持续到终结世界的大决战之日。然而非也,你会发现大概在发送了9个零窗口探测包之后,连接就消失了。netstat-st的结果中,呈现:

1connectionsabortedduetotimeout

看来想制造点事端,并非想象般容易!

如上所述,我展示了标准主线的Linux3.10内核的tcp_probe_timer函数,现在的问题是,为什么下面的条件被满足了呢?

if(icsk->icsk_probes_out>max_probes)

只有当这个条件被满足,tcp_write_err才会被调用,进而:

tcp_done(sk);//递增计数,即netstat-st中的那条“1connectionsabortedduetotimeout”NET_INC_STATS_BH(sock_net(sk),LINUX_MIB_TCPABORTONTIMEOUT);

按照注释和代码的确认,只要收到ACK,icsk_probes_out字段就将被清零,这是很明确的啊,我们在tcp_ack函数中便可看到无条件清零icsk_probes_out的动作:

staticinttcp_ack(structsock*sk,conststructsk_buff*skb,intflag){...sk->sk_err_soft=0;icsk->icsk_probes_out=0;tp->rcv_tstamp=tcp_time_stamp;...}

从代码上看,只要零窗口探测持续发送,不管退避到多久(最大TCP_RTO_MAX),只要对端会有ACK回来,icsk_probes_out就会被清零,上述的条件就不会被满足,连接就会一直在FIN_WAIT1状态,而从我们抓包看,确实是零窗口探测有去必有回的!

预期会永远僵在FIN_WAIT1状态的连接在一段时间后竟然销毁了。没有符合预期,到底发生了呢?

如果我们看高版本4.14版的Linux内核,同样是tcp_probe_timer函数,我们会看到一些不一样的代码和注释:

nstaticvoidtcp_probe_timer(structsock*sk){.../*RFC11224.2.2.17requiresthesendertostayopenindefinitelyas*longasthereceivercontinuestorespondprobes.Wesupportthisby*defaultandreseticsk_probes_outwithincomingACKs.Butifthe*socketisorphanedortheuserspecifiesTCP_USER_TIMEOUT,we*killthesocketwhentheretrycountandthetimeexceedsthe*correspondingsystemlimit.Wealsoimplementsimilarpolicywhen*weuseRTOtoprobewindowintcp_retransmit_timer().*/start_ts=tcp_skb_timestamp(tcp_send_head(sk));if(!start_ts)tcp_send_head(sk)->skb_mstamp=tp->tcp_mstamp;elseif(icsk->icsk_user_timeout&&(s32)(tcp_time_stamp(tp)-start_ts)>jiffies_to_msecs(icsk->icsk_user_timeout))gotoabort;nmax_probes=sock_net(sk)->ipv4.sysctl_tcp_retries2;if(sock_flag(sk,SOCK_DEAD)){constboolalive=inet_csk_rto_backoff(icsk,TCP_RTO_MAX)<TCP_RTO_MAX;nmax_probes=tcp_orphan_retries(sk,alive);//如果处在FIN_WAIT1的连接持续时间超过了TCP_RTO_MAX(这是前提)//如果退避发送探测的次数已经超过了配置参数指定的次数(这是附加条件)if(!alive&&icsk->icsk_backoff>=max_probes)gotoabort;//注意这个goto!直接销毁连接。if(tcp_out_of_resources(sk,true))return;}nif(icsk->icsk_probes_out>max_probes){abort:tcp_write_err(sk);}else{/*Onlysendanotherprobeifwedidn'tclosethingsup.*/tcp_send_probe0(sk);}}

我们来看这段代码的注释,RFC1122的要求:

RFC11224.2.2.17requiresthesendertostayopenindefinitelyas

longasthereceivercontinuestorespondprobes.Wesupportthisby

defaultandreseticsk_probes_outwithincomingACKs.

然后我们接着看这段注释,有一个But转折:

ButifthesocketisorphanedortheuserspecifiesTCP_USER_TIMEOUT,we

killthesocketwhentheretrycountandthetimeexceedsthecorrespondingsystemlimit.

看起来,这段注释是符合我们实验的结论的!然而我们实验的是3.10内核,而这个却是4.X的内核啊!即Linux在高版本内核上确实进行了优化,这是针对资源利用的优化,并且避免了有针对性的DDoS。

*我们实验所使用的内核版本不是社区主线版本,而是Redhat的版本!***Redhat显然会事先回移上游的patch,我们来确认一下我们所所用的实验版本3.10.0-862.2.3.el7.x86_64的tcp_probe_timer的源码。

为此,我们到下面的地址去下载Redhat(Centos…)专门的源码,我们看看它和社区同版本源码是不是在关于probe处理上有所不同:

Indexof/7.5.1804/updates/Source/SPackages

rpm2cpio../kernel-3.10.0-862.2.3.el7.src.rpm|cpio-idmvxzlinux-3.10.0-862.2.3.el7.tar.xz-dtarxvflinux-3.10.0-862.2.3.el7.tar

查看net/ipv4/tcp_timer.c文件,找到tcp_probe_timer函数:

看来是Redhat移植了4.X的patch,导致了源码的逻辑和社区版本的出现差异,这也就解释了实验现象!

那么这个针对orphanconnection的patch最初是来自何方呢?我们不得不去patchwork去溯源,以便得到更深入的Why。

在maillist,我找到了下面的链接:

http://lists.openwall.net/netdev/2014/09/23/8

Date:Mon,22Sep201420:52:13-0700

From:YuchungChengycheng@...gle.com

Cc:edumazet@…gle.com,andrey.dmitrov@…etlabs.ru,

??ncardwell@…gle.com,netdev@…r.kernel.org,

??YuchungChengycheng@...gle.com

Subject:[PATCHnet-next]tcp:abortorphansocketsstallingonzerowindowprobes

nCurrentlywehavetwodifferentpoliciesfororphansocketsthatrepeatedlystallonzerowindowACKs.IfasocketgetsazerowindowACKwhenitistransmittingdata,theRTOisusedtoprobethewindow.Thesocketisabortedafterroughlytcp_orphan_retries()retries(asintcp_write_timeout())..ButifthesocketwasidlewhenitreceivedthezerowindowACK,andlaterwantstosendmoredata,weusetheprobetimertoprobethewindow.IfthereceiveralwaysreturnszerowindowACKs,icsk_probeskeepsgettingresetintcp_ack()andtheorphansocketcanstallforeveruntilthesystemreachestheorphanlimit(ascommentedintcp_probe_timer()).ThisopensupasimpleattacktocreatelotsofhangingorphansocketstoburnthememoryandtheCPU,asdemonstratedintherecentnetdevpost“TCPconnectionwillhanginFIN_WAIT1afterclosingifzerowindowisadvertised.”TCPconnectionwillhanginFIN_WAIT1afterclosingifzerowindowisadvertisedn该链接最后面给出了patch:n...+max_probes=sysctl_tcp_retries2;if(sock_flag(sk,SOCK_DEAD)){constintalive=inet_csk_rto_backoff(icsk,TCP_RTO_MAX)<TCP_RTO_MAX;nmax_probes=tcp_orphan_retries(sk,alive);-+if(!alive&&icsk->icsk_backoff>=max_probes)+gotoabort;if(tcp_out_of_resources(sk,alive||icsk->icsk_probes_out<=max_probes))return;}nif(icsk->icsk_probes_out>max_probes){-tcp_write_err(sk);+abort:tcp_write_err(sk);}else{...

简单说一下这个patch的意义。

在实验2中,我用kill-STOP信号故意憋死了nc接收进程,以重现现象,然而事实上在现实中,存在下面两种不太友善情况:

接收端进程出现异常,或者接收端内核存在缺陷,导致进程挂死而软中断上下文的协议栈处理正常运行;

接收端就是一个恶意的DDoS进程,故意不接收数据以诱导发送端在FIN_WAIT2状态(甚至ESTAB状态)发送数据不成后发送零窗口探测而不休止。

无论哪种情况,最主动断开的发送端来讲,其后果都是消耗大量的资源,而orphan连接则占着茅坑不拉屎。这比较悲哀。

如果主动断开端调用了close关掉了进程,它会进入FIN_WAIT1状态,如果接收端的接收窗口呈现关闭状态(零窗口),此时它会不断发送零窗口探测包。发送多少次呢?有两种实现:

低版本内核(至少社区3.10及以下):永久尝试,如果探测ACK每次都返回,则没完没了。

高版本内核(至少社区4.6及以上):限制尝试tcp_orphan_retries次,不管是否收到探测ACK。

当然,其实还有关于非探测包的重传限制,比如关于TCP_USER_TIMEOUT这个socketoption的限制:

elseif(icsk->icsk_user_timeout&&(s32)(tcp_time_stamp(tp)-start_ts)>jiffies_to_msecs(icsk->icsk_user_timeout))gotoabort;

包括关于Keepalive的点点滴滴,本文就不多说了。

在此,先有个必要的总结。我老是说在学习网络协议的时候读码无益并不是说不要去阅读解析Linux内核源码,而是一定要先有实验设计的能力重现问题,然后再去核对RFC或者其它的协议标准,最后再去核对源码到底是怎么实现的,这样才能一气呵成。否则将有可能陷入深渊。

以本文为例,我假设你手头有3.10的源码,当你面对“FIN_WAIT1状态的TCP连接在持续退避的零窗口探测期间并不会如预期那般永久持续下去”这个问题的时候,你读源码是没有任何用的,因为这个时候你只能静静地看着那些代码,然后纠结自己是不是哪里理解错了,很多人甚至很难能想到去对比不同版本的代码,因为版本太多了。

源码只是一种实现的方式,而已,真正重要的是协议的标准以及标准是实现的建议,此外,各个发行版厂商完全有自主的权力对社区源码做任何的定制和重构,不光是Redhat,即便你去看OpenWRT的代码,也是一样,你会发现很多不一样的东西。

我并不赞同几乎每一个程序员都拥护的那种任何情况下源码至上,thewholeworldischeap,showmethecode的观点,当一个逻辑流程摆在那里没有源码的时候,当然那绝对是源码至上,否则就是纸上谈兵,逻辑至少要跑起来,而只有源码编译后才能跑起来,流程图和设计图是无法运行的,这个时候,你需要放弃讨论,潜心编码。然而,当一个网络协议已经被以各种方式实现了而你只是为了排查一个问题或者确认一个逻辑的时候,代码就退居二三线了,这时候,请“showmethestandard!”。

本文原本是想解释完FIN_WAIT1能持续多久就结束的,但是这样显得有点遗憾,因为我想本文的这个FIN_WAIT1的论题可以引出一个更大的论题,如果不继续说一说,那便是不负责任的。

是什么的?嗯,是TCP假连接的问题。那么何谓TCP假连接?

所谓的TCP假连接就是TCP的一端已经逃逸出了TCP状态机,而另一端却不知道的连接。

我们再看完美的TCP标准RFC793上的TCP状态图:

除了TIME_WAIT到CLOSED这唯一的出口,你是找不到其它出口的,也就是说,一个TCP端一旦发起了建立连接请求,暂不考虑同时打开同时关闭的情况,就一定要到其中一方的TIME_WAIT超时而结束。

然而,TCP的缺陷在于,TCP是一个端到端的协议,在协议层面上所有的端到端协议是需要底层的传送协议作为其支撑的,一旦底层永久崩坏,端到端协议将会面临状态机僵住的场景,而状态机僵住意味着对资源的永久消耗,因为连接再也释放不掉了!

随便举一个例子,在两端ESTAB状态的时候,把IP动态路由协议停掉并把把网线剪断,那么TCP两端将永远处在ESTAB状态,直到机器重启。为了解决这个问题,TCP引入了Keepalive机制,一旦超过一定时间没有互通有无,那么就会主动销毁这个连接,事实上,按照纯粹的TCP状态机而言,Keepalive机制是一种对TCP协议的污染。

是不是Keepalive就能完全避免假连接,死连接存在了呢?非也,Keepalive只是一种用户态按照自己的业务逻辑去检测并避免假连接的手段,而我们仔细观察TCP状态机,很多的步骤远不是用户态进程可是touch的,比如本文讲的FIN_WAIT1,一旦连接成为orphan的,将没有任何进程与之关联,虽然用户态设置的Keepalive也可以继续起作用,但万一用户态没有设置Keepalive呢??这时怎么办?

[root@localhost~]#sysctl-a|grepretriesnet.ipv4.tcp_orphan_retries=0net.ipv4.tcp_retries1=3net.ipv4.tcp_retries2=15net.ipv4.tcp_syn_retries=6net.ipv4.tcp_synack_retries=5net.ipv6.idgen_retries=3

嗯,这些就是避免TCP协议本身的状态机转换僵死所引入的控制层Keepalive机制,详细情况就自己去查阅Linux内核文档吧。

在具体实现上,防止状态机僵死的方法分为两类:

ESTABLISHED防止僵死的方法:使用用户进程设置的Keepalive机制

非ESTABLISHED防止僵死的方法:使用各种retries内核参数设置的timeout机制

END,本文到此结束,如果可以帮助到大家,还望关注本站哦!