显式拥塞通知ECN剖析
概述
“Instead of inferring congestion from the lost packets, Explicit Congestion Notification (ECN) was
suggested for routers to explicitly mark packets when they arrive to a congested point in the network.
When the TCP sender receives an echoed ECN notification from the receiver, it should reduce its
transmission rate to mitigate the congestion in the network. ECN allows the TCP senders to be
congestion-aware without necessarily suffering from packet losses.”
Author:zhangskd @ csdn
基本原理
路由器在出现拥塞时通知TCP。当TCP段传递时,路由器使用IP首部中的2位来记录拥塞,当TCP段到达后,
接收方知道报文段是否在某个位置经历过拥塞。然而,需要了解拥塞发生情况的是发送方,而非接收方。因
此,接收方使用下一个ACK通知发送方有拥塞发生,然后,发送方做出响应,缩小自己的拥塞窗口。
IP层对ECN的支持
在网络层,一个发送主机必须能够表明自身能支持ECN与否,路由器在转发时必须能够表明它正在经历拥塞。
IP首部中的8位服务类型域(TOS)原先在RFC791中被定义为发送优先级、时延、吞吐量、可靠性和消耗性
等特征。在RFC2474中被重新定义为包含一个6位的区分服务码点(DSCP)和两个未使用的位。DSCP值
表明一个在路由器上配置的队列的和队列相关联的发送优先级。IP对ECN的支持用到了TOS域中剩下的两位。
在include/ net/ inet_ecn.h中,
enum { INET_ECN_NOT_ECT = 0, /* TOS后两位为:00,表示不支持ECN*/ INET_ECN_ECT_1 = 1, /* TOS后两位为:01,表示支持ECN */ INET_ECN_ECT_0 = 2, /* TOS后两位为:10,也表示支持ECN*/ INET_ECN_CE = 3, /* TOS后两位为11,表示路由器发生了拥塞*/ INET_ECN_MASK = 3, /* ECN域的掩码*/ };
一个支持ECN的主机发送数据包时将ECN域设置为01或者10。对于支持ECN的主机发送的包,如果路径
上的路由器支持ECN并且经历拥塞,它将ECN域设置为11。如果该数值已经被设置为11,那么下游路由
器不会修改该值。
当路由器检测到拥塞时,设置ECN域为11。
static inline IP_ECN_set_ce(struct iphdr *iph) { u32 check = (__force__ u32) iph->check; u32 ecn = (iph->tos + 1) & INET_ECN_MASK; /* * After the last operation we have (in binary) : * INET_ECN_NOT_ECT = 01 * INET_ECN_ECT_1 = 10 * INET_ECN_ECT_0 = 11 * INET_ECN_CE = 00 */ if (! (ecn & 2) ) /*不支持ECN的返回0。已经设置拥塞的不重复设置,返回。*/ return !ecn; /* * The following gives us : * INET_ECN_ECT_1 => check += htons(oxFFFD) * INET_ECN_ECT_0 => check += htons(oxFFFE) */ check += (__force u16) htons(oxFFFB) + (__force u16) htons(ecn) ; iph->check = (__force __sum16) (check + (check >= 0xFFFF) ); iph->tos |= INET_ECN_CE; /* 把ECN域设置为11,表示发生了拥塞*/ return 1; }
检验ECN域是否表示拥塞:
static inline int INET_ECN_is_ce(__u8 dsfield) { return (dsfield & INET_ECN_MASK) == INET_ECN_CE; }
不支持ECN:
static inline int INET_ECN_is_not_ect(__u8 dsfield) { return (dsfield & INET_ECN_MASK) == INET_ECN_NOT_ECT; }
清除ECN标志:
static inline void IP_ECN_clear(struct iphdr *iph) { iph->tos &= ~INET_ECN_MASK; }
TCP层对ECN的支持
TCP使用6为保留位(Reserved)的后两位来支持ECN。两个新的标志CWR、ECE含义如下:
ECE有两个作用,在TCP三次握手时表明TCP端是否支持ECN;在传输数据时表明接收到的
TCP段的IP首部的ECN被设置为11,即接收端发现了拥塞。
CWR为发送端缩小拥塞窗口标志,用来通知接收端它已经收到了设置ECE标志的ACK。
当两个支持ECN的TCP端进行TCP连接时,它们交换SYN、SYN+ACK、ACK段。对于支持ECN
的TCP端来说,SYN段的ECE和CWR标志都被设置了,SYN的ACK只设置ECE标志。
从tcp连接上控制:
struct tcp_sock { ... u8 ecn_flags; /* ECN status bits.*/ ... } #define TCP_ECN_OK 1 /* 本端支持ECN */ #define TCP_ECN_QUEUE_CWR 2 /* 本端被通知了拥塞,此时作为发送方*/ #define TCP_ECN_DEMAND_CWR 4 /* 通知对端发生了拥塞,此时作为接收方*/
从数据包上控制:
struct tcp_skb_cb { ... __u8 flags; /* TCP header flags.*/ ... } #define TCPHDR_FIN 0x01 #define TCPHDR_SYN 0x02 #define TCPHDR_RST 0x04 #define TCPHDR_PSH 0x08 #define TCPHDR_ACK 0x10 #define TCPHDR_URG ox20 #define TCPHDR_ECE ox40 #define TCPHDR_CWR ox80
SYN包对ECN的处理:
/* Packet ECN state for a SYN */ static inline void TCP_ECN_send_syn(struct sock *sk, struct sk_buff *skb) { struct tcp_sock *tp = tcp_sk(sk); tp->ecn_flags = 0; if (sysctl_tcp_ecn == 1) { /* ecn在/proc/sys中被设置了*/ TCP_SKB_CB(skb)->flags |= TCPHDR_ECE | TCPHDR_CWR; /*对SYN数据包设置*/ tp->ecn_flags = TCP_ECN_OK; /*对TCP连接设置*/ } }
SYNACK包对ECN的处理:
static __inline__ void TCP_ECN_make_synack(struct request_sock *req, struct tcphdr *th) { if (inet_rsk(req)->ecn_ok) th->ece = 1; }
/* Packet ECN state for a SYN-ACK */ static inline void TCP_ECN_send_synack(struct tcp_sock *tp, struct sk_buff *skb) { TCP_SKB_CB(skb)->flags &= ~TCPHDR_CWR; if (! (tp->ecn_flags & TCP_ECN_OK) ) TCP_SKB_CB(skb)->flags &= ~TCPHDR_ECE; }
处理普通的数据包:
在tcp_transmit_skb中,
if (likely( (tcb->flags & TCPHDR_SYN) == 0) )/*表示是普通数据包,不含SYN标志*/ TCP_ECN_send(sk, skb, tcp_header_size);
/* Set up ECN state for a packet on an ESTABLISHED socket that is about to be sent. */ static inline void TCP_ECN_send(struct sock *sk, struct sk_buff *skb, int tcp_header_len) { struct tcp_sock *tp = tcp_sk(sk); if (tcp->ecn_flags & TCP_ECN_OK) { /* 此TCP连接支持ECN*/ /* Not-retransmitted data segment: set ECT and inject CWR. */ if (skb->len != tcp_header_len && !before(TCP_SKB_CB(skb)->seq, tp->snd_nxt) ) { /*不是ACK和重传的数据包*/ INET_ECN_xmit(sk); /* 设置数据包ECN域,表示支持*/ if (tp->ecn_flags & TCP_ECN_QUEUE_CWR) { /*发送端已收到拥塞信号*/ tp->ecn_flags &= ~TCP_ECN_QUEUE_CWR; tcp_hdr(skb)->cwr = 1; skb_shinfo(skb)->gso_type |= SKB_GSO_TCP_ECN; } } else { /* ACK or retransmitted segment : clear ECT | CE */ INET_ECN_dontxmit(sk); } if (tp->ecn_flags & TCP_ECN_DEMAND_CWR) /*作为接收端,要通知发送端*/ tcp_hdr(skb)->ece = 1; } } #define INET_ECN_xmit(sk) do { inet_sk(sk)->tos |= INET_ECN_ECT_0; } #define INET_ECN_dontxmit(sk) do { inet_sk(sk)->tos &= ~INET_ECN_MASK; } while(0)
处理流程
一个支持ECN的TCP主机在支持ECN的TCP连接上发送设置了IP首部为10或者01的TCP段。
支持ECN的路由器在经历拥塞时设置IP首部的ECN域为11。当一个TCP接收端发送针对收到一个设置
ECN位为11的TCP段的响应时,它设置TCP首部中的ECE标志,并且在接下来的ACK中也做同样设置。
当发送主机接收到了设置ECE标志的ACK时,它就像感知到丢包一样,开始减小发送窗口。在下一个
数据包中,发送者设置CWR标志。在接收到新的设置CWR标志的包时,接收着停止在接下来的ACK
中设置ECE标志。
结尾
关于对ECN实际性能,以及其优缺点的分析,在接下来的文章中会有阐述↖(^ω^)↗。