自TCP诞生以来就改变了网络通信的格局,而TCP协议随着网络基础设施的发展也在一路演进,形成了如今庞大复杂的TCP协议簇。如何深入理解TCP的设计理念以及几十年以来TCP协议的演进,有利于更好地了解网络编程。很多人不懂TCP,很多人了解并会用TCP但不知道其设计理念,但是这些才是网络编程的精粹所在。本文旨在从设计思路出发,更多的分析为什么要这么做而不是TCP是怎么做的,但是碍于自身水平所限,可能视野较为狭隘,疏漏错误之处请不吝指正。本文阅读的前提要求至少学习过《计算机网络》,看过《TCP/IP详解》及相关的RFC文档更佳。
当两台电脑被一根线连接起来的时候,网络就诞生了。随着几十年间的发展,网络变得越来越复杂。如同管理国家,企业一样,对于复杂到无法单点直接控制的情况,采取模块化以及分层的策略才是解决复杂问题的合理方法,有趣的是这也是Linux的一大设计哲学:复杂的问题可以通过抽象出中间层的方法分解为几个简单的问题。因此,我们有了网络分层。
常见的网络协议模型由两种:OSI标准七层模型,业界标准TCP/IP模型。二者其实表示的是一个意思,只不过对于我们开发者来说不需要特别细化的后三层,其结构如下图所示。
每层的核心作用可以概括如下:
在实际使用中,我们也是采取层层封装/拆封的方式传递数据,如下图所示
由此一来,每层各司其职,也可以各自进行扩展、优化、重构,从而构成了如今庞大复杂而又不失美感的网络协议模型。网络协议模型从下往上功能增加,需要实现的设备减少的同时设备的复杂性却在增加,这样保证了成本的最小化,至于性能则可以通过逐层逐步优化的方式慢慢改进,这也符合架构设计和重构的基本哲学。TCP协议就是这样的典型代表,实际上最开始的时候TCP并不考虑性能、效率、公平性,正是考虑了这些,TCP协议才复杂了起来。
正如其名字所展示的那样,TCP的作用是传输控制,也就是控制端到端的传输,那为何这种控制不在IP协议中实现呢?答案很简单,那就是这会增加IP协议的复杂性,而IP协议需要的就是简单。网络层往下是繁多的链路层协议,这些链路提供了截然不同且相差很远的语义,为了互联这些异构的网络,我们需要一个网络层协议起码要提供一些适配的功能,这种思想其实和通过虚拟文件系统抽象各种字符设备和块设备一样,所以必须简单,因为简单所以稳定而强大。这也是KISS原则的体现。
初级面试官常爱问TCP和UDP的区别,答案模板网上一搜一大把,往往问过答过也就过去了,但是你真的理解了其本质吗?
以上问题实质上可以说是同一个问题:如何理解TCP是端到端的协议。首先,从上图数据包的传递我们可以得出一个结论:中间传递过程不关心四层及以上,仅仅通过下三层完成多条路由,无论是TCP还是UDP都是一样的。下面我们来看看TCP的端到端特性。
这就是TCP端到端协议的本质:通过两端的数据结构、状态机、复杂的逻辑判断流程图对不可靠的链路和下层协议做到了可靠的传输保证。由于IP协议不提供保证,TCP也不能提供依赖于IP下层链路的这种保证,比如带宽,比如时延,这些都是链路层决定的,既然IP协议无法修补,TCP也不能,然而它却能修正始于IP层的一些“不可保证性质”,这些性质包括IP层的不可靠,IP层的不按顺序,IP层的无方向/无连接。
确切的说,TCP协议有两重身份
可以看出,不管从哪个角度,TCP都作为一个接口存在,作为网络协议,它和对端的TCP接口,实现TCP的控制逻辑,作为多路复用/解复用器,它和下层IP协议接口,实现协议栈的功能,而这正是分层网络协议模型的基本定义(两类接口,一类和下层接口,另一类和对等层接口)。
TCP被称之为可靠的传输协议,是因为其设计理念中强调了对可靠性的实现,我们可以归结为连接的可靠性以及数据报文传输的可靠性。
为了理解连接的可靠性,我们需要用到《TCP/IP详解》卷一中的经典TCP状态转移图。
何为可靠连接?直白的说就是我们可以知道连接是否建立,是否断开,表现为图中即从CLOSE
到ESTABLISHED
则表明连接建立,从ESTABLISHED
回到CLOSE
则连接断开。表现在协议里,则是三次握手和四次挥手。
这里又是初级面试官最爱问的问题之一,为什么是三次握手和四次挥手?答案其实很简单,所谓的握手挥手无非是通过一个SYN
和一个ACK
的来回确认彼此在线并初始化序列号。三次握手最重要的作用是传递了序列号,该序列号是可靠传输的保证,为此需要使双方均保证已经收到。而之所以建立之始可以只用三次,是因为初始两次握手并没有传递数据,所以可以将服务端发回客户端的ACK
和SYN
合并在一起发送。而连接断开的时候,如果当前还有数据传输,则该单向通道不可以立即关闭,因此只发送ACK
而没有FIN
,这才变成了三次握手和四次挥手。
上面说了三次握手和四次挥手,这里我们再来考虑一个情景:终止连接时的被动方发送了一个FIN
,主动方回复了的ACK
丢失,这会造成被动方重发FIN
,这个FIN可能会在互联网上存活MSL
时间(MSL,即报文最大存活时间,根据光速以及TTL等综合计算得到,详细算法可见维基和RFC文档)。如果没有TIME_WAIT
的话,假设连接1已经断开,然而其被动方最后重发的那个FIN
(或者FIN之前发送的任何TCP分段)还在网络上,然而连接2重用了连接1的5元组(源IP,目的IP,TCP,源端口,目的端口),连接1迟到的FIN到达了,这个FIN
将以比较低但是确实可能的概率终止掉连接2。
为何说是概率比较低呢?这涉及到一个匹配问题,迟到的FIN
分段的序列号必须落在连接2的一方的期望序列号范围之内。虽然这种巧合很少发生,但确实会发生,毕竟初始序列号是随机产生了。因此终止连接的主动方必须在接受了被动方FIN
且回复了ACK
之后,等待2*MSL
时间才能进入CLOSE
状态,之所以乘以2是因为最坏情况下,回复给被动方的ACK
在以最长路线(经历一个MSL
)丢包,再次收到被动方重发的FIN
也需要一个MSL
,合起来就是2倍MSL
时间。如果超过2MSL
也没有回复,则判断为被动方收到了该ACK
,如果是FIN
没有收到,那也不用担心上述的问题了,因为已经超过了一个数据包的存活时间了。
综上,TIME_WAIT状态的出现是因为
ACK
而重发FIN
ACK
传输可靠性主要表现为解决了IP层数据包的乱序和丢包问题,其实现简单的说就是给每个包加上序号,按照序号进行排序,如果有缺失则要求重发。但是由于端对端的协议我们只能控制两端而不能控制链路,因此存在以下问题
为了确保每个包都收到并有序,TCP设计了序列号。为了确保该序列号收到,TCP设计了ACK
机制。但是如果ACK
没有收到呢?为此TCP设计了超时机制。超时机制最大的问题在于如何判断对端是否收到了消息,即对端是否发送了ACK。为此我们需要想办法测算出正常情况下TCP发送一个数据包并收到ACK回复的时间,如果超过该时间还未收到ACK,则可认为是超时了。这就是RTT(Round Trip Time),也叫RTD(Round Trip Delay)。RTT的计算根据实际的协议会有着不同的定义方式,但是大致均采用滑动平均的方式过滤掉偶尔剧烈的波动,具体可以参考不同的RFC协议文档。实际我们使用的时候还会用到RTO(Retransmission TimeOut)。二者间动态变化关系可查看RFC6298,更多时候这些滑动平衡的系数来自于经验数值,并没有什么值得言道的地方。
很显然,对每一个TCP分段都生成一个计时器是最直接的方式:每个计时器在RTT时间后到期,如果没有收到确认则重传。然而这只是理论上的合理,对于大多数操作系统而言,这将带来巨大的内存开销和调度开销,因此采取每一个TCP连接单一计时器的设计则成了一个默认的选择。可是单一的计时器怎么管理如此多的发出去的TCP分段呢?又该如何来设计单一的计时器呢。
设计单一计时器有两个原则:1.每一个报文在长期收不到确认都必须可以超时;2.原则1中定义的长期不能和测量的RTT相隔太远。RFC2988定义的逻辑如下:
ACK
时,如果有数据在传输中,重新开启重传定时器。ACK
时,如果没有数据在传输中,则关闭重传定时器。 其中包含一条设计哲学:一个ACK
到来了,说明后续的ACK
很可能会依次到来,也就是说丢失的可能性并不大,另外,即使真的有后发的TCP分段丢失现象发生,也会在最多2倍定时器超时时间的范围内被重传(假设该报文是第一个报文发出启动定时器之后马上发出的,丢失了,第一个报文的ACK
到来后又重启了定时器,又经过了一个超时时间才会被重传)。网络拥塞会引起丢包,丢包会引起重传,过度重传反过来加重网络拥塞,这样设计可以缓解过多的重传,毕竟将启动定时器之后发送的数据的重传超时时间拉长了最多一倍左右。
当超时真的发生的时候,我们就需要重传了,重传的依据就是序列号。发送端会给每个报文加上一个序列号,而接收端会保存当前连续收到的最后一个序列号,并以此发送ACK。发送端若长期未收到ACK,即超时发生,则出发该序列号的包重传。另外,发送端重发了一个TCP报文并且接收到该TCP分段的确认号,并不能说明这个重发的报文被接收了,也可能是数据早就被接收了,只是由于其ACK丢失或者其ACK延迟到达导致了超时。值得说明的是**,接收端会丢弃任何重复的数据**,但是即使丢弃了重复的数据其ACK还是会照发不误的。
重传算法通常称之为ARQ(Auto Repeated Request),主要包括
端到端的流量控制使用滑动窗口来实现,滑动窗口本质上其实就是一个生产者消费者队列模型。滑动窗口会根据当前ACK的速率设置大小,尽量匹配收发两端的速率。仅从速率来看,该设计思路无可厚非,可以保证一个收发相对稳定可靠的队列模型。但是随着网络的发展,流量的爆炸式增加,除了速率以外我们会越来越重视效率。这引发了三个不同的问题:
ACK
的单独回复本身也会占用大量的带宽为此,TCP设计了几种不同的方案解决以上问题。
ACK
发送时机。ACK
的流量浪费,我们将ACK
和发回的数据包试图合并,如果等了一段可以接受的时间还是没有数据要发往发送端,此时就需要单独发送一个ACK
了,然而即使如此,这个延迟的ACK
也可能等到了后续到来的TCP分段,这样它们就可以取最大者一起返回了,要知道,TCP的确认号是收到的按序报文的最后一个字节的后一个字节。这就是延时ACK。RFC建议,延迟的ACK
最多等待两个分段的积累确认。这三个问题的解决方案是几乎同一时期提出的,各自为了解决不同的问题,但是其混杂在一起却造成了一些别的影响。
首先,我们可以分析出来Nagle算法和延时ACK对糊涂窗口综合征显然都是有利的:二者可以尽量拖延时间使得接收方尽可能多的处理数据从而增大窗口。但是延时ACK
和Nagle结合的时候就会出现一些不利因素:Nagle算法仅允许一个小包在路上,如果后续有多个小包而未积攒至足够大则不会发送。若此时延时ACK
也在工作并且接收端并无消息发送,则该ACK
只能等待超时后再传回。在这种场景下,Nagle和延时ACK
的叠加会导致性能的下降而无任何收益。对Nagle来说,其ACK
收到的时间延长了,对于延时ACK
来说,其期望的网络效率提高未实现。
因此,我们需要根据业务需要开启合适的流控算法,甚至对其进行修改优化,从而满足业务需求,提高网络传输的速率和效率。
端到端的TCP只能看到两个节点,那就是自己和对方,它们是看不到任何中间的路径的。可是IP网络却是一跳一跳的,它们的矛盾之处在于TCP的端到端流量控制必然会导致网络拥堵:每条TCP连接的一端只知道它对端还有多少空间用于接收数据,它们并不管到达对端的路径上是否还有这么大的容量。在早期网络数据较少的时候还未有这种烦恼,但是随着近30年网络的飞速发展,拥塞控制成了TCP的研究热点和痛点所在。
拥塞控制的本质依然是一种流控,但是和传统流控着眼于解决可靠性、网络速率、网络利用率不同,拥塞会直接导致整个网络的瘫痪。早期的TCP设计并无拥塞控制,遇到丢包则采用重传机制,而重传机制则在一定程度上加剧了网络的拥塞,导致了网络更加的不可用。由此,TCP拥塞控制应运而生。为了解决拥塞问题,拥塞控制必须设计成满足以下需求:
这两点也正是各种拥塞控制算法的出发点,其算法本身均是这两个需求的体现。最早期的经典拥塞控制算法为Tahoe, Reno和New Reno,即大名鼎鼎的慢开始、拥塞控制、快重传和快恢复。后续针对Reno的缺点,诞生出了Bic和更为高级的Cubic算法。除此之外,还有未解决特定场景而定制的一系列算法。如数据中心中的Vegas和New Vegas,无线网络的West Wood。这些算法均是延续着最早的设计需求和理念而发明,在前人的肩膀上拓展,Cubic至今也是Linux中的主流拥塞控制算法。但是值得一提的是,谷歌公司提出的BBR和BBR2.0则在一定程度上打破了常规拥塞控制算法的设计理念,真正的实现了拥塞控制的需求,让人们看到了拥塞控制未来的发展之路。后续会专门写一篇文章分析BBR源码及其思想。
下面我们来好好分析一下慢开始和拥塞避免的设计哲学。首先提出几个问题,如果知道的可以跳过该部分了。
ssthresh
是如何设置的?ssthresh
需要减半?为了回答这些问题,我们先要理解网络连接的容量。如下图所示为一个经典的发送、接收双方的链路抽象图。其中两端肥肥的部分代表的是发送、接收方的缓冲区,而中间细长管道则是网络链路。
先单看发送方,管道的容量取决于什么呢?有人肯定会说,这个问题太白痴了,管道的容量当然是取决于该管道实际的大小,即带宽时延乘积(BDP),但是实际不是这样的。一个管道或者说队列的容量,取决于其出口的速度大小,当发出速度和接受速度相等并填满管道时,则该管道满载。这其实是一道小学数学题:游泳池一边放水一边装水会空掉还是装满溢出?同样的,对于端到端的TCP连接来说,接收端处理的速度决定了该连接的最大容量。如果接收端处理无限快,我们可以认为该连接容量无穷大,可以以任意速度发包。
接着看看收发双端,相信大家都知道CPU指令流水线的原理,下图则是一个典型的慢开始阶段的收发段示意图(图片来源于《TCP/IP详解》,由于PDF质量差因此图片较为模糊)。由图可见,因为接收端速率和发送端相同,因此在一个RTT时间里,实际最多能发送管道容量N的两倍,即2 * N个数据包。
这2 * N个数据段发送的开始时间点是第1个数据段发送的时间,结束时间点是第一个数据段的ACK回到发送端的时间,正好是一个RTT,设发送速率为r,那么以下的等式显而易见:2 * N = r * RTT
下面我们再来分析一下上述流水线发包的一些关键点
这三个关键点,用大家最常见的描述即,慢开始,拥塞避免,拥塞时窗口减半。这么一看是不是感觉突然知道了慢开始和拥塞避免的设计思路了呢?
ssthresh
ssthresh
后,需要按照接收方的处理速度慢慢递增以免出现拥塞,因此该阶段称之为拥塞避免ssthresh
就应该等于当前窗口大小C的一半,又因为ssthresh
本身等于C(逐渐递增),因此ssthresh
取自身一半即为当前管道的实际N值(并不准)。 我们知道,ssthresh
的设置是以丢包作为反馈信号的,现在问题是,连接刚刚建立的时候,没有丢包作为反馈信号的时候,如何来设置ssthresh
?
一般而言,默认的实现都是将其设置为一个巨大的值如65536,然后最快的速度历经一次丢包,然后设置ssthresh
为丢包时窗口的一半,然后像ssthresh
的2倍缓慢逼近。但是这会带来问题,由于没有ssthresh
作为阈值限制,用丢包作为代价,太高昂。因此在慢启动过程中如果可以探测到ssthresh
的值,那就可以随时退出慢启动状态了。
根据上述公式 2 * N = r * RTT, 其中速率 r 为单位时间内发出的数据包数,假设在T时间内发出M个包,则公式可变为 T = (M / N) * (RTT / 2)。当 T 等于 RTT的一半时,则刚好可有M等于N,即达到了ssthresh
。由于我们无法单独探测M个数据段到达接收端并计时,我们可以变相等价使用ACK来计算,以一个窗口的第一个数据段作为计时开始Tstart
,每收到一个ACK即更新以下数值:
RTTmin
:采样周期内最小的RTT,以最大限度地表示A和B之间的理想往返时延。Tcurr
:当前时间 如果下列条件成立,则可以退出慢启动了:Tcurr - Tstart >= RTTmin / 2
然而现实并不是理想的,大多数情况下,以上的算法并没有带来比较好的效果,为什么呢?因为整个带宽不是一个TCP连接独享的,而是全世界的所有TCP连接甚至包括UDP共享的,因此以上的公式基本上无法表示任何真实的情况,所以实际当中,更倾向于使用RTT来预估网络已经被塞满。使用RTT来估算网络容量ssthresh
更加实际一些,因为它充分考虑了拥塞时的排队延时,因此在该方法下,退出慢启动的条件便成了:Tcurr_rtt > RTTmin + fixed_value
以上旨在解决首次慢启动在还没有ssthresh
值的时候预测ssthresh
的方式,其实在此后的任何时候,只要是慢启动,都可以用以上的算法来预测当前的ssthresh
,而不是说必须要用拥塞算法给出的ssthresh
或者说仅仅是1/2丢包窗口。
只有慢开始和拥塞避免的拥塞控制算法是不完美的,因为它太过于严苛了:
对于第一点,快重传进行了尝试解决。对于第二点,则是快恢复。慢开始、拥塞避免、快重传和快恢复结合起来,就是我们所舒适的Reno算法代表的经典拥塞控制算法。
理解了上述设计思路,大家就不难理解何为加性增乘性减(AIMD)了,其指的就是拥塞避免阶段加法形式的窗口增加和拥塞时的乘法性窗口递减过程。而为什么说这带来了TCP的公平性呢?下图是一副经典的说明图片。对于两个TCP连接R1和R2,假设网络总带宽为R,则二者必然会在某一个位置触发拥塞。拥塞之后,由于都是乘性减,因此对于占用窗口较大的一方会减少的更多,而之后窗口增加过程中由于系数相同,因此会同比增加,之后再次拥塞,再次减少,如图所示则为红线,随着不断地拥塞和窗口重置,该震荡曲线最终会收敛至y = x的曲线,即二者窗口相等。这就是TCP拥塞控制算法带来的公平性的体现。
如果降窗比例不相等,或者RTT时间不同,或者增加速度不同,则无法达到该效果,可见下图所示
该部分其实在前面已经有提到,而且也不打算在此展开讲,因为展开讲内容太多太多了,每一个算法都是几十页的论文加上几千上万行的源码,真有兴趣的同学可以先看《TCP/IP详解卷1》大致了解,再想深究可以就RFC文档、论文和源码进行深入研究。在这里之所以赘述,是为了更清晰的说明拥塞控制算法的分类和其特点。
早期的Tahoe,完善版的Reno,以及更完善的New Reno基本上奠定了以丢包驱动的拥塞控制算法类型,而后提出的BIC和CUBIC更是将该种拥塞控制算法发展到了巅峰(数学模型上的优化、进步以及多年经验积累)。至今,CUBIC依旧是TCP的默认拥塞控制算法。
从很早开始人们就对TCP拥塞控制算法进行了各种各样的优化,其中对于特殊使用场景的优化更是十分重要。这里面不得不提的就是数据中心等为代表的高速专用链路的通信。这些通道其特点不适用于丢包驱动,而是以时延驱动更为妥当(详细判断介绍可参考相应论文)。由此,Vegas和Fast成为了数据中心的主流拥塞控制算法。而Westwood和Westwood+则成了无线通信中的常用拥塞控制算法(更为常用的则是QUIC或者自定义的可靠UDP,性能更为出色)。
这里的主角就是BBR算法了,BBR全称Bottleneck Bandwidth and Round-trip propagation time
,简单的说就是带宽和时延乘积驱动的拥塞控制算法,即BDP驱动的拥塞控制算法。为什么该算法一经发出即引起广大轰动,不是因为它是谷歌提出来的(的确也是一部分原因),而是因为它的思想打破了长久以来的思维定式:管道填满和丢包是密切相关的。该算法提出:在现今的网络中,由于大量的缓冲区的存在,丢包发生的时候实际已经远远超过了实际的网络带宽,因此我们不应该等到丢包再进行拥塞控制,而是通过计算BDP来控制发送速率和窗口大小,从而使得网络一直处于将满未满的状态,即最完美的状态。BBR算法1.0尚显稚嫩,发展至2.0已经显现出了其无与伦比的潜力和作用,相信这是未来TCP拥塞控制算法的大势所趋。更为可怕的是,从谷歌的一些行动、博客和论文可以推测出,谷歌的野心在于通过可靠UDP的QUIC和BBR的结合,完美的取代已有的TCP/UDP,将网络传输层的API彻底收归己友,而事实上的确也是有着极强的竞争力。建议同学们有空一定要多多了解BBR和QUIC。
性能瓶颈的思考来源于之前被大佬问过的一道题:不考虑硬件性能的情况下,单机客户端、服务端最多能支持多少个连接?
这道题的答案主要考察了对连接五元组的理解,即源地址,源端口,目的地址,目的端口和协议类型。
C10K问题来源于Dan Kegel的个人博文《The C10K problem》,该文章建议深读。该作者在1999年提出了此问题,那时的服务器还只是 32 位系统,运行着 Linux 2.2 版本(后来又升级到了 2.4 和 2.6,而 2.6 才支持 x86_64),只配置了很少的内存(2GB)和千兆网卡。怎么在这样的系统中支持并发 1 万的请求呢?从资源上来说,对 2GB 内存和千兆网卡的服务器来说,同时处理 10000 个请求,只要每个请求处理占用不到 200KB(2GB/10000)的内存和 100Kbit (1000Mbit/10000)的网络带宽就可以。所以,物理资源是足够的,接下来自然是软件的问题,特别是网络的 I/O 模型问题。
在 C10K 以前,Linux 中网络处理都用同步阻塞的方式,也就是每个请求都分配一个进程或者线程。请求数只有 100 个时,这种方式自然没问题,但增加到 10000 个请求时,10000 个进程或线程的调度、上下文切换乃至它们占用的内存,都会成为瓶颈。既然每个请求分配一个线程的方式不合适,那么,为了支持 10000 个并发请求,这里就有两个问题需要解决。
解决方案主要来源于I/O模型的优化和工作模型的优化。
I/O模型从传统的阻塞请求方式改变为非阻塞
select
和poll
,后期有边缘触发的epoll
,设计理念为reactor模式。IOCP
,其设计理念为proactor模式。工作模型优化主要包括两种
bind() + listen()
后,创建多个子进程;然后,在每个子进程中,都通过 accept()
或 epoll_wait()
,来处理套接字。比如,最常用的反向代理服务器 Nginx 就是这么工作的。它也是由主进程和多个 worker 进程组成。主进程主要用来初始化套接字,并管理子进程的生命周期;而 worker 进程,则负责实际的请求处理。这里要注意,accept()
和 epoll_wait()
调用,还存在一个惊群的问题。换句话说,当网络 I/O 事件发生时,多个进程被同时唤醒,但实际上只有一个进程来响应这个事件,其他被唤醒的进程都会重新休眠。其中,accept()
的惊群问题,已经在 Linux 2.6 中解决了;而 epoll
的问题到了 Linux 4.5
,才通过 EPOLLEXCLUSIVE
解决。随着摩尔定律带来的服务器性能提升以及互联网的普及,C10K已经被远远的甩开,而C10M问题迎面而来。当各种软件、硬件的优化很可能都已经做到头了,特别是当升级完硬件(比如足够多的内存、带宽足够大的网卡、更多的网络功能卸载等)后,你可能会发现,无论你怎么优化应用程序和内核中的各种网络参数,想实现 1000 万请求的并发,都是极其困难的,这就是C10M问题。
究其根本,还是 Linux 内核协议栈做了太多太繁重的工作。从网卡中断带来的硬中断处理程序开始,到软中断中的各层网络协议处理,最后再到应用程序,这个路径实在是太长了,就会导致网络包的处理优化,到了一定程度后,就无法更进一步了。要解决这个问题,最重要就是跳过内核协议栈的冗长路径,把网络包直接送到要处理的应用程序那里去。这里有两种常见的机制,DPDK 和 XDP。
DPDK是用户态网络的标准。它跳过内核协议栈,直接由用户态进程通过轮询的方式,来处理网络接收。说起轮询,你肯定会下意识认为它是低效的象征,但是进一步反问下自己,它的低效主要体现在哪里呢?是查询时间明显多于实际工作时间的情况下吧!那么,换个角度来想,如果每时每刻都有新的网络包需要处理,轮询的优势就很明显了。比如:在 PPS 非常高的场景中,查询时间比实际工作时间少了很多,绝大部分时间都在处理网络包。而跳过内核协议栈后,就省去了繁杂的硬中断、软中断再到 Linux 网络协议栈逐层处理的过程,应用程序可以针对应用的实际场景,有针对性地优化网络包的处理逻辑,而不需要关注所有的细节。此外,DPDK 还通过大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
XDP(eXpress Data Path)则是 Linux 内核提供的一种高性能网络数据路径。它允许网络包在进入内核协议栈之前就进行处理,也可以带来更高的性能。XDP 底层和 bcc-tools 一样都是基于 Linux 内核的 eBPF
机制实现的。
短短一篇文章(其实很长)想弄懂TCP的全部内容显然是不现实的,本文希望能给同学们提供一个全新的思路去学习、掌握TCP,并通过理解其设计理念去进行优化、改进甚至自己创造更好的通信协议。如果真的能起到那么一点点帮助,那这篇文章就有其价值了,希望大家都能有所收获。
[1] wiki
[2] elixir.bootlin.com/linux
[3] woboq
[4] Linux-insides
[5] 深入理解Linux内核
[6] Linux内核设计的艺术
[7] 极客时间 趣谈Linux操作系统
[8] 深入理解Linux网络技术内幕
[9] CSDN dog250多篇博文
[10] TCP/IP详解