TCP’s Congestion Control Implementation in Linux Kernel 文献阅读笔记
作者:Somaya Arianfar
Aalto University
Somaya.Arianfar@aalto.fifi
因为我还是小白,不确定内容正确与否,如有错误请指出
论文地址:https://wiki.aalto.fi/download/attachments/69901948/TCP-CongestionControlFinal.pdf
论文基于:3.6.6内核
我自己参考的源码基于:5.11.0内核
因为前段时间刚开始学eBPF实现拥塞控制算法,想看看内核拥塞控制算法是怎么实现的,找资料很多都只是算法原理(真小白想看看源码都不知道具体是哪个文件,痛苦好几天),这篇论文感觉比较好。
这个用来当做记录,方便以后用到再查,有些对于我现在做的事情用处不大的地方只是复制了机翻(因为看不懂)。可以用来勉强看看大概流程,
1. 介绍
略
2. 结构
四个主要部分:
- 拥塞控制框架本身
- 拥塞控制框架与TCP的其余部分之间的接口
- 恢复状态机
- 以及不同的拥塞控制算法的细节。
2.1 重要的数据类型
2.1.1 tcp_ca_state:
tcp拥塞控制的是实现使用状态机来保持和切换不同的状态。这些不同的状态在tcp.h中以枚举类型定义。
/*linux 5.11.0 tcp.h
*位于linux-source-5.11.0\usr\include\linux\ 用于存放内核头文件的目录
*这写是看注释和查了一些博客记的注释,
*因为我还是小白,不确定内容正确与否,如有错误请指出
*/
/*
* Sender's congestion state indicating normal or abnormal situations
* in the last round of packets sent. The state is driven by the ACK
* information and timer events.
* 发送方的拥塞状态,指示发送的最后一轮数据包中的正常或异常情况。
* 该状态由ACK信息和计时器事件驱动。
*/
enum tcp_ca_state {
/*
* Nothing bad has been observed recently.
* 最近没有发生什么不好的情况
* No apparent reordering, packet loss, or ECN marks.
* 没有明显的重新排序、数据包丢失或ECN标记。
*/
TCP_CA_Open = 0,
#define TCPF_CA_Open (1<<TCP_CA_Open)
/*
* The sender enters disordered state when it has received DUPACKs or
* SACKs in the last round of packets sent. This could be due to packet
* loss or reordering but needs further information to confirm packets
* have been lost.
* 当发送方在最后一轮发送的数据包中接收到Dupacks或Sacks时,
* 发送方进入无序状态。这可能是由于数据包丢失或重新排序,
* 但需要进一步的信息来确认数据包已丢失。
*/
TCP_CA_Disorder = 1,
#define TCPF_CA_Disorder (1<<TCP_CA_Disorder)
/*
* The sender enters Congestion Window Reduction (CWR) state when it
* has received ACKs with ECN-ECE marks, or has experienced congestion
* or packet discard on the sender host (e.g. qdisc).
* 当发送方收到带有ECN-ECE标记的ACK,
* 或在发送方主机(如qdisc)上遇到拥塞或数据包丢弃时,
* 发送方进入拥塞窗口缩减(CWR)状态。
*/
TCP_CA_CWR = 2,
#define TCPF_CA_CWR (1<<TCP_CA_CWR)
/*
* The sender is in fast recovery and retransmitting lost packets,
* typically triggered by ACK events.
* 发送方正在快速恢复和重新传输丢失的数据包,通常由ACK事件触发。
*/
TCP_CA_Recovery = 3,
#define TCPF_CA_Recovery (1<<TCP_CA_Recovery)
/*
* The sender is in loss recovery triggered by retransmission timeout.
* 发送方处于由重传超时触发的丢包恢复状态中。
*/
TCP_CA_Loss = 4
#define TCPF_CA_Loss (1<<TCP_CA_Loss)
};
- Open:正常状态。在此状态下接受到的数据包将通过一种快速路径运行。TCP快速路径消除了在被标记的数据包上或在可疑的丢失或无序交付的情况下所需要的额外处理。
- Disorder:这种状态与Open非常相似,但需要更注意一些。当有一些SACKs或dupACKs时输入时,一些处理会从快速路径移动到慢路径。
- CWR:当发生了阻塞,丢包或者是收到ECN标记(交换机用来通知发送方可能会出现拥塞)是,进入的拥塞窗口减少的这个状态。
- Recovery:该状态表明拥塞窗口已经减少,连接处于快速重传阶段。
- Loss:发送方处于由重传超时触发的丢包恢复状态中。
2.1.2 tcp_congestion_ops
是一个结构体,也是定义在tcp.h,用来描述不同的拥塞控制算法的函数指针(每个算法都要填充这个结构体当中的指针),这个结构体在内核当中通过register后被组织成一个链表,用户可以通过命令行指定使用哪个拥塞控制算法。
/*
* Interface for adding new TCP congestion control handlers
*/
#define TCP_CA_NAME_MAX 16 //
#define TCP_CA_MAX 128
#define TCP_CA_BUF_MAX (TCP_CA_NAME_MAX*TCP_CA_MAX)
#define TCP_CONG_NON_RESTRICTED 0x1
#define TCP_CONG_RTT_STAMP 0x2
struct tcp_congestion_ops {
//所有拥塞控制算法组织成单链表
struct list_head list;
//当前只定义了两个标记:
//TCP_CONG_NON_RESTRICTED: 如果没有设置该标记,表示算法的使用者需要有网络管理权限
//TCP_CONG_RTT_STAMP:
unsigned long flags;
/* initialize private data (optional) 初始化私有数据*/
void (*init)(struct sock *sk);
/* cleanup private data (optional) 释放私有数据*/
void (*release)(struct sock *sk);
/* return slow start threshold (required) 返回慢开始门限*/
u32 (*ssthresh)(struct sock *sk);
/* lower bound for congestion window (optional) 拥塞窗口下限*/
u32 (*min_cwnd)(const struct sock *sk);
/* do new cwnd calculation (required) 进行新的cwnd计算*/
void (*cong_avoid)(struct sock *sk, u32 ack, u32 in_flight);
/* call before changing ca_state (optional)
在更改ca_state之前调用(ca_state暂时不懂)*/
void (*set_state)(struct sock *sk, u8 new_state);
/* call when cwnd event occurs (optional)
cwnd 事件发生时调用(可选)(cwnd event是什么?)*/
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
/* new value of cwnd after loss (optional)
发生丢包后cwnd的新值*/
u32 (*undo_cwnd)(struct sock *sk);
/* hook for packet ack accounting (optional)
数据包ack计数*/
void (*pkts_acked)(struct sock *sk, u32 num_acked, s32 rtt_us);
/* get info for inet_diag (optional) */
void (*get_info)(struct sock *sk, u32 ext, struct sk_buff *skb);
//可以为每个拥塞控制算法提供一个名字
char name[TCP_CA_NAME_MAX];
struct module *owner;
};
一些重要的函数调用:
-
init()
: 在接收到第一次确认之后和第一次调用拥塞控制算法之前调用该函数。 -
pkts_acked()
:一种确认信息,它确认一些新的数据包,从而导致对这个函数的调用。通过num_acked参数传递由此确认确认的数据包数。 -
cong_avoid()
:每次接收到确认并且拥塞控制状态允许拥塞窗口增加时,都调用此函数。 -
undo_cwnd()
: 在确认错误丢失检测(由于错误超时或数据包重新排序)后,返回新的拥塞窗口大小。
2.2 文件
这里列出了在内核中处理TCP代码的主文件。除非另有说明,否则可以在Linux内核代码中的net/ipv4/目录下找到许多这些文件。
tcp.h
:该文件包括与TCP相关的定义,包括上面定义的数据结构。这个文件同时存在于include/net/
和include/linux/
directories。tcp_input.c
:这是处理来自网络的传入数据包的最大和最重要的文件。它还包含了恢复状态机的代码。tcp_output.c
:这个文件会处理向网络发送数据包的问题。它包含了一些从拥塞控制框架中调用的函数。tcp_ipv4.c
:IPv4 TCP特定代码。此函数将相关的数据包交给拥塞控制框架。tcp_timer.c
:实现计时器管理功能。tcp_cong.c
:实现了可插入的TCP拥塞控制支持和默认为newReno实现的拥塞控制的核心框架。tcp_[name of algorithm].c
:这些文件实现了不同的算法和特定的拥塞控制逻辑。例如,tcp_vegas.c实现Vegas逻辑,tcp_cubic.c实现TCP Cubic算法。
3. 恢复状态机的信息流
在本节中,我们将描述在建立TCP连接和交换数据和确认数据包后会发生什么。拥塞窗口的调整和通过恢复状态机的转换主要取决于ack的接收情况,或者特定的拥塞信号,如超时和显式拥塞通知(ECN[10])位。TCP表示拥塞的简单形式是通过数据包丢弃。然而,ECN允许进行拥塞通知,而不丢弃数据包。在拥塞的情况下,ECN感知的路由器可以在IP头中标记,而不是丢弃数据包。数据包的接收方通过在TCP报头中设置ECN_Echo
(ECE[10]))标志,将拥塞指示回传给发送方。
我们在这里的主要关注点是处理接收到的数据包。但首先,我们简要描述了数据包发送方为处理ACK并在以后识别拥塞而采取的对策。
3.1 恢复处理对策
在数据发送端处理ack和识别拥塞的主要要素是 retransmission queue
和 retransmission timer
.在数据包的传输之后,总是将该数据包的副本放在重传队列中。ACK的接收会导致相应的重传队列中的副本被删除。在当前内核中,重传队列被定义为结构体struct sock
的成员,在write queue
之下。
每次发送数据包时,都为该包设置一个重传计时器。这个计时器会随着时间的推移而减少。在基本场景中,如果一个包的重传计时器在收到该包的确认之前过期,则认为该包丢失。在这种情况下,丢失的数据包将被重新传输。
无论如何,有三个标记位,用来标记重传输队列中的数据包:SACKED (S), RETRANS (R) and LOST(L)
。队列中设置了这些位的数据包被相应地计算在变量sacked_out
、retrans_out
和lost_out
中。虽然计算retrans_out
的适当值来计算重传数据包的数量是相当直接的,但标记正确的数据包的正确集合并且计算sacked_out
和lost_out
的正确值要复杂一些。(While calculating the proper value for retrans_out for counting the number of retransmitted packets is pretty straight forward, marking right set of packets and calculating proper values for sacked_out and lost_out are a bit more complicated.)
sacke_out记录非按序到达,所以不能被接受的的数据包的数量。对于SACKs,这个数字只是SACKed数据的简单数量。如果没有SACK,则是通过计数重复ack来计算的。
对于标记丢失的数据包和计算lost_out
,基本上有两种算法:
-
FACK
:在描述FACK算法来计算lost_out
之前,我们引入了另一个变量,称为fackets_out
。在代码中,fackets_out包括到迄今为止接收到的最高的SACK块的SACKed数据包和它们之间的洞。( In the code,fackets_out
includes both SACKed packets up to the highest received SACK block so far and holes in between them.)一旦FACK算法判定某些包丢失了,则判定所有非SACKed包直到最前面的SACK丢失。例如:lost_out =fackets_out - sacked_out 以及 left_out = fackets_out.如果网络不重新排序数据包,这似乎是一个正确的估计。但是重新排序可能会使这种估计无效。实现默认使用FACK,直到怀疑路径上有重新排序。重新排序经常被怀疑随着重复的ACKs和SACKs的到来。 -
NewReno:当连接处于Recovery状态并且部分ACK到达时,则假设又丢失了一个数据包。这个启发式方法在NewReno和SACK中也是相同的。
在TCP的重传逻辑[1]中:如果出现重传超时,TCP发送方进入重传超时恢复,其中拥塞窗口被初始化为一个段,而仍未确认的整个窗口被重传。因此,在一个RTO(重传输超时)之后,当整个队列被视为丢失时,lost_out等于packets_out。
3.2 恢复状态机
如前所述,tcp_input.c中的函数处理接收到的数据包。因此,我们在这里描述的函数调用主要来自tcp_input.c
,除非另有说明。如图1中可以看到,在终端主机上接收到一个数据包会触发一个调用tcp_event_data_recv().
这个函数本身由tcp_rcv_established()
调用,而tcp_rcv_established()
在tcp_ipv4.c
中被tcp_v4_do_rcv()
调用。调用tcp_event_data_recv()的结果是测量MSS和RTT,并触发一个ACK(或SACK)。包装过程受益于两种操作方式:
- Quick ACK:在TCP连接开始时使用,以便拥塞窗口可以快速增长.
- Delayed ACK:一个连接可以在一段时间后切换到此模式。在这种情况下,一个ACk表示收到多个数据包。
TCP会根据所经历的拥塞情况在这两种模式之间进行切换。默认情况下,启用快速ACK机制,ACK包立即触发,以快速提高拥塞窗口,特别是对于批量数据传输。一段时间后,当拥塞窗口增长得足够多时,可以使用延迟的ack来减少过度的协议处理。传入的ack在tcp_acks()
中进行处理。在这个函数中,序列号被处理,以明确接收ACK后需要的操作。例如,一些适当的反应可能是:清除重传队列,标记SACKed数据包,对重复的ack做出反应,重新排序检测和推进拥塞窗口。这里,我们描述了在tcp _ acks ( )中处理ACK时调用的两个最重要的函数。
3.2.1 tcp_sacktag_write_queue()
输入的SACKs在tcp_sacktag_write_queue()
中进行处理。在这里,当tcp_is_sackblock_valid()
在SACKs到达时标记重传队列。此函数也用于sack块的验证。SACK块范围验证检查接收到的SACK块是否符合预期的序列限制,ie.它是在SND.UNA和SND.NXT中间。还有另一个函数来限制sacked_out,这样与lost_out的和就不会大于packets_out。
3.2.2 tcp_fastretrans_alert()
在tcp_fastretrans_alert()
函数中实现了基本的恢复逻辑及其相关的状态转换。这个函数描述了Linux NewReno/SACK/FACK/ECN状态机,在可疑ACKs的情况下从tcp_acks()
调用它。可疑的ACK发生在第一次看到拥堵时,或者换句话说,到达的ACK是不寻常的。SACK,或者当TCP连接已经经历了一些异常情况,导致它从连接打开状态移动到恢复状态机中的任何其他状态时,如下面所述和第.??.节中所述
如图所示, 不同的函数集合被调用来检查连接的状态,并在每个状态下执行适当的操作。在tcp_fastretrans_alert()
中执行的最重要的函数调用描述如下:
tcp_check_sack_reneging()
:当接收到SACK时(通过前面提到的另一个函数),将标记重传队列中的数据包。然而,如果接收到的ACK/SACK指向一个记忆中的SACK,这可能与对SACK的错误认识有关。tcp_check_sack_reneging()
函数可以处理这种错误的情况。tcp_time_to_recover()
:此函数检查连接中丢失的数据包数量等参数,以决定是否适合移动到恢复状态。换句话说,这个函数决定了我们决定那个孔是由丢包造成的,而不是由重新排序造成的时刻。如果它决定这是恢复时间;CA状态将切换到Recovery状态。tcp_try_to_open()
: 如果还没有转移到恢复状态,此函数将根据包中的指示检查切换状态和其他适当的反应。例如,如果设置了数据包报头中的ECE标志,然后,该状态将切换到CWR。然后,通过调用tcp_cwnd_down,将减少拥塞窗口。- `tcp_update_scoreboard()·:此函数将标记丢失的数据包。根据SACK或FACK的选择,所有未被saced的数据包(直到最大序号被sacked)都可能被标记为丢失的数据包。此外,已过期重传计时器的未确认数据包在此函数中被标记为丢失。此功能中的所有标记触发丢包、sacked和遗漏数据包的重新计算。
tcp_xmit_retransmit_queue()
:此函数会触发对丢失的数据包的重传。它决定,我们应该重新传输什么来填补由丢失的数据包造成的漏洞。tcp_try_undo_<someyhing>()
:算法中逻辑上最复杂的部分是撤销启发法。由于快速重传(重新排序)和低估RTO,可能会发生错误重传。分析时间戳和D-SACKs可以识别这种错误重传的发生。错误重传的检测和拥塞窗口减少可以取消以及恢复阶段可以中止。这个逻辑隐藏在几个名为tcp_try_undo_<something>
的函数中。
以上功能主要用于恢复状态机,以及在需要重传时绕过重传队列。但是,我们将在下一节中讨论计算拥塞窗口大小的实际增加/减少量的实现。
4. 拥塞控制算法
基本的拥塞控制功能的核心功能是在tcp_cong.c
中定义的。TCP的最初的reno算法直接在tcp_reno_cong_avoid()
中实现。而在其他算法中,tcp_slow_start()
和tcp_cong_avoid_ai()
等函数会根据不同算法所做的计算向前移动拥塞窗口。
这些函数从代码中的不同位置调用,例如从tcp_input.c
或任何 tcp_[name of algorithm].c
文件。
图3显示了在代码中设置和初始化的不同拥塞控制算法的高级抽象。从用户的角度来看,这个结构中唯一可配置的部分是拥塞控制算法的选择。为了实现这个目标,该实现在不同的文件中使用了可插入的代码片段。
要注册可插拔的拥塞控制算法,它们在不同的文件中,如tcp_vegas.c
和tcp_cubic.c
中的实现包括一个结构体tcp_congestion_ops
,用于存储和初始化相关的函数调用和算法的名称。所有这些实现都通过从tcp_cong.c
调用(连接到)tcp_register_congestion_control
来注册到系统中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2zbEzg9x-1650624813124)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20220422181940640.png)]
然而对于每一个连接使用的算法都是通过内核初始化或者通过sysctl命令来设置的。在设置了拥塞控制算法后,将使用tcp_congestion_ops中定义的钩子从代码的其余部分中访问相关算法的特定函数。
所有算法的实现或多或少依赖于flight size的计算和拥塞窗口的估计大小。flight size显示已发送但尚未累计确认的数据量。因此,它可以用于推进拥塞窗口,或估计正确的值,例如ssthresh。在最佳情况下,flight size应该是带宽延迟乘积的反映。
在代码中,flight size通过in_flight
变量显示。
in_flight = packets_out + retrans_out - left_out
在上式中,packets_out是传输的最高数据段(SND.NXT)减去第一个未确认的段(SND.UNA)…如式所示,这个估计还需要考虑重传数据包的数量作为flight size计算的一部分。理论上,retrans_out和packets_out的总和应该表示在任何时刻的飞行大小。然而,在实践中,由于使用了SACKs和其他特性,packets_out本身也反映了那些以SACKed数据包或丢失数据包的形式离开网络的数据包,因此不在在flight size中。在上面的公式中,left_out是离开网络但尚未离开ACKed的这些数据包的数量。
left_out = sacked_out + lost_out
4.1 TCP cubic算法的理论
原来的TCP拥塞控制的雷诺算法是在链路容量和往返时间都有限的情况下设计的。现在,随着带宽延迟产品的增长,TCP在增加拥塞窗口大小方面的迟缓行为可能导致网络资源的利用不足。
TCP cubic算法是对TCP拥塞控制算法的最新改进之一,它将TCP的线性窗口增长函数变为三次函数,以提高高带宽延迟乘积网络情况下的带宽利用率。在不同往返时间的流量之间实现了较好的公平性水平。所有这些属性都使cubic算法成为Linux中的默认拥塞控制算法。
由于它来自于这个算法的名称,所以窗口增长函数是自上次数据包丢失以来的运行时间的三次函数。该算法将一个W_max注册为最后一个数据包丢失事件发生时的窗口大小。然后,该算法实现了对拥塞窗口的乘法减小。然后,当算法从快速恢复中进入拥塞避免阶段时,它就开始使用我们稍后将描述的三次函数来增加窗口。cubic继续以凹形增长,直到窗口大小等于W_max。然后,三次函数变成一个凸轮廓,凸窗口从那里继续增长。窗口增长的凹凸方式有助于网络资源的稳定性和更好的利用
4.2 代码中的TCP CUBIC算法
如前所述,不同的可插入拥塞控制算法在tcp_[name of algorithm].c
文件中实现。它们通过启动一个tcp_congestion_ops实例,向系统注册它们自己及其函数调用。其中一种算法,我们将在这里解释一下,是TCP cubic算法。TCP立方在代码中启动其函数调用如下:
static struct tcp_congestion_ops cubictcp
__read_mostly = {
.init = bictcp_init,
.ssthresh = bictcp_recalc_ssthresh,
.cong_avoid = bictcp_cong_avoid,
.set_state = bictcp_state,
.undo_cwnd = bictcp_undo_cwnd,
.pkts_acked = bictcp_acked,
.owner = THIS_MODULE,
.name = "cubic",
};
在初始化阶段之后,对每个接收到的确认都调用bictcp_acked()
,并触发适当的函数调用来增加/减少拥塞窗口。该功能主要根据以下内容跟踪延迟和延迟确认比:slidingwindowratio = (15∗ratio+sample)/16
跟踪延迟ACK的原因是TCP cubic中实现的逻辑。在Cubic中,如果ACK正常,拥塞窗口总是增加的,并且流量受拥塞窗口的限制。如果接收者使用的是延迟确认,则代码需要适应这个问题。
TCPcuce的代码集成了自己的实现,可以在缓慢启动阶段和避免拥塞阶段改变拥塞窗口大小。因此,bictcp_acked()也可以导致调用到hystart_update()启动的结果。慢启动阶段的hystart_update()基于HyStart[4]算法而不是标准的TCP慢启动逻辑增加拥塞控制窗口。在这个实现中,当拥塞窗口大于某个阈值时(hystart_low_window__read_mostly=16),将触发HyStart逻辑。
Cubic Hystart使用基于RTT的启发式算法,在丢包开始发生之前退出缓慢开始。Cubic HyStart使用延迟进行拥塞指示,但它从拥塞检测开始退出慢开始阶段,并进入cubic的标准拥塞避免。
对于拥塞避免阶段,TCPcubic[5]的窗口增长函数采用以下三次方程:
(具体讲解去知乎看吧,有一篇)
在代码中,在避免拥塞阶段调用bictcp_cong_avoid()。在这个阶段,计算适当的拥塞窗口大小是基于上述逻辑和在bictcp_update()函数来完成的。总体内容在tcp_cubic.c文件中。