Linux tcp sack_reneging分析

    sack_reneging表示发送端之前收到的sack为虚假sack,也就是说发送端之前标记的sack,可能又被接收端丢弃了。

    接收端收到乱序包后会先存放在out_of_order_queue队列里,然后等后续每次收到正常数据包后再检查乱序队列里是否有可以合并到sk_receive_queue的乱序包,当socket接收队列分配的内存(sk_rmem_alloc)超出设定的阈值(rmem_default)时,tcp会将乱序队列的数据包清空,这种场景下就需要发送端再次重传已经sack过的数据包了。

tcp_try_rmem_schedule

static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb,
				 unsigned int size)
{
	//sk_rmem_alloc表示socket接收队列分配的内存大小,在tcp_rcv_established->tcp_queue_rcv->skb_set_owner_r
	//流程里,将接收到的skb加入到socket->sk_receive_queue,然后同步增加队列分配的内存大小;
	//在skb释放流程(skb_free)里会调用析构函数destructor,然后回收skb的内存信息
	//sk_rcvbuf为socket分配的接收buffer,对应/proc/sys/net/core/rmem_default的值
	//如果sk接收队列分配的内存超过允许的内存时,需要对接收队列先清理
	//sk_rmem_schedule里检测可分配的socket buffer(sk->sk_forward_alloc)空间是否能容纳的下skb
	if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
	    !sk_rmem_schedule(sk, skb, size)) {

		if (tcp_prune_queue(sk) < 0)
			return -1;

		if (!sk_rmem_schedule(sk, skb, size)) {
			if (!tcp_prune_ofo_queue(sk))
				return -1;

			if (!sk_rmem_schedule(sk, skb, size))
				return -1;
		}
	}
	return 0;
}

    检查socket接收队列分配内存是在tcp_try_rmem_schedule里实现的,当判断没有空闲的socket内存时就会开始清理乱序队列数据包,最终进入__skb_queue_purge,该函数遍历乱序队列,然后将其上的skb清空。

static inline void __skb_queue_purge(struct sk_buff_head *list)
{
	struct sk_buff *skb;
	while ((skb = __skb_dequeue(list)) != NULL)
		kfree_skb(skb);
}

tcp_clean_rtx_queue

    接收端收到ack数据包后,会进入tcp_clean_rtx_queue清理发送队列的skb,在这个函数就会检查是否发生了sack_reneging,其判断的逻辑是:找到最新ack的下一个skb,如果该skb之前被标记过sack,那说明发生了sack_reneging,因为如果接收端是正常的话,那正常接收端回复的ack序列号是要包含该sack的数据包,现在ack号没有包含,那说明接收端很可能已经将该sack数据包丢弃了,发送端判断触发了sack_reneging,会对tcp的flag置上FLAG_SACK_RENEGING标记。

