【Linux】网络专题(五)——TCP拥塞控制

一般而言,拥塞的时候会出现很多问题,其中尤为频发的是报文段丢失和乱序。丢失的可能原因有不少,例如路由器转发队列满时丢弃,甚至可能是随机早期丢弃(RED);乱序就是到达先后顺序颠倒,乱序的报文可能走过了不同的转发路径,其转发排队时间、链路传播时间等都可能不一样,进而抹平乃至逆转了发送的先后时差。

接下来我们会先介绍CC的一些理论,然后分析现代的Linux网络栈是怎样实现拥塞控制的。

1. 拥塞控制方法

1.1. TCP Tahoe

Tahoe包括慢启动和拥塞避免。最开始慢启动,cwnd设为1 MSS,每一次收到ACK,cwnd都会被增大一个MSS。直到cwnd到达ssthresh(它有一个初始值,例如64 KB),此时进入拥塞避免每一个RTT才会使cwnd增大一个MSS。

全程如果出现丢包事件(超时或者3个DACK),cwnd都会回到1 MSS,开始慢启动,同时ssthresh变为原先cwnd的一半。

ssthresh其实指示了上一次发生拥塞时的窗口大小的一半,换言之,cwnd到达ssthresh的时候,已经到了拥塞的一半,所以cwnd的增长不能够太激进。

1.2. TCP Reno

Reno在Tahoe之上提出了快速恢复算法。DACK指示的丢包可以说明,既然一个RTO内还能收到3个DACK,说明拥塞应该并不严重,所以也不必将cwnd暴力地置为1。

此时的做法是,将ssthresh置为cwnd的一半,所以前面提到的ssthresh的意义没有变,还是拥塞点的一半;而cwnd在ssthresh的基础上加3 MSS,它的意思是既然收到3个DACK,说明DACK对应的报文段还是正常发出去了,应该计到cwnd的变化中。接下来重设计时器,进入快速恢复。

快速恢复中,每收到一个DACK,都为cwnd加1 MSS。收到new ACK,就回到拥塞避免;如果计时器超时了,就进入慢启动。

1.3. TCP NewReno

Reno考虑了一个数据包丢失导致DACK的情况。那如果是多个数据包丢失呢?Reno只能看到第一个丢失的数据包,所以将窗口减半,重传一次,收到new ACK,退出快速恢复;然后又看到一个,再减半、重传;如此反复几次,窗口就减得差不多了。

为了解决这一问题,NewReno确保一个窗口内的所有丢失报文都收到ACK后,才退出快速恢复。可以想想看,Reno的问题出在哪里?如果丢失的报文不止一个,接收方发回的new ACK可能还是没有赶上发送方发送的最新的报文段的序号。

例如,假设6号报文段已经发送完,而4、5、6均在网络中丢失。发送方重传4,假设成功了,那么new ACK就是5,换言之还有报文段没送达,这种情况下就不应该退出快速恢复,而是继续重发。直到某次new ACK(例如ACK 11)恰好应答了所有已发送报文段(例如发送方发送完第10个报文段),才退出快速恢复。

上面例子的两种情况分别对应收到PACK(Partial ACK)和RACK(Recovery ACK)。在TCP Reno的基础上作补充,快速恢复状态下,收到PACK,则重发PACK对应的数据报;收到RACK,则进入拥塞避免。

1.4. TCP SACK

现代的TCP一个RTT能发好多报文段,一个窗口内丢包的期望值也增加了。TCP NewReno虽然减少了窗口缩减的次数,但没有改变一个RTT只重传一个报文段的本质。假如丢失的速率比重传的速率更高,接收方的缓存有可能会越来越少,使rwnd处于低位,这可就拉胯了。

为了提高重传效率,接收方可以多提供点信息,即把窗口内报文段的接受情况写在TCP报头里,这样发送方一分析就知道该批量重传哪些报文了。

