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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章