顯式擁塞通告(ECN)及其在Linux上的實現


1 ECN簡介

首先看看ECN握手報文的特點,根據RFC3168,ECN握手報文IP頭部不能夠設置ECT和CE位的

SYN報文TCP標志字段的CWR和ECE位被置1

 

SYN-ACK報文的CWR位被置0,ECE位被置1

報文在網絡上傳輸的過程中,如果路由器判斷自身發生擁塞則在報文的IP首部設置CE標志

服務器端在接收到有CE標志的報文后,立即構造帶有ECE標志的ACK報文,服務器端在接收到該ACK報文后進入TCP_CA_CWR狀態,在該狀態下發送窗口每兩個ACK減1。

 發生擁塞之前的報文都被確認后,客戶端會走出TCP_CA_CWR狀態,轉入TCP_CA_Open狀態,重新開始擁塞避免,並向服務端發送CWR標志,終止服務端向客戶端發送ECE報文。

 

2 ECN在Linux上的實現

以下所有分析基於Linux內核3.16.38

Linux內核通過調整tcp_sock結構體的ecn_flags來標識ECN所處的狀態,在文件include/net/tcp.h, line 393內,Linux定義了ECN可能的4種狀態,本文將通過這4中狀態的轉化把ECN從協議棧中肢解出來。

393 #define TCP_ECN_OK              1  //套接字支持ECN協議
394 #define TCP_ECN_QUEUE_CWR       2  //發送端在接收到ECE報文后,設置該標志,並將擁塞狀態機設置為TCP_CA_CWR狀態
395 #define TCP_ECN_DEMAND_CWR      4  //接收端處於該狀態,將在所有ACK報文中添加ECE,直到接收到CWR報文
396 #define TCP_ECN_SEEN            8  //是否接收到過ECT報文

2.1 實現握手

STEP1 :客戶端發送SYN,用戶態程序調用connect后,內核態通過tcp_connect構造SYN報文,tcp_connect會調用TCP_ECN_send_syn函數,該函數通過系統配置sysctl_tcp_ecn判斷是否啟用了ECN協議,如果啟用了ECN協議,則在SYN報文中添加ECE和CWR標志,並臨時設置該套接字為TCP_ECN_OK,這里說臨時的原因為在ECN握手失敗后,該標志還可能被取消。

3046 /* Build a SYN and send it off. */
3047 int tcp_connect(struct sock *sk)
3048 {
        ...
3070         TCP_ECN_send_syn(sk, buff);
3071 
        ...
3089 }

 

328 /* Packet ECN state for a SYN.  */
329 static inline void TCP_ECN_send_syn(struct sock *sk, struct sk_buff *skb)
330 {
331         struct tcp_sock *tp = tcp_sk(sk);
332 
333         tp->ecn_flags = 0;
334         if (sock_net(sk)->ipv4.sysctl_tcp_ecn == 1) {              //通過 /proc/sys/net/ipv4/tcp_ecn進行配置
335                 TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_ECE | TCPHDR_CWR;    //SYN報文需要添加ECE和CWR
336                 tp->ecn_flags = TCP_ECN_OK;                    //握手階段ecn_flags設為支持ECN通信,如果握手失敗TCP_ECN_OK會被取消
337         }
338 }

STEP2 :服務端處理SYN, tcp_rcv_state_process函數是接收數據時TCP層上的必經之路,它會根據報文類型調用不同函數來處理,所有握手報文都會交給tcp_v4_conn_request,而tcp_v4_conn_request又會調用TCP_ECN_create_request進行ECN-SYN報文,當SYN報文符合ECN-SYN標准時,套接字添加支持ECN標識。

 
 
         
5611 int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
5612                       const struct tcphdr *th, unsigned int len) 
5613 {
        ...
5623 switch (sk->sk_state) { 5624 case TCP_CLOSE: 5625 goto discard; 5626 5627 case TCP_LISTEN: 5628 if (th->ack) 5629 return 1; 5630 5631 if (th->rst) 5632 goto discard; 5633 5634 if (th->syn) { 5635 if (th->fin) 5636 goto discard; 5637 if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)    //調用tcp_v4_conn_request
5638 return 1; 5639 5657 kfree_skb(skb); 5658 return 0; 5659 } 5660 goto discard; 5661
            ...
5662         case TCP_SYN_SENT:
5663                 queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
5672         }
1257 int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
1258 {
        ...
1326         if (!want_cookie || tmp_opt.tstamp_ok)
1327                 TCP_ECN_create_request(req, skb, sock_net(sk));
        ...1398 }