TCP报头的20字节虽然基本写死了,但是选项字段有40字节,还可以放一些东西。选项编号占1字节,长度占1字节,后面紧邻着各个区间段,包括4字节的开始编号和4字节的结束编号,所以理论上这样的区间段最多能放4个。但其实SACK还要和另外一个时间戳选项并存,时间戳占10字节,所以也就放得下3个区间段了。

选项的具体内容是接收方提供的,但其实在缓冲区不足的时候,接收方可能会把已经接收的段删除掉而不再另行通知,即有违约(renege)的可能;更糟糕的是,这一字段还可以被精心设计用于攻击发送方,导致发送方panic,不过该bug已被修复。

在连接建立时,通信双方可协商是否支持SACK。不支持SACK,就使用(New)Reno。Tahoe被Reno向后兼容,但是已经没什么OS会用这个算法了。

2. 拥塞状态

这是各状态的定义。

/*
 * 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.
 */
enum tcp_ca_state {
	/*
	 * Nothing bad has been observed recently.
	 * No apparent reordering, packet loss, or ECN marks.
	 */
	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.
	 */
	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).
	 */
	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.
	 */
	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)
};

3. 显式拥塞通知ECN

ECN需要TCP和IP层的共同支持。简单说,路由器是拥塞的报告方,会在IP层的一对比特位中记录是否发生了拥塞。接收方收到报文段时,就知道它在传输的过程中是否发生了拥塞。然而,发送方才真正地需要了解拥塞状况并调节发送速率,所以接收方会把拥塞信息设置在ACK数据报并发回到发送方。

3.1. IP层支持

enum {
	INET_ECN_NOT_ECT = 0,
	INET_ECN_ECT_1 = 1,
	INET_ECN_ECT_0 = 2,
	INET_ECN_CE = 3,
	INET_ECN_MASK = 3,
};

它们用于表示ECN的状态。00表示不支持ECN,01或10表示支持ECN(但没有发生拥塞),11表示发生拥塞。设置拥塞比特在这里实现:

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
	 */
	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));
	iph->tos |= INET_ECN_CE;
	return 1;
}

进行了一些位运算骚操作,并重新计算了校验和。

3.2. TCP层支持

TCP有什么字段是支持ECN的?或许我们可以用选项字段?实际上没必要,TCP用了6位保留位的后2位CWR、ECE来支持ECN,它的后面紧接着就是传统的6个flag。

  • ECE用于ACK,表明收到了来自路由器的拥塞通知。
  • CWR是发送端拥塞窗口减小标志,用于通知接收端已经收到了设置ECE标志的ACK。

三次握手时,通过将ECE和CWR都设为1,可构造一个SYN报文段,表示自己支持ECN。宏定义如下,包含了其他一些flag,可自行体会。

#define TCPHDR_FIN 0x01
#define TCPHDR_SYN 0x02
#define TCPHDR_RST 0x04
#define TCPHDR_PSH 0x08
#define TCPHDR_ACK 0x10
#define TCPHDR_URG 0x20
#define TCPHDR_ECE 0x40
#define TCPHDR_CWR 0x80

#define TCPHDR_SYN_ECN	(TCPHDR_SYN | TCPHDR_ECE | TCPHDR_CWR)

4. 快速重传

快速重传的状态处理函数在tcp_fastretrans_alert,接下来我们梳理快速重传的发生时机和具体动作。

4.1. tcp_ack中

这是处理到来ACK的函数,定义于tcp_input.c。函数具体做了什么我们就不展开分析了,源码其实写得很清楚,或许我会在另一篇文章出给出讲解。我们只考虑快速重传的可能触发时机:

  • 当前CC状态不是Open。
  • 收到的ACK是SACK、dup ACK,或者ACK中带ECE位。
#define FLAG_CA_ALERT		(FLAG_DATA_SACKED|FLAG_ECE|FLAG_DSACKING_ACK)

