主要介绍下RTO超时、NewReno、SACK以及RACK等情况下的报文丢失判断。
RTO超时标记丢失报文
在RTO超时处理中,套接口进入TCP_CA_Loss状态,由函数tcp_timeout_mark_lost标记套接口丢失报文。
/* Enter Loss state. */
void tcp_enter_loss(struct sock *sk)
{
tcp_timeout_mark_lost(sk);
如下函数tcp_timeout_mark_lost,如果SACK确认了重传队列首部的报文(本该由ACK.SEQ确认),表明对端丢弃了其OFO队列,。
static void tcp_timeout_mark_lost(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
head = tcp_rtx_queue_head(sk);
is_reneg = head && (TCP_SKB_CB(head)->sacked & TCPCB_SACKED_ACKED);
if (is_reneg) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPSACKRENEGING);
tp->sacked_out = 0;
tp->is_sack_reneg = 1;
} else if (tcp_is_reno(tp)) {
tcp_reset_reno_sack(tp);
}
遍历套接口重传队列,如果依据RACK算法,报文并没有超时,不标记,否则,调用函数tcp_mark_skb_lost对重传队列中的报文进行丢失标记。
skb = head;
skb_rbtree_walk_from(skb) {
if (is_reneg)
TCP_SKB_CB(skb)->sacked &= ~TCPCB_SACKED_ACKED;
else if (tcp_is_rack(sk) && skb != head &&
tcp_rack_skb_timeout(tp, skb, 0) > 0)
continue; /* Don't mark recently sent ones lost yet */
tcp_mark_skb_lost(sk, skb);
}
tcp_verify_left_out(tp);
tcp_clear_all_retrans_hints(tp);
/* 虽然在tcp_verify_retransmit_hint函数中设置了retransmit_skb_hint指针,
* 但是,这里又做了清除此指针的操作。 */
如下tcp_mark_skb_lost函数,由子函数tcp_skb_mark_lost_uncond_verify完成丢失标记。如果此报文被重传过,既然已经丢失,清除其重传状态位TCPCB_SACKED_RETRANS,并且将套接口重传计数retrans_out减去报文数量。
注意重传报文再度丢失的情况下,sacked状态位为:(TCPCB_LOST | TCPCB_EVER_RETRANS),没有标志位TCPCB_SACKED_RETRANS。
void tcp_mark_skb_lost(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
tcp_skb_mark_lost_uncond_verify(tp, skb);
if (TCP_SKB_CB(skb)->sacked & TCPCB_SACKED_RETRANS) {
/* Account for retransmits that are lost again */
TCP_SKB_CB(skb)->sacked &= ~TCPCB_SACKED_RETRANS;
tp->retrans_out -= tcp_skb_pcount(skb);
NET_ADD_STATS(sock_net(sk), LINUX_MIB_TCPLOSTRETRANSMIT,
tcp_skb_pcount(skb));
在函数tcp_skb_mark_lost_uncond_verify中,子函数tcp_verify_retransmit_hint用于设置下一次重传时,应使用的重传报文skb(保存在retransmit_skb_hint)。子函数tcp_sum_lost更新套接口丢失报文计数。
如果报文没有设置过丢失标记TCPCB_LOST,并且也没有被SACK确认(对于Reno,无标志TCPCB_SACKED_ACKED),增加丢失报文计数lost_out,并且设置TCPCB_LOST标志位。
void tcp_skb_mark_lost_uncond_verify(struct tcp_sock *tp, struct sk_buff *skb)
{
tcp_verify_retransmit_hint(tp, skb);
tcp_sum_lost(tp, skb);
if (!(TCP_SKB_CB(skb)->sacked & (TCPCB_LOST|TCPCB_SACKED_ACKED))) {
tp->lost_out += tcp_skb_pcount(skb);
TCP_SKB_CB(skb)->sacked |= TCPCB_LOST;
}
}
/* This must be called before lost_out is incremented */
static void tcp_verify_retransmit_hint(struct tcp_sock *tp, struct sk_buff *skb)
{
if (!tp->retransmit_skb_hint ||
before(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(tp->retransmit_skb_hint)->seq))
tp->retransmit_skb_hint = skb;
}
如下tcp_sum_lost函数,这里有两种情况: a)报文没有被标记为丢失,第一次丢失; b)重传报文的再度丢失。增加套接口报文丢失计数lost。注意这里的lost通过包括重传丢失报文,而以上的lost_out计数不包括重传丢失。
/* Sum the number of packets on the wire we have marked as lost.
* There are two cases we care about here:
* a) Packet hasn't been marked lost (nor retransmitted), and this is the first loss.
* b) Packet has been marked both lost and retransmitted, and this means we think it was lost again.
*/
static void tcp_sum_lost(struct tcp_sock *tp, struct sk_buff *skb)
{
__u8 sacked = TCP_SKB_CB(skb)->sacked;
if (!(sacked & TCPCB_LOST) ||
((sacked & TCPCB_LOST) && (sacked & TCPCB_SACKED_RETRANS)))
tp->lost += tcp_skb_pcount(skb);
Reno标记丢失报文
在tcp_fastretrans_alert函数中,如果处于TCP_CA_Recovery拥塞状态的套接口,未能施行拥塞撤销操作(报文确实已经丢失),参见函数tcp_try_undo_partial(接收到对原始报文的确认)和tcp_try_undo_dsack(全部重传被DSACK确认)。进行丢包标记,由函数tcp_identify_packet_loss完成。
或者,套接口处于TCP_CA_Loss拥塞状态,并且tcp_process_loss未能执行拥塞撤销(恢复到TCP_CA_Open),函数tcp_identify_packet_loss执行丢包标记。
或者套接口拥塞状态不等于TCP_CA_Recovery或TCP_CA_Loss,由tcp_identify_packet_loss函数执行丢包标记。
static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
int num_dupack, int *ack_flag, int *rexmit)
{
/* E. Process state. */
switch (icsk->icsk_ca_state) {
case TCP_CA_Recovery:
...
tcp_identify_packet_loss(sk, ack_flag);
break;
case TCP_CA_Loss:
tcp_process_loss(sk, flag, num_dupack, rexmit);
tcp_identify_packet_loss(sk, ack_flag);
if (!(icsk->icsk_ca_state == TCP_CA_Open || (*ack_flag & FLAG_LOST_RETRANS)))
return;
default:
if (tcp_is_reno(tp)) {
...
}
if (icsk->icsk_ca_state <= TCP_CA_Disorder)
tcp_try_undo_dsack(sk);
tcp_identify_packet_loss(sk, ack_flag);
对于Reno/NewReno-TCP,由函数tcp_newreno_mark_lost执行丢包标记。
static void tcp_identify_packet_loss(struct sock *sk, int *ack_flag)
{
struct tcp_sock *tp = tcp_sk(sk);
if (tcp_rtx_queue_empty(sk))
return;
if (unlikely(tcp_is_reno(tp))) {
tcp_newreno_mark_lost(sk, *ack_flag & FLAG_SND_UNA_ADVANCED);
} else if (tcp_is_rack(sk)) {
...
如下tcp_newreno_mark_lost函数,当拥塞状态小于TCP_CA_Recovery,并且SACK(对于Reno,sacked_out等于dupack数量)确认的报文数量大于等于乱序等级时,认为发生了丢包,由于无法确认丢包数量,这里认为数量为1。
如果重传队列首报文长度大于MSS,执行分片处理,仅标记一个长度为MSS的报文为丢失状态。
void tcp_newreno_mark_lost(struct sock *sk, bool snd_una_advanced)
{
const u8 state = inet_csk(sk)->icsk_ca_state;
struct tcp_sock *tp = tcp_sk(sk);
if ((state < TCP_CA_Recovery && tp->sacked_out >= tp->reordering) ||
(state == TCP_CA_Recovery && snd_una_advanced)) {
struct sk_buff *skb = tcp_rtx_queue_head(sk);
if (TCP_SKB_CB(skb)->sacked & TCPCB_LOST)
return;
mss = tcp_skb_mss(skb);
if (tcp_skb_pcount(skb) > 1 && skb->len > mss)
tcp_fragment(sk, TCP_FRAG_IN_RTX_QUEUE, skb, mss, mss, GFP_ATOMIC);
tcp_skb_mark_lost_uncond_verify(tp, skb);
RACK标记丢失报文
参见上节的tcp_identify_packet_loss函数,对于启用RACK算法的TCP,由函数tcp_rack_mark_lost标记丢包,完成之后,如果之前的重传报文数量大于当前的重传数量,表明丢失了部分重传报文,设置FLAG_LOST_RETRANS标志。
static void tcp_identify_packet_loss(struct sock *sk, int *ack_flag)
{
struct tcp_sock *tp = tcp_sk(sk);
if (tcp_rtx_queue_empty(sk))
return;
if (unlikely(tcp_is_reno(tp))) {
...
} else if (tcp_is_rack(sk)) {
u32 prior_retrans = tp->retrans_out;
tcp_rack_mark_lost(sk);
if (prior_retrans > tp->retrans_out)
*ack_flag |= FLAG_LOST_RETRANS;
如下tcp_rack_mark_lost函数,由子函数tcp_rack_detect_loss标记丢失报文,并且设置重传队列中报文超时的定时器ICSK_TIME_REO_TIMEOUT。
void tcp_rack_mark_lost(struct sock *sk)
{
if (!tp->rack.advanced)
return;
/* Reset the advanced flag to avoid unnecessary queue scanning */
tp->rack.advanced = 0;
tcp_rack_detect_loss(sk, &timeout);
if (timeout) {
timeout = usecs_to_jiffies(timeout) + TCP_TIMEOUT_MIN;
inet_csk_reset_xmit_timer(sk, ICSK_TIME_REO_TIMEOUT,
timeout, inet_csk(sk)->icsk_rto);
函数tcp_rack_detect_loss负责依据RACK算法标记丢失报文,即如果发送时间靠后的报文已经被确认(ACK或者SACK),那么之前的未确认报文认为已经丢失。为抵御乱序的情况,RACK在确认报文和丢失报文之间设置了一定的时间差值。
如下遍历tsorted时间排序的报文链表,从最早发送的报文开始,如果其已经被标记为丢失,但是还没有重传,不进行处理。如果报文剩余时间小于等于0,表明已经超时,由函数tcp_mark_skb_lost进行标记,否则,如果报文剩余时间大于0,计算超时时长,返回给调用函数设置定时器。
static void tcp_rack_detect_loss(struct sock *sk, u32 *reo_timeout)
{
*reo_timeout = 0;
reo_wnd = tcp_rack_reo_wnd(sk);
list_for_each_entry_safe(skb, n, &tp->tsorted_sent_queue, tcp_tsorted_anchor) {
struct tcp_skb_cb *scb = TCP_SKB_CB(skb);
/* Skip ones marked lost but not yet retransmitted */
if ((scb->sacked & TCPCB_LOST) &&
!(scb->sacked & TCPCB_SACKED_RETRANS))
continue;
if (!tcp_rack_sent_after(tp->rack.mstamp,
tcp_skb_timestamp_us(skb),
tp->rack.end_seq, scb->end_seq))
break;
/* A packet is lost if it has not been s/acked beyond
* the recent RTT plus the reordering window.
*/
remaining = tcp_rack_skb_timeout(tp, skb, reo_wnd);
if (remaining <= 0) {
tcp_mark_skb_lost(sk, skb);
list_del_init(&skb->tcp_tsorted_anchor);
} else {
/* Record maximum wait time */
*reo_timeout = max_t(u32, *reo_timeout, remaining);
如下定时器超时处理函数,调用以上tcp_rack_detect_loss函数标记丢失报文。
void tcp_rack_reo_timeout(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
prior_inflight = tcp_packets_in_flight(tp);
tcp_rack_detect_loss(sk, &timeout);
SACK标记丢失报文
首先看一下tcp_fastretrans_alert函数中对丢包(do_lost)的判断,如果接收到dupack,或者对端SACK序号块确认的最高序号,超出SND.UNA加上乱序级别的值,认为套接口发生了丢包。另外,对于TCP_CA_Recovery拥塞状态的套接口,如果接收到的ACK报文(dupack)未能推进SND.UNA,并且Partial-Recovery未能实行,对于Reno-TCP(无SACK)或者tcp_force_fast_retransmit为真,设置丢包变量do_lost。
对于未启用RACK算法的情况,如果判断发生丢包,使用函数tcp_update_scoreboard处理。
static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
int num_dupack, int *ack_flag, int *rexmit)
{
bool do_lost = num_dupack || ((flag & FLAG_DATA_SACKED) &&
tcp_force_fast_retransmit(sk));
...
/* E. Process state. */
switch (icsk->icsk_ca_state) {
case TCP_CA_Recovery:
if (!(flag & FLAG_SND_UNA_ADVANCED)) {
...
} else {
if (tcp_try_undo_partial(sk, prior_snd_una))
return;
/* Partial ACK arrived. Force fast retransmit. */
do_lost = tcp_is_reno(tp) ||
tcp_force_fast_retransmit(sk);
}
if (!tcp_is_rack(sk) && do_lost)
tcp_update_scoreboard(sk, fast_rexmit);
*rexmit = REXMIT_LOST;
如果TCP套接口协商了SACK(非Reno/NewReno),并且SACK确认的报文数量大于乱序级别,即认为最早发送的报文已经丢失,sacked_upto表示丢失的报文中所包含的SACK确认报文的数量。或者fast_rexmit为真,仅将重传队列头部的首个报文标记为丢失。
static void tcp_update_scoreboard(struct sock *sk, int fast_rexmit)
{
struct tcp_sock *tp = tcp_sk(sk);
if (tcp_is_sack(tp)) {
int sacked_upto = tp->sacked_out - tp->reordering;
if (sacked_upto >= 0)
tcp_mark_head_lost(sk, sacked_upto, 0);
else if (fast_rexmit)
tcp_mark_head_lost(sk, 1, 1);
以下看一下fast_rexmit的设置,在函数tcp_fastretrans_alert中,如果套接口拥塞状态为TCP_CA_Loss,并且tcp_process_loss未能执行拥塞撤销(恢复到TCP_CA_Open),丢包确实发生。
或者,套接口的拥塞状态不等于TCP_CA_Recovery也不等于TCP_CA_Loss,在tcp_time_to_recover函数检测到需要进行快速恢复时,设置fast_rexmit变量为真。
static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
int num_dupack, int *ack_flag, int *rexmit)
{
/* E. Process state. */
switch (icsk->icsk_ca_state) {
case TCP_CA_Recovery: ... break;
case TCP_CA_Loss:
tcp_process_loss(sk, flag, num_dupack, rexmit);
tcp_identify_packet_loss(sk, ack_flag);
if (!(icsk->icsk_ca_state == TCP_CA_Open ||
(*ack_flag & FLAG_LOST_RETRANS)))
return;
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, (flag & FLAG_ECE));
fast_rexmit = 1;
}
if (!tcp_is_rack(sk) && do_lost)
tcp_update_scoreboard(sk, fast_rexmit);
*rexmit = REXMIT_LOST;
如下tcp_mark_head_lost函数,如果之前已经标记过丢失报文,取出保存的skb和丢包数量值;否者由重传队列的首报文开始遍历。如果仅标记一个报文(mark_head为真),并且保存的丢失报文开始序号在SND.UNA之后,表明完成请求的一个丢包的标记。
static void tcp_mark_head_lost(struct sock *sk, int packets, int mark_head)
{
/* Use SACK to deduce losses of new sequences sent during recovery */
const u32 loss_high = tcp_is_sack(tp) ? tp->snd_nxt : tp->high_seq;
WARN_ON(packets > tp->packets_out);
skb = tp->lost_skb_hint;
if (skb) {
/* Head already handled? */
if (mark_head && after(TCP_SKB_CB(skb)->seq, tp->snd_una))
return;
cnt = tp->lost_cnt_hint;
} else {
skb = tcp_rtx_queue_head(sk);
cnt = 0;
}
由报文skb开始遍历,如果当前遍历报文的结束序号位于最高的丢包序号之后,结束遍历。如果当前遍历的SACK所确认报文数量达到要求的packets值,退出遍历,或者,对于Reno而言,存在边界报文,并且此报文未被SACK确认,尝试将边界报文进行分片,标记分片后报文为丢失报文。
最后,使用tcp_skb_mark_lost函数对SACK未确认的报文进行丢包标记。
skb_rbtree_walk_from(skb) {
tp->lost_skb_hint = skb;
tp->lost_cnt_hint = cnt;
if (after(TCP_SKB_CB(skb)->end_seq, loss_high))
break;
oldcnt = cnt;
if (tcp_is_reno(tp) || (TCP_SKB_CB(skb)->sacked & TCPCB_SACKED_ACKED))
cnt += tcp_skb_pcount(skb);
if (cnt > packets) {
if (tcp_is_sack(tp) ||
(TCP_SKB_CB(skb)->sacked & TCPCB_SACKED_ACKED) ||
(oldcnt >= packets))
break;
mss = tcp_skb_mss(skb);
lost = (packets - oldcnt) * mss;
if (lost < skb->len &&
tcp_fragment(sk, TCP_FRAG_IN_RTX_QUEUE, skb, lost, mss, GFP_ATOMIC) < 0)
break;
cnt = packets;
}
tcp_skb_mark_lost(tp, skb);
if (mark_head) break;
如下函数tcp_skb_mark_lost,如果报文没有被标记过丢失(TCPCB_LOST),也没有被SACK确认(TCPCB_SACKED_ACKED),将其设置TCPCB_LOST标志,并且更新lost_out丢包统计。函数tcp_verify_retransmit_hint用于更新retransmit_skb_hint重传报文指针,其中记录的为首个应当重传的报文。
static void tcp_skb_mark_lost(struct tcp_sock *tp, struct sk_buff *skb)
{
if (!(TCP_SKB_CB(skb)->sacked & (TCPCB_LOST|TCPCB_SACKED_ACKED))) {
tcp_verify_retransmit_hint(tp, skb);
tp->lost_out += tcp_skb_pcount(skb);
tcp_sum_lost(tp, skb);
TCP_SKB_CB(skb)->sacked |= TCPCB_LOST;
内核版本 5.0