737 static inline void
738 TCP_ECN_create_request(struct request_sock *req, const struct sk_buff *skb,
739                 struct net *net)
740 {
741         const struct tcphdr *th = tcp_hdr(skb);
742 
743         if (net->ipv4.sysctl_tcp_ecn && th->ece && th->cwr &&        //服務端也配置了ECN,同時SYN報文中函授ECE和CWR
744             INET_ECN_is_not_ect(TCP_SKB_CB(skb)->ip_dsfield))
745                 inet_rsk(req)->ecn_ok = 1;                   //套接字設置支持ECN標識 
746 }

 STEP3 :客戶端處理SYN-ACK報文,首先調用tcp_rcv_synsent_state_process,繼而調用TCP_ECN_rcv_synack來完成ECN握手。

5611 int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
5612                       const struct tcphdr *th, unsigned int len) 
5613 {

        ...
5662         case TCP_SYN_SENT:
5663                 queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
5672         }
        ...
5672 } 
5384 static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
5385                                          const struct tcphdr *th, unsigned int len)
5386 {
            ...
5446                 TCP_ECN_rcv_synack(tp, th);
            ...5602 }
246 static inline void TCP_ECN_rcv_synack(struct tcp_sock *tp, const struct tcphdr *th)
247 {
248         if ((tp->ecn_flags & TCP_ECN_OK) && (!th->ece || th->cwr))          //SYN-ACK報文含有CWR或不含ECE則握手失敗,客戶端撤銷TCP_ECN_OK
249                 tp->ecn_flags &= ~TCP_ECN_OK;
250 }

2.2 客戶端發送帶有ECT的報文

所有支持ECN通信的流,在傳輸層 tcp_transmit_skb -> TCP_ECN_send -> INET_ECN_xmit的流程中都會打上ECT(0)標記。

350 static inline void TCP_ECN_send(struct sock *sk, struct sk_buff *skb,
351                                 int tcp_header_len)
352 {
353         struct tcp_sock *tp = tcp_sk(sk);
354 
355         if (tp->ecn_flags & TCP_ECN_OK) {      //ECN通信添加ECT
356                 /* Not-retransmitted data segment: set ECT and inject CWR. */
357                 if (skb->len != tcp_header_len &&
358                     !before(TCP_SKB_CB(skb)->seq, tp->snd_nxt)) {
359                         INET_ECN_xmit(sk);
360                         if (tp->ecn_flags & TCP_ECN_QUEUE_CWR) {
361                                 tp->ecn_flags &= ~TCP_ECN_QUEUE_CWR;
362                                 tcp_hdr(skb)->cwr = 1;
363                                 skb_shinfo(skb)->gso_type |= SKB_GSO_TCP_ECN;
364                         }
365                 } else {
366                         /* ACK or retransmitted segment: clear ECT|CE */
367                         INET_ECN_dontxmit(sk);
368                 }
369                 if (tp->ecn_flags & TCP_ECN_DEMAND_CWR)
370                         tcp_hdr(skb)->ece = 1;
371         }
372 }
 51 static inline void INET_ECN_xmit(struct sock *sk)
 52 {
 53         inet_sk(sk)->tos |= INET_ECN_ECT_0;
 54         if (inet6_sk(sk) != NULL)
 55                 inet6_sk(sk)->tclass |= INET_ECN_ECT_0;
 56 }

2.3 路由器處理ECT報文

根據設計思路,路由器在認為發生擁塞時,給所有支持ECN協議的流打上CE標記,然而路由器如何判斷擁塞發生並沒有一個統一的標准,一般來說為平滑后的隊列長度超過一定閾值,以RED隊列為例,它維護一個隊列長度的移動平均值,在該值大於設置的閾值,之后以一定概率給過往的報文打上CE標記(沒有啟用ECN時為以一定概率丟棄報文)。

 59 static int red_enqueue(struct sk_buff *skb, struct Qdisc *sch)
 60 {
        ...
 72         switch (red_action(&q->parms, &q->vars, q->vars.qavg)) {    //根據平均隊列長度決定如何處理報文
 73         case RED_DONT_MARK:
 74                 break;
 75 
 76         case RED_PROB_MARK:                          //標記報文
 77                 sch->qstats.overlimits++;
 78                 if (!red_use_ecn(q) || !INET_ECN_set_ce(skb)) {     //沒有啟用ECN,或者打CE標記失敗則丟棄報文
 79                         q->stats.prob_drop++;
 80                         goto congestion_drop;            
 81                 }
 82 
 83                 q->stats.prob_mark++;
 84                 break;
 85 
 86         case RED_HARD_MARK:
 87                 sch->qstats.overlimits++;
 88                 if (red_use_harddrop(q) || !red_use_ecn(q) ||
 89                     !INET_ECN_set_ce(skb)) {
 90                         q->stats.forced_drop++;
 91                         goto congestion_drop;
 92                 }
 93 
 94                 q->stats.forced_mark++;
 95                 break;
 96         }
        ...

110 }