static inline bool tcp_ack_is_dubious(const struct sock *sk, const int flag)
{
	return !(flag & FLAG_NOT_DUP) || (flag & FLAG_CA_ALERT) ||
		inet_csk(sk)->icsk_ca_state != TCP_CA_Open;
}
	if (tcp_ack_is_dubious(sk, flag)) {
		if (!(flag & (FLAG_SND_UNA_ADVANCED | FLAG_NOT_DUP))) {
			num_dupack = 1;
			/* Consider if pure acks were aggregated in tcp_add_backlog() */
			if (!(flag & FLAG_DATA))
				num_dupack = max_t(u16, 1, skb_shinfo(skb)->gso_segs);
		}
		tcp_fastretrans_alert(sk, prior_snd_una, num_dupack, &flag,
				      &rexmit);
	}

此时有快速重传的可能性,故需要进一步确认,并在有必要的时候修改CC状态。

ACK处理结束的时候,会统一调用以下函数,以完成速率采样、拥塞控制和可能的重传:

	tcp_rate_gen(sk, delivered, lost, is_sack_reneg, sack_state.rate);
	tcp_cong_control(sk, ack, delivered, flag, sack_state.rate);
	tcp_xmit_recovery(sk, rexmit);

4.2. tcp_fastretrans_alert

该函数只是进行alert,计算已离开主机的报文数(包括已被对方接收但并未ACK的报文,和已经被不可靠网络弄丢的报文),修改标志位,并进行CC状态转换;换言之,不会进行拥塞控制操作(如窗口调节),也不参与重发。

第一步,如果收到的ACK置了ECE位,说明收到显示拥塞通知,此时要开始减小拥塞窗口。

	 * A. ECE, hence prohibit cwnd undoing, the reduction is required. */
	if (ece_ack)
		tp->prior_ssthresh = 0;

第二步,如果在ACK报文处理中发现有SACK违约,即ACK指向了SACK中已经接收到的段。说明接收方处于重度拥塞,或者接收方处理出现BUG。我们先将其记下,设个计时器,等max(RTT/2, 10ms)的时间,好让接收方能够发送更多的ACK,并期望接收方重新履行SACK。如果接收方仍然违约,则必须执行重传。

/* If ACK arrived pointing to a remembered SACK, it means that our
 * remembered SACKs do not reflect real state of receiver i.e.
 * receiver _host_ is heavily congested (or buggy).
 *
 * To avoid big spurious retransmission bursts due to transient SACK
 * scoreboard oddities that look like reneging, we give the receiver a
 * little time (max(RTT/2, 10ms)) to send us some more ACKs that will
 * restore sanity to the SACK scoreboard. If the apparent reneging
 * persists until this RTO then we'll clear the SACK scoreboard.
 */
static bool tcp_check_sack_reneging(struct sock *sk, int flag)
{
	if (flag & FLAG_SACK_RENEGING) {
		struct tcp_sock *tp = tcp_sk(sk);
		unsigned long delay = max(usecs_to_jiffies(tp->srtt_us >> 4),
					  msecs_to_jiffies(10));

		inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
					  delay, TCP_RTO_MAX);
		return true;
	}
	return false;
}

static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
				  int num_dupack, int *ack_flag, int *rexmit)
{
	/* ... */
	/* B. In all the states check for reneging SACKs. */
	if (tcp_check_sack_reneging(sk, flag))
		return;
}

第三步,检查状态一致性。

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

检查的是left_out,我们要确保这个数量不能多于packet_out。那应该是显然的,前者是离开主机的未被确认的报文数,后者是离开发送队列的未被确认的报文数;如果出现了反常,就需要进行WARN。