static int tcp_clean_rtx_queue(struct sock *sk, int prior_fackets,
			       u32 prior_snd_una, long sack_rtt_us)
{
	const struct inet_connection_sock *icsk = inet_csk(sk);
	struct skb_mstamp first_ackt, last_ackt, now;
	struct tcp_sock *tp = tcp_sk(sk);
	u32 prior_sacked = tp->sacked_out;
	u32 reord = tp->packets_out;
	bool fully_acked = true;
	bool rtt_update;
	long ca_seq_rtt_us = -1L;
	long seq_rtt_us = -1L;
	struct sk_buff *skb;
	u32 pkts_acked = 0;
	int flag = 0;

	first_ackt.v64 = 0;

	//移除已经ack的skb,同时更新计数信息,sack_out、packets_out等同步减少
	while ((skb = tcp_write_queue_head(sk)) && skb != tcp_send_head(sk)) {
		struct tcp_skb_cb *scb = TCP_SKB_CB(skb);
		u8 sacked = scb->sacked;
		u32 acked_pcount;

		/* Determine how many packets and what bytes were acked, tso and else */
		//计算能确认的段数
		//如果skb->end_seq大于snd_una,说明这个skb,要么不能被ack,要么只能被ack一部分(tso)
		if (after(scb->end_seq, tp->snd_una)) {
			//如果不是tso,或者skb的开始序列号seq大于snd_una,那说明整个skb都不能被确认删除
			//后面的skb也不再需要遍历了,退出for循环
			if (tcp_skb_pcount(skb) == 1 ||
			    !after(tp->snd_una, scb->seq))
				break;

			//如果是tso,那计算能被ack的段数
			acked_pcount = tcp_tso_acked(sk, skb);
			//没有可以被确认的,后面的也不需要再遍历,退出循环
			if (!acked_pcount)
				break;

			//fully_acked表示整个skb是否都可以被确认,还是只能确认一部分,为true表示全部确认,false表示
			//只能确认一部分
			fully_acked = false;
		} else {
			acked_pcount = tcp_skb_pcount(skb);
		}

		if (sacked & TCPCB_RETRANS) {
			if (sacked & TCPCB_SACKED_RETRANS)
				tp->retrans_out -= acked_pcount;
			flag |= FLAG_RETRANS_DATA_ACKED;
		} else {
			last_ackt = skb->skb_mstamp;
			WARN_ON_ONCE(last_ackt.v64 == 0);
			if (!first_ackt.v64)
				first_ackt = last_ackt;

			if (!(sacked & TCPCB_SACKED_ACKED))
				reord = min(pkts_acked, reord);
			if (!after(scb->end_seq, tp->high_seq))
				flag |= FLAG_ORIG_SACK_ACKED;
		}

		if (sacked & TCPCB_SACKED_ACKED)
			tp->sacked_out -= acked_pcount;
		if (sacked & TCPCB_LOST)
			tp->lost_out -= acked_pcount;

	
		//修改已发送但未ack的包个数信息
		tp->packets_out -= acked_pcount;
		pkts_acked += acked_pcount;

		/* Initial outgoing SYN's get put onto the write_queue
		 * just like anything else we transmit.  It is not
		 * true data, and if we misinform our callers that
		 * this ACK acks real data, we will erroneously exit
		 * connection startup slow start one packet too
		 * quickly.  This is severely frowned upon behavior.
		 */
		if (!(scb->tcp_flags & TCPHDR_SYN)) {
			flag |= FLAG_DATA_ACKED;
		} else {
			flag |= FLAG_SYN_ACKED;
			tp->retrans_stamp = 0;
		}

		if (!fully_acked)
			break;

		//把已经收到ack的skb从sk->write_queue里移除
		tcp_unlink_write_queue(skb, sk);
		//释放skb
		sk_wmem_free_skb(sk, skb);
		if (skb == tp->retransmit_skb_hint)
			tp->retransmit_skb_hint = NULL;
		if (skb == tp->lost_skb_hint)
			tp->lost_skb_hint = NULL;
	}

	if (likely(between(tp->snd_up, prior_snd_una, tp->snd_una)))
		tp->snd_up = tp->snd_una;

	//走到这里,说明skb是不能被确认的,但这个skb如果之前被sack过,那说明接收端有可能已经把这个skb删了
	//不然本次的ack的序列号应该是可以包含这个之前sack过的,这种情况就认为这个被sack过的skb是虚假的sack
	//设置标志位FLAG_SACK_RENEGING,后面会在tcp_fastretrans_alert里会重新rto定时器,
	//rto定时器超时后,会把write队列的skb都重传一遍,包括sack过的。
	//(tcp_retransmit_timer -->tcp_enter_loss--->tcp_retransmit_skb,在tcp_enter_loss里会清除sack标志位)
	if (skb && (TCP_SKB_CB(skb)->sacked & TCPCB_SACKED_ACKED))
		flag |= FLAG_SACK_RENEGING;

	skb_mstamp_get(&now);
	if (first_ackt.v64) {
		seq_rtt_us = skb_mstamp_us_delta(&now, &first_ackt);
		ca_seq_rtt_us = skb_mstamp_us_delta(&now, &last_ackt);
	}

	rtt_update = tcp_ack_update_rtt(sk, flag, seq_rtt_us, sack_rtt_us);

	if (flag & FLAG_ACKED) {
		const struct tcp_congestion_ops *ca_ops
			= inet_csk(sk)->icsk_ca_ops;

		tcp_rearm_rto(sk);
		if (unlikely(icsk->icsk_mtup.probe_size &&
			     !after(tp->mtu_probe.probe_seq_end, tp->snd_una))) {
			tcp_mtup_probe_success(sk);
		}

		if (tcp_is_reno(tp)) {
			tcp_remove_reno_sacks(sk, pkts_acked);
		} else {
			int delta;

			/* Non-retransmitted hole got filled? That's reordering */
			if (reord < prior_fackets)
				tcp_update_reordering(sk, tp->fackets_out - reord, 0);

			delta = tcp_is_fack(tp) ? pkts_acked :
						  prior_sacked - tp->sacked_out;
			tp->lost_cnt_hint -= min(tp->lost_cnt_hint, delta);
		}

		tp->fackets_out -= min(pkts_acked, tp->fackets_out);

		if (ca_ops->pkts_acked)
			ca_ops->pkts_acked(sk, pkts_acked, ca_seq_rtt_us);

	} else if (skb && rtt_update && sack_rtt_us >= 0 &&
		   sack_rtt_us > skb_mstamp_us_delta(&now, &skb->skb_mstamp)) {
		/* Do not re-arm RTO if the sack RTT is measured from data sent
		 * after when the head was last (re)transmitted. Otherwise the
		 * timeout may continue to extend in loss recovery.
		 */
		tcp_rearm_rto(sk);
	}

#if FASTRETRANS_DEBUG > 0
	WARN_ON((int)tp->sacked_out < 0);
	WARN_ON((int)tp->lost_out < 0);
	WARN_ON((int)tp->retrans_out < 0);
	if (!tp->packets_out && tcp_is_sack(tp)) {
		icsk = inet_csk(sk);
		if (tp->lost_out) {
			pr_debug("Leak l=%u %d\n",
				 tp->lost_out, icsk->icsk_ca_state);
			tp->lost_out = 0;
		}
		if (tp->sacked_out) {
			pr_debug("Leak s=%u %d\n",
				 tp->sacked_out, icsk->icsk_ca_state);
			tp->sacked_out = 0;
		}
		if (tp->retrans_out) {
			pr_debug("Leak r=%u %d\n",
				 tp->retrans_out, icsk->icsk_ca_state);
			tp->retrans_out = 0;
		}
	}
#endif
	return flag;
}

tcp_check_sack_reneging

    后面进入拥塞状态切换tcp_fastretrans_alert的时候,就会检查是否有sack_reneging标志位,如果有,则会重置retrans定时器,此时设定的定时器超时时间比正常的rto超时时间短。

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;
}

tcp_retransmit_timer

    重传定时器超时后进入超时处理函数tcp_retransmit_timer,该函数会进入tcp_enter_loss,在tcp_enter_loss里将当前的拥塞状态设定为丢包状态,并且将发送队列的sack标记全部清除,超时处理函数最终进入tcp_retransmit_skb,重传发送队列的skb。

void tcp_enter_loss(struct sock *sk)
{
	tcp_for_write_queue(skb, sk) {
		if (skb == tcp_send_head(sk))
			break;

		TCP_SKB_CB(skb)->sacked &= (~TCPCB_TAGBITS)|TCPCB_SACKED_ACKED;
		if (!(TCP_SKB_CB(skb)->sacked&TCPCB_SACKED_ACKED) || is_reneg) {
			TCP_SKB_CB(skb)->sacked &= ~TCPCB_SACKED_ACKED;
			TCP_SKB_CB(skb)->sacked |= TCPCB_LOST;
			tp->lost_out += tcp_skb_pcount(skb);
			tp->retransmit_high = TCP_SKB_CB(skb)->end_seq;
		}
	}
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值