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 }