第四步,检测从拥塞状态返回的条件。

	/* D. Check state exit conditions. State can be terminated
	 *    when high_seq is ACKed. */
	if (icsk->icsk_ca_state == TCP_CA_Open) {
		WARN_ON(tp->retrans_out != 0);
		tp->retrans_stamp = 0;
	} else if (!before(tp->snd_una, tp->high_seq)) {
		switch (icsk->icsk_ca_state) {
		case TCP_CA_CWR:
			/* CWR is to be held something *above* high_seq
			 * is ACKed for CWR bit to reach receiver. */
			if (tp->snd_una != tp->high_seq) {
				tcp_end_cwnd_reduction(sk);
				tcp_set_ca_state(sk, TCP_CA_Open);
			}
			break;

		case TCP_CA_Recovery:
			if (tcp_is_reno(tp))
				tcp_reset_reno_sack(tp);
			if (tcp_try_undo_recovery(sk))
				return;
			tcp_end_cwnd_reduction(sk);
			break;
		}
	}

首先,Open状态是最常见的。需要断言下,肯定不应该有正在重传的报文吧?

其次对非正常的状态进行处理。对于tp->snd_una >= tp->high_seq,前者是期望收到ACK的第一个字节序号,而后者是拥塞开始时将要发送的第一个字节序号,该式成立时说明拥塞前发送的所有报文都已确认收到,可以考虑退出拥塞状态。如果此时是CWR状态,还要确保拥塞期间发送的报文也可以被ACK,这起码说明拥塞比之前好多了,可以结束该状态,并进入Open。如果是Recovery状态,那么根据协议,此时也可以退出快速恢复。

第五步,进行状态转移。这里内容比较多就不一一列举了,我们看Disorder、Loss和Open状态下的状态转移:

	default:
		/* ... */

		tcp_identify_packet_loss(sk, ack_flag);
		if (!tcp_time_to_recover(sk, flag)) {
			tcp_try_to_open(sk, flag);
			return;
		}

		/* ... */

		/* Otherwise enter Recovery state */
		tcp_enter_recovery(sk, ece_ack);
		fast_rexmit = 1;

进入Recovery状态,并作一个fast_rexmit的标记;或者尝试进入Open状态。

做完这些之后,为需要重传的报文打标记,并将rexmit标记REXMIT_LOST

	if (!tcp_is_rack(sk) && do_lost)
		tcp_update_scoreboard(sk, fast_rexmit);
	*rexmit = REXMIT_LOST;

4.3. tcp_cong_control

tcp_cong_control是拥塞控制引擎的调用函数,方便兼容各种各样的拥塞控制算法。看看这注释,“终极”拥塞控制函数。

它会尝试调用icsk->icsk_ca_ops中注册的拥塞控制函数,如果没有注册则采用默认策略:使用传统的窗口减小tcp_cwnd_reduction和增大tcp_cwnd_reduction函数。

/* The "ultimate" congestion control function that aims to replace the rigid
 * cwnd increase and decrease control (tcp_cong_avoid,tcp_*cwnd_reduction).
 * It's called toward the end of processing an ACK with precise rate
 * information. All transmission or retransmission are delayed afterwards.
 */
static void tcp_cong_control(struct sock *sk, u32 ack, u32 acked_sacked,
			     int flag, const struct rate_sample *rs)
{
	const struct inet_connection_sock *icsk = inet_csk(sk);

	if (icsk->icsk_ca_ops->cong_control) {
		icsk->icsk_ca_ops->cong_control(sk, rs);
		return;
	}

	if (tcp_in_cwnd_reduction(sk)) {
		/* Reduce cwnd if state mandates */
		tcp_cwnd_reduction(sk, acked_sacked, flag);
	} else if (tcp_may_raise_cwnd(sk, flag)) {
		/* Advance cwnd if state allows */
		tcp_cong_avoid(sk, ack, acked_sacked);
	}
	tcp_update_pacing_rate(sk);
}

4.4. tcp_xmit_recovery

在拥塞控制算法更新窗口大小之后,tcp_xmit_recovery将处理可能产生的重传。

/* Congestion control has updated the cwnd already. So if we're in
 * loss recovery then now we do any new sends (for FRTO) or
 * retransmits (for CA_Loss or CA_recovery) that make sense.
 */
static void tcp_xmit_recovery(struct sock *sk, int rexmit)
{
	struct tcp_sock *tp = tcp_sk(sk);

	if (rexmit == REXMIT_NONE || sk->sk_state == TCP_SYN_SENT)
		return;

	if (unlikely(rexmit == REXMIT_NEW)) {
		__tcp_push_pending_frames(sk, tcp_current_mss(sk),
					  TCP_NAGLE_OFF);
		if (after(tp->snd_nxt, tp->high_seq))
			return;
		tp->frto = 0;
	}
	tcp_xmit_retransmit_queue(sk);
}

rexmit标志为REXMIT_NONE的时候自然不用重传;但是以下两种情况时,rexmit会被设为需要重传:

  • REXMIT_LOST:在tcp_fastretrans_alert中确认有丢包,或在tcp_process_loss(Loss状态)收到PACK,确认需要继续重传。
  • REXMIT_NEW:需要发送一些新的数据,和F-RTO机制相关,这里不再做更深的探究。

REXMIT_LOST的情况下,我们会进入输出引擎的tcp_xmit_retransmit_queue,后者经过一系列调用链,到达TCP协议的重传入口__tcp_retransmit_skb

5. 超时重传

5.1. 初始化重传定时器

icsk作为面向连接套接字,理应有重传管理。该结构体中有一个域叫icsk_retransmit_timer,它就是重传定时器。我们看一下它的初始化:

void tcp_init_sock(struct sock *sk)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	/* ... */
	tcp_init_xmit_timers(sk);
	/* ... */
}

tcp_init_xmit_timers初始化了很多定时器,例如经典的重传、delayed ACK、keep-alive计时器等。在内核学习笔记(七)——定时器和时间管理中我们讲过了定时器的基础知识和使用方法。

TIME_WAIT计时器在哪里?

TIME_WAIT计时器放在了一个迷你套接字inet_timewait_sock中,它派生出的tcp_timewait_sock只有232字节,相比于tcp_sock的2192字节,它在不违反TCP协议的条件下大幅节约了TIME_WAIT状态使用的内存。
void tcp_init_xmit_timers(struct sock *sk)
{
	inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,
				  &tcp_keepalive_timer);
	hrtimer_init(&tcp_sk(sk)->pacing_timer, CLOCK_MONOTONIC,
		     HRTIMER_MODE_ABS_PINNED_SOFT);
	tcp_sk(sk)->pacing_timer.function = tcp_pace_kick;

	hrtimer_init(&tcp_sk(sk)->compressed_ack_timer, CLOCK_MONOTONIC,
		     HRTIMER_MODE_REL_PINNED_SOFT);
	tcp_sk(sk)->compressed_ack_timer.function = tcp_compressed_ack_kick;
}

5.2. 启动重传定时器

L4/L3数据通路中我们提到过,TCP协议在向IP层交付新的报文段后,就启动一次tcp_event_new_data_sent,在其中完成计数、定时器等工作。我们看看它的实现:

/* Account for new data that has been sent to the network. */
static void tcp_event_new_data_sent(struct sock *sk, struct sk_buff *skb)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	unsigned int prior_packets = tp->packets_out;

	WRITE_ONCE(tp->snd_nxt, TCP_SKB_CB(skb)->end_seq);

	__skb_unlink(skb, &sk->sk_write_queue);
	tcp_rbtree_insert(&sk->tcp_rtx_queue, skb);

	if (tp->highest_sack == NULL)
		tp->highest_sack = skb;

	tp->packets_out += tcp_skb_pcount(skb);
	if (!prior_packets || icsk->icsk_pending == ICSK_TIME_LOSS_PROBE)
		tcp_rearm_rto(sk);

	NET_ADD_STATS(sock_net(sk), LINUX_MIB_TCPORIGDATASENT,
		      tcp_skb_pcount(skb));
}