2.4 服務器端處理CE報文

服務器端在收到帶有CE標志的IP報文后,將套接字結構體tp->ecn_flags置TCP_ECN_DEMAND_CWR,並進入quick ack模式,之后所有ack報文都置有ECE標志,直到接收端接收到CWR報文后,取消TCP_ECN_DEMAND_CWR。

STEP1 : 轉入TCP_ECN_DEMAND_CWR狀態。具體流程為tcp_rcv_established -> tcp_event_data_recv -> TCP_ECN_check_ce,在TCP_ECN_check_ce中檢查報文是否包含CE標記,在遇到CE標記時轉入TCP_ECN_DEMAND_CWR狀態。

220 static inline void TCP_ECN_check_ce(struct tcp_sock *tp, const struct sk_buff *skb)
221 {
222         if (!(tp->ecn_flags & TCP_ECN_OK))
223                 return;
224 
225         switch (TCP_SKB_CB(skb)->ip_dsfield & INET_ECN_MASK) {
226         case INET_ECN_NOT_ECT:
227                 /* Funny extension: if ECT is not set on a segment,
228                  * and we already seen ECT on a previous segment,
229                  * it is probably a retransmit.
230                  */
231                 if (tp->ecn_flags & TCP_ECN_SEEN)
232                         tcp_enter_quickack_mode((struct sock *)tp);
233                 break;
234         case INET_ECN_CE:
235                 if (!(tp->ecn_flags & TCP_ECN_DEMAND_CWR)) {
236                         /* Better not delay acks, sender can have a very low cwnd */
237                         tcp_enter_quickack_mode((struct sock *)tp);    //進入quick ack模式,立即構造ack報文
238                         tp->ecn_flags |= TCP_ECN_DEMAND_CWR;        //在ecn_flags中添加TCP_ECN_DEMAND_CWR狀態
239                 }
240                 /* fallinto */
241         default:
242                 tp->ecn_flags |= TCP_ECN_SEEN;
243         }
244 }

STEP2 :構造ECE - ACK。在構造ACK報文時,tcp_transmit_skb 調用 TCP_ECN_send判斷是否處於TCP_ECN_DEMAND_CWR狀態,並決定是否在ACK報文中添加ECE標志。

350 static inline void TCP_ECN_send(struct sock *sk, struct sk_buff *skb,
351                                 int tcp_header_len)
352 {
353         struct tcp_sock *tp = tcp_sk(sk);
354 
355         if (tp->ecn_flags & TCP_ECN_OK) {
356                 /* Not-retransmitted data segment: set ECT and inject CWR. */
357                 if (skb->len != tcp_header_len &&
358                     !before(TCP_SKB_CB(skb)->seq, tp->snd_nxt)) {
359                         INET_ECN_xmit(sk);
360                         if (tp->ecn_flags & TCP_ECN_QUEUE_CWR) {
361                                 tp->ecn_flags &= ~TCP_ECN_QUEUE_CWR;
362                                 tcp_hdr(skb)->cwr = 1;
363                                 skb_shinfo(skb)->gso_type |= SKB_GSO_TCP_ECN;
364                         }
365                 } else {
366                         /* ACK or retransmitted segment: clear ECT|CE */
367                         INET_ECN_dontxmit(sk);
368                 }
369                 if (tp->ecn_flags & TCP_ECN_DEMAND_CWR)       //在CWR狀態時,給ACK報文添加ece標志 
370                         tcp_hdr(skb)->ece = 1;
371         }
372 }

STEP3 :退出TCP_ECN_DEMAND_CWR狀態。具體流程為tcp_rcv_established -> tcp_data_queue -> TCP_ECN_accept_cwr,在TCP_ECN_accept_cwr中,判斷接收到的報文中是否有CWR標志,並決定是否退出TCP_ECN_DEMAND_CWR狀態。

