TCP系列32—窗口管理&流控—6、TCP zero windows和persist timer
一、简介
我们之前介绍过,TCP报文中的window size表示发出这个报文的一端准备多少bytes的数据,当TCP的一端一直接收数据,但是应用层没有及时读取的话,数据一直在TCP模块中缓存,最终受限于接收缓存的大小,window size会变为0,此时我们称呼这个接收窗口为零窗(zero window),对端也不能在发送更多的数据。如果随后本端应用层从TCP接收缓存中读取了足够数据,TCP模块有了足够的新的接收缓存的时候,就会发送一个TCP报文,并带有一个有效非零的Window size来指示对端自己已经可以接收新数据了。这个带有有效Window size的报文我们称为窗口更新(window update)报文。窗口更新一般就是一个普通的ACK报文,并不会带有有效的数据(pure ACK),ACK报文不消耗系列号,如果发生丢失并不会进行重传。因此TCP需要处理window update消息丢失的场景。
如果窗口更新报文发生丢失,那么接收端(这里的接收端是指window update消息的发送端)会等待发送端发送新的数据,而发送端会等待接收window update消息来发送新的数据,这种场景下,两端互相等待对方,就会产生一种deadlock(还记得Nagle算法和延迟ACK同时生效的时候也会产生类似的deadlock吧)。为了阻止这种死锁一直等待下去,TCP的发送端会使用一个persist timer定时器来定时查询接收端的window size是否增长,每当这个定时器超时的时候,发送端就会发送window probes报文。接收端在接收到window probe消息的时候会提供一个带有window size的ACK报文。RFC1122建议初始window probe定时器定时时间为RTO,随后进行window probe 的时候应该进行指数回退,最大指数回退次数为tcp_retries2,如果此时还没收到有效的window size,则会一直进行window probe过程(我们之前通过示例介绍过RTO超时最后会释放连接,这个是与window probe的重要区别)。发送端。window probe报文中可以包含1byte的数据也可能不包含数据。当window probe包含数据的时候,接收端可以选择接收包含的数据也可以选择不接受包含的数据。关于persist timer,我们前面在介绍cork算法的时候就接触过了。
另外在下面的示例中我们将会看到,linux上发出的window probe消息是不带有有效数据的,而且window probe的系列号位于snd_nxt的前面,linux接收到这种报文的时候会认定这种报文为无效的系列号。对于这种类型的报文回复ACK时候受到参数tcp_invalid_ratelimit控制,这个参数控制了TCP对于这类系列号无效的报文的ACK回复速率,例如下面示例中我们设置tcp_invalid_ratelimit=1200,含义就是说linux对于这类无效报文的ACK回复间隔最小为1200ms,只要间隔大于1200ms,linux就会立即回复一个dup ACK报文,并不受我们之前介绍的延迟ACK策略的影响(延迟ACK一般是针对有效数据来说的)。
二、wireshark示例
1、综合示例
我们设置tcp_retries2=6,tcp_invalid_ratelimit=1200,通过socket选项SO_RCVBUF设置server端和client端的window size如下图所示,通过这个示例我们还会进一步说明一下之前介绍过的延迟ACK的处理。
No1-No3:首先client和server端通过三次握手建立TCP连接,其中server端的window size为3000bytes
接着client端执行一次write操作,一次write写入5000bytes的数据。
No4:client端内核在从用户空间读取数据前会先获取当前的发送MSS,可以从图中看到,server端的接收窗口大小为3000bytes,但是MSS为65495bytes,显然不能按照这个MSS来发送,当出现这种情况的时候,linux的发送端(即本例中client端)会取对端最大接收窗口的一半1500bytes为发送mss。选定MSS后,接着linux内核会从client端尝试以1500bytes为单位来复制应用程序的数据(共5000bytes)然后发送出去,No4即对应client发出的第一个数据包。
No5:server端在接收到client端的No4数据包的时候,会初始化quick ACK模式,此时client端的rcv_mss为1500,rcv_wnd为3000,rcv_wnd/(2*rcv_mss)=1,因此quick ACK计数器初始化为1,对No4报文执行quick ACK反馈No5后,quick ACK计数器变为0,关于延迟ACK的相关内容可以参考前面系列文章
No6:接着client端内核继续从应用层复制1500bytes的数据并发送出去,此时client端一共发出了3000bytes的数据,而server端应用层一直没有读取TCP模块接收的数据。可以看到wireshark提示TCP Window Full信息。
No7:No5数据包发出去后quick ACK计数器变为0,此时server端对No6数据包执行延迟ACK策略,定时时间为40ms。从wireshark可以看到No7与No6数据包实际间隔大约为38ms,这种定时误差问题是由于TCP模块的tick精度问题造成的,前面相关文章已经解释了,此处不再赘言。server端的3000bytes已经全部被占用了,此时server端只能回复Window size为0的ACK报文,通知client自己不再准备接收新数据了。可以看到wireshark提示了TCP zero Window提示信息。因为No7延迟了40ms回复ACK,所以当client收到这个报文的时候,client端的已经完全把应用层的数据复制到了内核中,之前一次write写入了5000bytes数据,已经发出了3000bytes的数据,此时client端内核中还剩余2000bytes的数据待发送。client端内核虽然在收到No7报文之前就已经准备好发送数据了,但是由于window size限制而没发送出去。此时收到No7的ACK后会再次尝试发送剩余的2000bytes的数据,但是同样由于window size限制而发送失败(如果client忽视window size的限制强制发送,server端会怎么办?我们后面文章在用示例来说明),发送失败后,linux会判断如果当前已经已经发出的还未收到ACK确认包的报文个数为0并且还有待发送数据的话就会启动persist timer定时器,定时时间为RTO(当前RTO大约为208ms)。
No8:上面设置的persist timer定时器超时后,强制发送No8报文,注意No8报文的seq实际上比No7报文的ack number小1,而且Len=0,发送完这个报文后设置persist timer定时器,定时时间为上一次定时时间的2倍(大约为416ms)。wireshark对于No8报文的提示为TCP Keep-Alive,实际上这个报文的功能并不是Keep-Alive的功能,后面文章我们会介绍TCP Keep-Alive的。
No9:接着我们看到server端对于No8报文立即回复了一个No9的ACK确认报文,这里起作用的并不是quick ACK模式,而是linux对于类似No8这种window probe报文会认为是无效的系列号,只要当前时间距离上次回复无效系列号报文的ACK确认包时间超过了tcp_invalid_ratelimit参数设置的时间,那么linux就会立即回复ACK确认报文,可以看到这个ACK报文window size仍然为0。
No10:No8设置的定时器超时后,发出No10的window probe报文,并设置persist timer定时时间为4*RTO。
可以看到server端收到No10报文后并没有立即回复ACK确认包,原因是No10和No9的间隔时间并没有超过1200ms的ACK发送间隔。
No11:persist timer再次超时,发出No11报文,并重新设置persist timer的定时时间为8*RTO
No12:server端在收到No11报文的时候,发现这个报文系列号无效,同时距离上一次回复无效系列号报文ACK确认包的时间(即No9的时间)已经超过了1200ms,因此立即回复ACK确认包。
No13-No18:这几个报文重复前面的指数回退过程,server端判断无效系列号报文的ACK间隔超过1200ms后立即回复ACK确认包。
No19:client在发送No19的window probe报文的时候发现,前面已经连续发送了No8、No10、No11、No13、No15、No17共6个window probe报文,已经达到了tcp_retries2的配置值,因此随后client端不在进行指数回退的过程,对persist timer定时器的定时间隔固定为2^6*RTO,大约为13.312ms。可以看到这里没有释放TCP连接,而在RTO重传指数回退过程中,当超过根据tcp_retries2计算的最大重传时间的时候就会释放TCP连接。
No20-No30:client端持续进行window probe过程,这个与上面处理类似,不再多说
No31:接着在No30之后server端应用程序完全读取出TCP中的3000bytes的缓存数据,server端发送window update消息给client端,通知对端可以发送新的数据
No32-No33:client端收到window update后,立即把剩余的2000byte分两个数据包发出
No34:server端收到No32报文的时候,发现距离上一次收到有效数据的时间超过了一个RTO,因此进入quick ACK模式,设置quick ACK计数器为rcv_wnd/(2*rcv_mss)=1,立即回复ACK确认包报文后,quick ACK计数器减1变为0
No35:server端在收到No33报文后,此时quick ACK计数器为0,进入延迟ACK处理,延迟ACK定时器超时后触发回复No35的ACK确认包。
补充说明:
1、MSS相对与发送窗口折半的限制处理,请参考tcp_bound_to_half_wnd
2、persist timer的定时器的初始启动__tcp_push_pending_frames,随后超时处理tcp_probe_timer
3、linux对于示例中window probe消息的处理以及与参数tcp_invalid_ratelimit的关系,参考tcp_validate_incoming和tcp_sequence