TCP报文丢失判断

主要介绍下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

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页