209 static inline void TCP_ECN_accept_cwr(struct tcp_sock *tp, const struct sk_buff *skb)
210 {
211         if (tcp_hdr(skb)->cwr)  //退出TCP_ECN_DEMAND_CWR狀態
212                 tp->ecn_flags &= ~TCP_ECN_DEMAND_CWR;
213 }

2.5 客戶端處理ECE報文

客戶端首先要根據ACK報文中的ECE調整TCP擁塞狀態機到TCP_CA_CWR狀態,並開始減小發送窗口,在擁塞發生之前的所有報文都被確認后,恢復TCP_CA_Open狀態,並向服務器端發送CWR終止服務器端的TCP_ECN_DEMAND_CWR,結束整個流程。

STEP1 :處理ACK報文,並確定是否包含ECE標志,流程為tcp_rcv_established -> tcp_ack 。

 

3360 static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
3361 {
        ...
3407         if (!(flag & FLAG_SLOWPATH) && after(ack, prior_snd_una)) {
          ...
3419 } else { 3420 if (ack_seq != TCP_SKB_CB(skb)->end_seq) 3421 flag |= FLAG_DATA; 3422 else 3423 NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPPUREACKS); 3424 3425 flag |= tcp_ack_update_window(sk, skb, ack, ack_seq); 3426 3427 if (TCP_SKB_CB(skb)->sacked) 3428 flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una, 3429 &sack_rtt_us); 3430 3431 if (TCP_ECN_rcv_ecn_echo(tp, tcp_hdr(skb)))  //確定ACK報文是否含有ECE標志 3432 flag |= FLAG_ECE;         3433 3434 tcp_ca_event(sk, CA_EVENT_SLOW_ACK); 3435 }
        ...
3508 }

 STEP2 : 調整擁塞控制狀態機。具體流程為tcp_rcv_established -> tcp_ack -> tcp_fastretrans_alert -> tcp_try_to_open,其中tcp_fastretrans_alert 函數為整個TCP擁塞控制狀態機的核心,而tcp_try_to_open函數則根據flag中是否包含FLAG_ECE標志,確定是否將擁塞狀態機調整為TCP_CA_CWR狀態。擁塞控制狀態機進入TCP_CA_CWR狀態后,協議棧需要調用tcp_enter_cwr函數來保存當前snd_nxt等重要的變量,之后發送窗口大致為每兩個ack減1。

2557 static void tcp_try_to_open(struct sock *sk, int flag, const int prior_unsacked)
2558 {
2559         struct tcp_sock *tp = tcp_sk(sk);
2560 
2561         tcp_verify_left_out(tp);
2562 
2563         if (!tcp_any_retrans_done(sk))
2564                 tp->retrans_stamp = 0;
2565 
2566         if (flag & FLAG_ECE)        //將擁塞控制狀態機從OPEN或REORDER狀態調整為CWR狀態
2567                 tcp_enter_cwr(sk, 1);
2568 
2569         if (inet_csk(sk)->icsk_ca_state != TCP_CA_CWR) {
2570                 tcp_try_keep_open(sk);
2571         } else {
2572                 tcp_cwnd_reduction(sk, prior_unsacked, 0);
2573         }
2574 }

STEP3 : 退出CWR狀態。上面介紹過Linux的擁塞控制狀態機主要由tcp_fastretrans_alert 控制,在STEP2中記錄的snd_nxt之前的報文都被確認后,擁塞狀態機也將退出TCP_CA_CWR狀態,並轉入TCP_CA_Open狀態。

2774 static void tcp_fastretrans_alert(struct sock *sk, const int acked,
2775                                   const int prior_unsacked,
2776                                   bool is_dupack, int flag)
2777 {
        ...
2801         /* D. Check state exit conditions. State can be terminated
2802          *    when high_seq is ACKed. */
2803         if (icsk->icsk_ca_state == TCP_CA_Open) {
2804                 WARN_ON(tp->retrans_out != 0);
2805                 tp->retrans_stamp = 0;
2806         } else if (!before(tp->snd_una, tp->high_seq)) {
2807                 switch (icsk->icsk_ca_state) {
2808                 case TCP_CA_CWR:
2809                         /* CWR is to be held something *above* high_seq
2810                          * is ACKed for CWR bit to reach receiver. */
2811                         if (tp->snd_una != tp->high_seq) {      //退出TCP_CA_Cwr
2812                                 tcp_end_cwnd_reduction(sk);
2813                                 tcp_set_ca_state(sk, TCP_CA_Open);
2814                         }
2815                         break;
2816 
                ...
2824 } 2825 }
        ...
2886 }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM