`
simohayha
  • 浏览: 1387161 次
  • 性别: Icon_minigender_1
  • 来自: 火星
社区版块
存档分类
最新评论

linux 内核tcp拥塞处理(一)

阅读更多
这次我们来分析tcp的拥塞控制,我们要知道协议栈都是很保守的,也就是说只要有一个段被判断丢失,它就会认为发生了拥塞.而现在还有另一种,也就是路由器来通知我们发生了拥塞,这里ip头还会有一个ECN的位(准确的说是两位),来表示已经发送拥塞,不过这里要注意首先收到ECN的是接受方,可是真正需要被通知的却是发送方,因此当接受方收到ECN之后,用下一个ack来通知发送方有拥塞发生了,然后发送方才会做出响应.

可以看到这里会有个问题的,那就是我们如何来之到对端是否支持ECN,在内核中一般都是在握手的时候就会确定对端是否支持ECN.这里可以看到我们ip头里面必须用到2位,因为这里我们会有3个状态:

第一个发送端不支持ECN,第二个状态发送端支持ECN,第三个状态,发生了拥塞.

可以看到我们在握手的时候双方通过交换ECN的信息,从而能得到这条连接是否支持ECN.

下面这段在tcp_transmit_skb中的代码片断就是如何通知对端本地支持ecn的代码。可以看到代码很简单,就是判断是否是一个syn包,如果是的话就进入ecn的握手处理。


if (likely((tcb->flags & TCPCB_FLAG_SYN) == 0))
		TCP_ECN_send(sk, skb, tcp_header_size);


而在TCP_ECN_send中最终会通过下面这两个宏来设置是否支持ecn。可以看到都是通过设置tos。


//支持
#define	INET_ECN_xmit(sk) do { inet_sk(sk)->tos |= INET_ECN_ECT_0; } while (0)

//不支持
#define	INET_ECN_dontxmit(sk) \
	do { inet_sk(sk)->tos &= ~INET_ECN_MASK; } while (0)



内核中是使用ip头的TOS域的剩余2两位来表示ECN的.下面就是ECN的三种状态:

enum {

//发送端不支持ecn
	INET_ECN_NOT_ECT = 0,
//下面这个貌似没有用到,不知道有什么意义。
	INET_ECN_ECT_1 = 1,
//发送端支持ecn
	INET_ECN_ECT_0 = 2,
//发生了拥塞
	INET_ECN_CE = 3,

//掩码
	INET_ECN_MASK = 3,
};



而这里通过ecn来设置拥塞是通过IP_ECN_set_ce方法来做的,这个设置是在ip层(是在qos的enqueue也就是出队列方法)来做的,我们先来看这个方法。这个方法就是通过ip头的tos域来判断是否为INET_ECN_CE,如果是这个则说明发生了拥塞(路由器通知我们),此时我们需要设置这个ip头的tos域,然后发送给对端,从而通知对端。


static inline int IP_ECN_set_ce(struct iphdr *iph)
{
	u32 check = (__force u32)iph->check;
	u32 ecn = (iph->tos + 1) & INET_ECN_MASK;

	/*
	 * After the last operation we have (in binary):
	 * INET_ECN_NOT_ECT => 01
	 * INET_ECN_ECT_1   => 10
	 * INET_ECN_ECT_0   => 11
	 * INET_ECN_CE      => 00
	 */
//可以看到如果没有发生拥塞或者说不支持ecn的话直接返回。
	if (!(ecn & 2))
		return !ecn;

	/*
	 * The following gives us:
	 * INET_ECN_ECT_1 => check += htons(0xFFFD)
	 * INET_ECN_ECT_0 => check += htons(0xFFFE)
	 */
///然后开始计算对应的域。
	check += (__force u16)htons(0xFFFB) + (__force u16)htons(ecn);

	iph->check = (__force __sum16)(check + (check>=0xFFFF));

//设置tos为 INET_ECN_CE从而通知对端。
	iph->tos |= INET_ECN_CE;
	return 1;
}


然后我们来看接受端如何来处理ECN通知的拥塞,这里检测拥塞(ECN通知的)是通过TCP_ECN_check_ce这个方法来做的。

static inline void TCP_ECN_check_ce(struct tcp_sock *tp, struct sk_buff *skb)
{
	if (tp->ecn_flags & TCP_ECN_OK) {
//如果发生了拥塞,则设置flags。
		if (INET_ECN_is_ce(TCP_SKB_CB(skb)->flags))
			tp->ecn_flags |= TCP_ECN_DEMAND_CWR;
		/* Funny extension: if ECT is not set on a segment,
		 * it is surely retransmit. It is not in ECN RFC,
		 * but Linux follows this rule. */
		else if (INET_ECN_is_not_ect((TCP_SKB_CB(skb)->flags)))
			tcp_enter_quickack_mode((struct sock *)tp);
	}
}



接下来来看拥塞状态机,也就是发送的状态机,在linux内核中,发送端的状态分为下面5种,而这个状态是保存在inet_connection_sock的icsk_ca_state域中的。

enum tcp_ca_state
{
	TCP_CA_Open = 0,
#define TCPF_CA_Open	(1<<TCP_CA_Open)
	TCP_CA_Disorder = 1,
#define TCPF_CA_Disorder (1<<TCP_CA_Disorder)
	TCP_CA_CWR = 2,
#define TCPF_CA_CWR	(1<<TCP_CA_CWR)
	TCP_CA_Recovery = 3,
#define TCPF_CA_Recovery (1<<TCP_CA_Recovery)
	TCP_CA_Loss = 4
#define TCPF_CA_Loss	(1<<TCP_CA_Loss)
};


然后就简要的描述下这4个状态。

1 TCP_CA_Open

这个状态是也就是初始状态,我们可以看到在tcp_create_openreq_child(这个函数的意思可以看我前面的blog)中,当我们new一个新的socket之后就会设置这个socket的状态为TCP_CA_Open。这个也可以说是fast path。

2 TCP_CA_Disorder

当发送者检测到重复的ack或者sack就进入这个状态。在这个状态,拥塞窗口不会被调整,但是这个状态下的话,每一次新的输入数据包都会触发一个新的端的传输。

3 TCP_CA_CWR

这个状态叫做 (Congestion Window Reduced),顾名思义,也就是当拥塞窗口减小的时候会进入这个状态。比如当发送者收到一个ECN,此时就需要减小窗口。这个状态能够被Recovery or Loss 所打断。当接收到一个拥塞提醒的时候,发送者是每接收到一个ack,就减小拥塞窗口一个段,直到窗口大小减半。因此可以这么说当发送者正在减小窗口并且没有任何重传段的时候,就会处于CWR状态。

4 TCP_CA_Recovery

当足够数量的(一般是3个)的连续的重复ack到达发送端,则发送端立即重传第一个没有被ack的数据段,然后进入这个状态。处于这个状态的时候,发送者也是和CWR状态类似,每次接收到ack后减小窗口。在这个状态,拥塞窗口不会增长,发送者要么重传标记lost的段,要么传输新的段。当发送者进入这个状态时的没有被ack的段全部ack之后就离开这个状态。

5 TCP_CA_Loss

当RTO超时后,发送者就进入这个状态。此时所有的没有被ack的段都标记为loss,然后降低窗口大小为1,然后进入慢开始阶段。loss状态不能被其他状态所中断。而这个状态的退出只有当进入loss时,所有的被标记为loss的段都得到ack后,才会再次返回open状态。


然后我们再来看一下,linux中拥塞控制的用到的一些变量,这些变量前面的blog或多或少都有提过了,这次再统一描述一下。

在linux的协议栈实现中,用于拥塞控制的变量的关系满足下面的表达式:

left_out = sacked_out + lost_out
packets_out = SND.NXT - SND.UNA
in_flight = packets_out - left_out + retrans_out


sacked_out指的是被sack的段的个数。
lost_out指的是在网络中丢失的段的数目。这里要注意loss_out所包括的段依赖于所选择的recovery 方法的不同而不同(也就是依赖于不同的算法)。而且我们知道tcp并不能准确的得到数据包是否被丢弃,因此这个值只能是个猜测值。

因此我们可以看到left_out表示的就是已经离开网络可是还没有被ack的数据包。

packets_out指的是发送了还没有被确认的数据包。

retrans_out指的是重传的段的数目。

在linux协议栈的实现中,有两种算法来计算lost packets

1 FACK

这个算法是最简单的一个启发式算法,在这种算法中

lost_out = fackets_out - sacked_out并且left_out = fackets_out.

可以看到这里丢失的包不包括sacked的数据包。而fackets_out也就是sack和丢失的数据包的总和。

2 NewReno

这里还有一个classic Reno算法,是比较老的一个算法,这个算法中发送端收到一个新的ACK后旧退出TCP_CA_Recovery状态,而在NewReno中,只有当所有的数据包都被确认后才退出
TCP_CA_Recovery状态。

这里还有两个很重要的方法,分别是tcp_time_to_recover和tcp_xmit_retransmit_queue,这两个函数内核注释的也很详细:


引用


tcp_time_to_recover determines the moment _when_ we should reduce CWND and,
* hence, slow down forward transmission. In fact, it determines the moment
* when we decide that hole is caused by loss, rather than by a reorder.
*
* tcp_xmit_retransmit_queue() decides, _what_ we should retransmit to fill
* holes, caused by lost packets.


这两个方法后面会详细分析。

然后我们主要来看tcp_fastretrans_alert这个函数,所有的拥塞处理可以说都是在这个函数中进行的。这个函数被调用的条件是:

引用
* - each incoming ACK, if state is not "Open"
* - when arrived ACK is unusual, namely:
* * SACK
* * Duplicate ACK.
* * ECN ECE.


通过前面的blog我们知道,我们是在tcp_ack()中调用的,进入这个函数就意味着我们碰到了拥塞状态或者在拥塞状态处理tcp。在这个函数中,实现了下面的这些算法:

1 失败重传。
2 从不同的拥塞状态恢复。
3 检测到一个失败的拥塞状态从而使数据包的传输的延迟。
4 从所有的拥塞状态恢复到Open状态。



我们来一段段的看代码:

下面这一段主要是进行一些初始化,以及校验工作。

这里的这些flag标记就不详细介绍了,我前面的blog都有介绍,并且内核源码注释的也比较详细。

struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
///FLAG_SND_UNA_ADVANCED表示Snd_una被改变,也就是当前的ack不是一个重复ack。而FLAG_NOT_DUP表示也表示不是重复ack。
	int is_dupack = !(flag & (FLAG_SND_UNA_ADVANCED | FLAG_NOT_DUP));
//判断是否有丢失的段。
	int do_lost = is_dupack || ((flag & FLAG_DATA_SACKED) &&
				    (tcp_fackets_out(tp) > tp->reordering));
	int fast_rexmit = 0, mib_idx;

//如果发送未确认的数据包为0,则我们必须要重置sacked_out.
	if (WARN_ON(!tp->packets_out && tp->sacked_out))
		tp->sacked_out = 0;
//如果sacked_out为0,则fackets_out也必须设置为0.这是因为fack的计数依赖于最少一个sack的段。
	if (WARN_ON(!tp->sacked_out && tp->fackets_out))
		tp->fackets_out = 0;

	/* Now state machine starts.
	 * A. ECE, hence prohibit cwnd undoing, the reduction is required. */
///如果接收到ece则reset慢开始的界限。这是因为我们就要开始减小窗口了,所以这样就能停止慢开始。
	if (flag & FLAG_ECE)
		tp->prior_ssthresh = 0;

	/* B. In all the states check for reneging SACKs. */
//这个主要用来判断当前的ack是不是确认的是已经被sack的数据段,如果是的话,说明对端有bug或者不能正确的处理OFO的数据段。此时我们需要destroy掉所有的sack信息,然后返回。
	if (tcp_check_sack_reneging(sk, flag))
		return;

	/* C. Process data loss notification, provided it is valid. */

//这段主要是为了判断数据是否丢失。前两个判断主要是flag和fack的判定。
	if (tcp_is_fack(tp) && (flag & FLAG_DATA_LOST) &&
//这个为真说明一些段可能已经被接受端sack掉了。
	    before(tp->snd_una, tp->high_seq) &&
//这个说明当前已经进入了拥塞处理的状态。
	    icsk->icsk_ca_state != TCP_CA_Open &&
//这个为真说明在重传队列开始的一些段已经丢失。
	    tp->fackets_out > tp->reordering) {
///到达这里,我们将会依次标记所有的重传段都为丢失,直到我们发现第一个被sacked的段。
		tcp_mark_head_lost(sk, tp->fackets_out - tp->reordering);
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPLOSS);
	}

	/* D. Check consistency of the current state. */
	tcp_verify_left_out(tp);



下一篇我将会通过配合tcp_fastretrans_alert的剩余代码详细描述拥塞状态的变迁,以及各个状态的进入,退出条件.

这里可以看到慢开始这些没有介绍,这是因为我前面的blog对这些已经介绍过了,相关的可以看我前面的blog.
分享到:
评论

相关推荐

    linux tcp(linux tcp下拥塞控制的经典文章)

    主要讲解的是linux内核下的拥塞控制机制,想对这方面了解的学生可以看看。

    Linux内核中拥塞控制算法的比较分析 (2015年)

    采用网络测试床的实验研究方法,在Linux网络测试床上对内核中的Cubic、Htcp、Hybla、Westwood和Veno这5种拥塞控制协议在不同的网络环境下就链路利用率、RTT公平性和TCP友好性3个性能指标进行测试实验,分析实验结果,...

    LEo卫星网络TCP拥塞控制算法改进研究 (2006年)

    基于Vegas拥塞控制算法提出了一个适合于LEO卫星网络环境的TCP拥塞控制算法(Vegas-AB),该算法是Vegas算法和Vegas-A算法的调和,可以动态地改变Alpha、Beta,Vegas-AB算法与Vegas-A算法的主要分支大体保持一致。...

    论文研究-基于NS-3的卫星链路TCP仿真研究.pdf

    通过将NS-3与linux内核模块结合,仿真分析了linux内核中的11种TCP拥塞控制算法。以此提供了一种基于NS-3搭建TCP连接的仿真方法,可根据此方法仿真卫星链路的TCP连接,同样也可进行其他场景的TCP连接仿真分析。仿真...

    TCP-LEDBAT:实现TCP顶部的LEDBAT拥塞控制的内核模块

    TCP-LEDBAT内核模块这是使用Linux内核模块化拥塞控制框架通过TCP实现的LEDBAT拥塞控制算法。 当前版本已更新,可以在Linux内核5.4.x版本下进行编译。 使用内核(最高3.3.x)的版本带有“ 3.3”标签。 该模块尚未通过...

    linux内核阅读自己的笔记(用WinOrganizer工具打开)

    397 * 设置与tcp特性有关的信息,拥塞控制信息,窗口信息以及 398 * 设置套接字的状态为TCP_CLOSE,这里调用tcp_v4_init_sock() 401 err = sk-&gt;sk_prot-&gt;init(sk); ----------------------------&gt;1842 /* NOTE:...

    lksctp-rs:Rust 的 Linux 内核 SCTP 低级绑定

    Rust 绑定到 Linux 内核的 SCTP 网络功能。 道歉 我为名称的长首字母缩写词道歉,但“lksctp”是我创建绑定的项目名称,所以我认为应该保留它。 LK = Linux 内核 SCTP = 流控制传输协议 RS = (for) RuSt SCTP ...

    源自新浪的TCP性能分析工具Tcpdive.zip

     Tcpdive是基于linux内核的探测点机制,使用systemtap脚本语言和内嵌C代码来实现的。  通过定义几类相互关联的探测点和库函数,来收集和处理运行中内核的数据,以及修改内核的处理逻辑。  为什么要基于systemtap...

    Linux高性能服务器编程

    3.5.1 访问不存在的端口 3.5.2 异常终止连接 3.5.3 处理半打开连接 3.6 TCP交互数据流 3.7 TCP成块数据流 3.8 带外数据 3.9 TCP超时重传 3.10 拥塞控制 3.10.1 拥塞控制概述 3.10.2 慢启动和拥塞避免 ...

    nsdi_act:用于TCP实施的ACT测试平台

    TCP拥塞控制(ACT)自动化测试平台概述ACT的实现有四个主要模块:ns3,DCE,Linux内核和ase_brain。 对于ns3-dce,系统根据给定的随机分布来控制p2p通道中的数据包延迟。 它以输入参数配置(即input.txt)作为输入。...

    TCP-Fit-Illinois:尝试为OMNeT ++和INET实现TCP-Fit和TCP-Illinois

    TCP-Fit和TCP-伊利诺伊州尝试为OMNeT ++和INET实现TCP-Fit和TCP-Illinois拥塞控制机制TCP-Fit通过遵循伪代码和本文的一些建议来实现: : TCP-Illinois是Linux内核实现中的一个端口,位于: : 两种机制均已达到最终...

    SDN-TCPCongestionDetection:使用机器学习的SDN网络中拥塞检测的硕士学位论文

    使用机器学习的软件定义网络中的TCP拥塞检测本文的思想是使用决策树算法检测SDN网络中的拥塞使用的工具POX OpenFlow控制器(强烈建议使用OF版本1.3或1.5的基于Ryu Python的控制器)。 Mininet仿真器Wireshark Iperf...

    MultiTCP-开源

    MultiTCP是用于测试TCP拥塞避免算法和其他TCP增强功能的基准环境。 它使用内核日志来打印段统计信息和内部TCP变量,以分析和了解Linux TCP行为。

    K4YT3X的强化系统配置-Linux开发

    该存储库托管了我的sysctl.conf强化版本。 该配置文件旨在为Linux系统提供更好的安全性...防止读取内核指针禁用所有程序的Ptrace禁止SUID / GUID程序进行核心转储禁用IPv4 / IPv6路由启用BBR TCP拥塞控制启用SYN cookie

    Controlling Queue Delay

    路由器配备很大的入站缓冲区,以便不惜一切代价避免丢包...为了解决这个问题,有人提出了新的CoDel主动队列管理算法,且已经在Linux内核3.5以上版本中实现。ACM的这篇论文《Controlling Queue Delay》有详细的介绍。

    sysctl:K4YT3X的强化系统配置

    防止读取内核指针禁用所有程序的Ptrace禁止通过SUID / GUID程序进行核心转储禁用IPv4 / IPv6路由启用BBR TCP拥塞控制启用S​​YN cookie以减轻SYN泛洪攻击启用IP反向路径过滤以进行源验证...在应用配置文件之前,请...

Global site tag (gtag.js) - Google Analytics