关于Linux TCP接收缓存以及接收窗口的一个细节解析
关于TCP的接收缓存以及通告窗口,一般而言懂TCP的都能说出个大概,但是涉及到细节的话可能理解就不那么深入了。
问题:
明明在接收端有8192字节的接收缓存,为什么收了不到8000字节的数据就ZeroWindow了呢?
深入接收缓存管理机制的过程中,你可能会在代码的注释中看到这样的分割,将接收缓存分割成了所谓的networkbuffer和applicationbuffer,具体参见__tcp_grow_window的注释:
1.通告窗口与接收缓存在TCP的配置中,有一个接收缓存的概念,另外在TCP滑动窗口机制中,还有一个接收窗口的概念,毋庸置疑,接收窗口所使用的内存必须分配自接收缓存,因此二者是包容的关系。
但这不是重点,重点是:接收窗口无法完全占完接收缓存的内存,即接收缓存的内存并不能完全用于接收窗口!Why?
这是因为接收窗口是TCP层的概念,仅仅描述TCP载荷,然而这个载荷之所以可以收到,必须使用一个叫做数据包的载体,在Linux中就是skb,另外为了让协议运行,必须为载荷封装TCP头,IP头,以太头等等。
我用下图来解释接收缓存以及其和TCP数据包的关系:
【注意,当我说“TCP数据包”的时候,我的意思是这是一个带有以太头的完整数据包,当我说“TCP数据段”的时候,我想表达的则是我并不关系IP层及以下的东西。】
图示的最后,我特意标红了一个“极力要避免”的警示,确实,如果直接把可用的窗口都通告出去了,且发送端并不按照满MSS发送的话,是存在溢出风险的,这要怎么解决呢?
需要C/C++Linux服务器架构师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
附:如何确定通告窗口可以使用的接收缓存
在代码中,我们注意一个函数tcp_fixup_rcvbuf:
staticvoidtcp_fixup_rcvbuf(structsock*sk){u32mss=tcp_sk(sk)-advmss;u32icwnd=TCP_DEFAULT_INIT_RCVWND;intrcvmem;/*Limitto10segmentsifmss=1460,*or14600/msssegments,withaminimumoftwosegments.*/if(mss1460)icwnd=max_t(u32,(1460*TCP_DEFAULT_INIT_RCVWND)/mss,2);rcvmem=SKB_TRUESIZE(mss+MAX_TCP_HEADER);//将rcvbuf按比例缩放到其(n-1)/n可以完全容纳TCP纯载荷的程度,n由系统参数_adv_win_scale来确定。while(tcp_win_from_space(rcvmem)mss)rcvmem+=128;rcvmem*=icwnd;if(sk-sk_rcvbufrcvmem)sk-sk_rcvbuf=min(rcvmem,sysctl_tcp_rmem[2]);}以上函数确定了接收缓存,其中有3个要点:
1).初始通告窗口的大小
默认是10个MSS满1460字节的段,这个数值10来自google的测试,与拥塞窗口的初始值一致,然而由于MSS各不同,其会按照1460/mss的比例进行缩放来适配经验值10。
2).TCP载体开销的最小值128
展开宏SKB_TRUESIZE会发现其最小值就是128,这对通告窗口慢启动过程定义了一个安全下界,载荷小于128字节的TCP数据段将不会增加通告的上限大小。
3).参数tcp_adv_win_scale的含义
对比我上面的图示,上述代码的注释,我们知道tcp_adv_win_scale就是控制“载荷/载体”比例的,我们看一下其KernelDOC
tcp_adv_win_scale-INTEGERCountbufferingoverheadasbytes/2^tcp_adv_win_scale(iftcp_adv_win_scale0)orbytes-bytes/2^(-tcp_adv_win_scale),ifitis=0.Possiblevaluesare[-31,31],:1
这个参数曾经的default值是2而不是1,这意味着以往TCP的载荷占比由3/4变成了1/2,好像是开销更大了,这是为什么呢?以下是该Change的patch描述:
单纯从TCP载荷比来讲,开销的增加意味着效率的降低,然而注意到这部分开销的增加并非网络协议头所为,而是skb_shared_info结构体被计入开销以及skb结构体等系统载体的膨胀所导致:
我们分别来看一下2.6.32和3.10两个版本的sk_buff的大小,怎么看呢?不要想着写一个模块然后打印sizeof,直接用slabtop去看即可,里面信息很足。
a).2.6.32版本的sk_buff大小
slabtop的结果是:
skbuff_head_cache550615256151:tunables120608:slabdata41410
我们看到其大小是256字节。
b).3.10版本的sk_buff大小
slabtop的结果是:
skbuff_head_cache36753675320252:tunables000:slabdata1471470
我们看到其大小是320字节。
差别并不是太大!这不是主要因素,但确实会有所影响。
除了skb的膨胀之外,系统中还有别的膨胀,比如为了效率的“对齐开销”,但更大的开销增加是skb_shared_info结构体的计入(个人认为以前开销中不计入skb_shared_info结构体是错误的)等,最终导致新版本(以3.10+为例)的内核计算TRUESIZE的方法改变:
packet_size=mss+MAX_TCP_HEADER+SKB_DATA_ALIGN(sizeof(structsk_buff))+SKB_DATA_ALIGN(sizeof(structskb_shared_info)))
然而以往的老内核(以2.6.32为例),其开销的计算是非常鲁莽的,少了很多东西:
packet_size=mss+MAX_TCP_HEADER+16+sizeof(structsk_buff);
虽然这种开销的膨胀在TCP层面几乎看不到什么收益(反而付出了代价,你不得不配置更大的rcvbuf),然而skb等并不单单服务于TCP,这种膨胀的收益可能被调度,中断,IP路由,负载均衡等机制获取了,记住两点即可:首先,Linux内核各个子系统是一个整体,其次,内存越来越便宜而时间一去不复返,空间换时间,划得来!
在谈如何规避溢出风险之前,我必须先说一下这个风险并不是常在的,如果应用程序非常迅速的读取TCP数据并释放skb,那么几乎不会有什么风险,问题在于应用程序并不受TCP层的控制,所以我说的“溢出风险”指的是一种合理但很极端的情况,那就是应用程序在TCP层收满一窗数据前都不会去读取数据,这虽然很极端,但是符合TCP滑动窗口的规范:通告给发送端的窗口表示发送端可以一次性发送这么多的数据,至于应用程序什么时候来读取,滑动窗口机制并不控制。
我们知道,TCP拥塞控制通过慢启动来规避突发造成的网络缓存溢出的的风险,事实上拥塞控制也是一种流量控制,作为标准的方案,慢启动几乎是规避溢出的标配方案!这很好理解,慢启动的含义是“快速地从起点试探到稳态”,并非其字面含义所说的“慢慢地启动”,之所以有“慢”字是因为与进入稳定状态后相比,它的起点是低的。这和开车是一样的道理,静止的汽车从踩下油门开始一直到匀速,是一个快速加速的过程,达到100km/h的时间也是一个重要的指标,当然,很多情况下是越小越好!
所以说,通告窗口也是采用慢启动方式逐步张开的。
2.0、收到极小载荷的TCP数据包时的慢启动
比如说收到了一个只包含1个字节载荷的数据包时,此时仅仅skb,协议头等开销就会超过几百字节,通告窗口增加是非常危险的。LinuxTCP实现中,将128字节定为下限,凡是收到小于128字节载荷的数据包,接收一大窗的数据非常有可能造成缓存溢出,因此不执行慢启动。
2.1、收到满MSS的TCP数据包时的慢启动
如果能保证发送端一直发送满MSS长度的TCP数据包,那么接收缓存是不会溢出的,因为整个通告窗口可以使用的内存就是通过这个满MSS长度和接收缓存按照比例缩放而生成的,但是谁也不能保证发送端会一直发送满MSS长度的TCP数据包,所以就不能允许发送端一下子发送所有可用的窗口缓存那么大的数据量,因此慢启动是必须的。
收到满MSS长度的数据或者大于MSS长度的数据,窗口可以毫无压力地增加2个MSS大小。
2.2、收到非满MSS
这里的情况比较复杂了。虽然收到数据长度比MSS小的TCP数据包有缓存溢出的风险,但是受限于当前的通告窗口上限(由于慢启动的功劳)小于整个可用的通告窗口内存,这种情况下即便是发送一整窗的数据,也不会造成整个接收缓存的溢出。这就是说某些时候,当当前的接收窗口上限未达到整个可用的窗口缓存时,长度小于MSS的TCP数据包的额外高于(n-1)/n比例的开销可以暂时“借用”剩余的窗口可用的缓存,只要不会造成溢出,管它是不是借用,都是可以接受的。
如此复杂的情况,我画了一个稍微复杂点的图来展示,以节省文字篇幅:
看懂了上图之后,我来补充一个动态过程,如果持续收到小包的情况下,会怎样?
如果持续收到小于MSS的小包,假设长度都相等,那么从慢启动开始,通告窗口的最大值,即rcv_ssthresh将会在每收到一个数据包后从初始值开始按照2倍数据段长度的增量持续增长,直到其达到小于所有可用通告窗口内存的某个值停止再增长,增长到该值的位置时,一整窗的数据连同其开销将会完全占满整个rcvbuf。
为什么拥塞窗口的慢启动是直接增加的拥塞窗口的值,通告窗口的慢启动并不直接增加通告窗口而是增加的通告窗口的上限呢?
这是因为通告窗口的实际值并非单单由接收缓存溢出检测这么一个因素控制,这个因素事实上反而不是主导因素,主导因素是应用程序是不是即时腾出了接收缓存。我们从代码中如何确定通告窗口的逻辑中可以看出:
最后,总结一幅图,将上面谈到的所有这些概念与Linux内核协议栈TCP实现关联起来:
推荐阅读
-
为注入电子元器件分销业务,聚隆科技拟最低15亿收购联合创泰
1月29日,资本邦了解到,A股公司聚隆科技(300475.SZ)签署《重大资产重组框架协议》。聚隆科技于2021年1月28日与英唐创泰、黄泽伟、彭红签订了《重大资产重组框架协议》。公司正在筹划拟以现金方式购买联合创泰100%股权,标的资产初步交易作价为15.8亿元至17.8亿元之间。根据初步研究和测...
-
上海断桥铝门窗系统的彩色玻璃选择和应用技巧 门窗工厂选择干货
断桥铝门窗系统通常是由断桥铝型材和玻璃组成的。彩色玻璃是一种广泛使用的玻璃类型,其颜色可以通过在制造过程中添加金属氧化物或其他化学物质来实现。彩色玻璃可以在门窗系统中起到装饰和隐私保护的作用,同时也能提供良好的采光效果。在选择和应用彩色玻璃时,有几个关键因素需要考虑。一、颜色选择彩色玻璃可以有多种颜...
-
生捷科技发布新一代超高密度测序芯片
日前,由生捷科技(杭州)有限公司和浙江清华长三角研究院共同研制的呼吸道疾病多病原体测序芯片正式对外发布,该芯片可以同时对包括新型冠状病毒在内的100多种病毒进行高通量测序,实现精确检测新型冠状病毒和其他常见呼吸道病原体。业内普遍认为,该产品在目前巩固国内抗疫成果、支援全球抗击新型冠状病毒的进程中将发...
-
昊诚锂电取得一次锂锰超薄电池及其制备方法专利,量产导入快速简便、超薄
金融界2024年4月8日消息,据国家知识产权局公告,武汉昊诚锂电科技股份有限公司取得一项名为“一次锂锰超薄电池及其制备方法“,授权公告号CN109935745B,申请日期为2019年3月。专利摘要显示,本发明公开了一种一次锂锰超薄电池。它包括正极,负极和铝塑膜,所述铝塑膜内表面上对称设有正极坑和负极...