做了下面一些事情:

  • 将刚发送出去的skb从sk_write_queue中移除,然后再插入到tcp_rtx_queue中,这一步语义非常自然。
  • 设置SACK相关的参数。
  • 更新packets_out,它代表已离开TCP发送队列的报文数量。
  • 调用tcp_rearm_rto重启超时计时器。
  • 更新SNMP的一些统计数据。

tcp_rearm_rto调用tcp_reset_xmit_timer以重启计时器,主要看下面else分支的部分:

/* Restart timer after forward progress on connection.
 * RFC2988 recommends to restart timer to now+rto.
 */
void tcp_rearm_rto(struct sock *sk)
{
	// Omit some boring initialization here

	if (!tp->packets_out) {
		inet_csk_clear_xmit_timer(sk, ICSK_TIME_RETRANS);
	} else {
		u32 rto = inet_csk(sk)->icsk_rto;
		// Omit some edge case handling for RTO here
		tcp_reset_xmit_timer(sk, ICSK_TIME_RETRANS, rto,
				     TCP_RTO_MAX);
	}
}

tcp_reset_xmit_timer中,启动重传超时器前,不应直接将其设为“当前时间+RTO”,因为从L4到离开主机还有一段时间,我们需要预估一个合理值并将其加上去,该工作由tcp_pacing_delay完成。最后通过icsk的通用接口inet_csk_reset_xmit_timer完成超时器的重设。

static inline void tcp_reset_xmit_timer(struct sock *sk,
					const int what,
					unsigned long when,
					const unsigned long max_when)
{
	inet_csk_reset_xmit_timer(sk, what, when + tcp_pacing_delay(sk),
				  max_when);
}

5.3. 超时重传回调操作

我们来关注超时重传的回调操作,通过一些锁操作后,tcp_write_timer会调用tcp_write_timer_handler。后者可能走向很多分支,但对于超时重传的情况,最终调用的是tcp_retransmit_timer。我们把关注的最核心的部分抽出来看看,其中我将个人理解以行内注释写在了代码中。

void tcp_retransmit_timer(struct sock *sk)
{
	// ...

	// Retreive a skb that caused timeout, then rtx it.
	skb = tcp_rtx_queue_head(sk);
	if (WARN_ON_ONCE(!skb))
		return;

	// ...

	if (!tp->snd_wnd && !sock_flag(sk, SOCK_DEAD) &&
	    !((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV))) {
		// ...
		if (tcp_jiffies32 - tp->rcv_tstamp > TCP_RTO_MAX) {
			// TCP_RTO_MAX is 120 seconds. If we couldn't recv
			// any ACK for more than 2 mins, some bad things
			// may have happened on the peer or network. We can't
			// wait no more but have to close the connection.
			tcp_write_err(sk);
			goto out;
		}
		// Loss of packet is confirmed, we'll enter Loss state.
		tcp_enter_loss(sk);
		// Only rtx the skb that caused timeout.
		tcp_retransmit_skb(sk, skb, 1);
		// ...
		goto out_reset_timer;
	}

	// ...

out_reset_timer:
	// Omit some adjustment of RTO here. Next step is to reset the rtx timer.
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				  tcp_clamp_rto_to_user_timeout(sk), TCP_RTO_MAX);
	// ...

out:;
}

调用tcp_retransmit_skb时已经与快速重传合流,之后进入所有重传的入口__tcp_retransmit_skb

参考资料

TCP拥塞控制算法之NewReno和SACK – CSDN

Throughput Analysis of TCP SACK in comparison to TCP Tahoe, Reno, and New Reno against Constant Rate Assignment (CRA) of 2500 and 4500 bps – JCSCM ’12

内核tcp的定时器管理

发表评论

您的电子邮箱地址不会被公开。