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