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从协议栈中肢解出来。

 #define TCP_ECN_OK              1  //套接字支持ECN协议
#define TCP_ECN_QUEUE_CWR 2  //发送端在接收到ECE报文后,设置该标志,并将拥塞状态机设置为TCP_CA_CWR状态
#define TCP_ECN_DEMAND_CWR 4  //接收端处于该状态,将在所有ACK报文中添加ECE,直到接收到CWR报文
#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握手失败后,该标志还可能被取消。

 /* Build a SYN and send it off. */
int tcp_connect(struct sock *sk)
{
        ...
TCP_ECN_send_syn(sk, buff);         ...
}
 /* 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 = ;
if (sock_net(sk)->ipv4.sysctl_tcp_ecn == ) {              //通过 /proc/sys/net/ipv4/tcp_ecn进行配置
TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_ECE | TCPHDR_CWR;    //SYN报文需要添加ECE和CWR
tp->ecn_flags = TCP_ECN_OK;                    //握手阶段ecn_flags设为支持ECN通信,如果握手失败TCP_ECN_OK会被取消
}
}

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标识。

 

 int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
                       const struct tcphdr *th, unsigned int len) 
{
        ...
switch (sk->sk_state) {
case TCP_CLOSE:
goto discard; case TCP_LISTEN:
if (th->ack)
return ; if (th->rst)
goto discard; if (th->syn) {
if (th->fin)
goto discard;
if (icsk->icsk_af_ops->conn_request(sk, skb) < )    //调用tcp_v4_conn_request
return ; kfree_skb(skb);
return ;
}
goto discard;

            ...
         case TCP_SYN_SENT:
5663 queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
         }
 int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
        ...
if (!want_cookie || tmp_opt.tstamp_ok)
TCP_ECN_create_request(req, skb, sock_net(sk));
        ... }
 static inline void
TCP_ECN_create_request(struct request_sock *req, const struct sk_buff *skb,
struct net *net)
{
const struct tcphdr *th = tcp_hdr(skb); if (net->ipv4.sysctl_tcp_ecn && th->ece && th->cwr &&        //服务端也配置了ECN,同时SYN报文中函授ECE和CWR
INET_ECN_is_not_ect(TCP_SKB_CB(skb)->ip_dsfield))
inet_rsk(req)->ecn_ok = ;                   //套接字设置支持ECN标识 
}

 STEP3 :客户端处理SYN-ACK报文,首先调用tcp_rcv_synsent_state_process,继而调用TCP_ECN_rcv_synack来完成ECN握手。

 int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{         ...
case TCP_SYN_SENT:
queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
}
        ...
5672 } 
 static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
            ...
TCP_ECN_rcv_synack(tp, th);
            ... }
 static inline void TCP_ECN_rcv_synack(struct tcp_sock *tp, const struct tcphdr *th)
{
if ((tp->ecn_flags & TCP_ECN_OK) && (!th->ece || th->cwr))          //SYN-ACK报文含有CWR或不含ECE则握手失败,客户端撤销TCP_ECN_OK
tp->ecn_flags &= ~TCP_ECN_OK;
}

2.2 客户端发送带有ECT的报文

所有支持ECN通信的流,在传输层 tcp_transmit_skb -> TCP_ECN_send -> INET_ECN_xmit的流程中都会打上ECT(0)标记。

 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 (tp->ecn_flags & TCP_ECN_OK) {      //ECN通信添加ECT
/* Not-retransmitted data segment: set ECT and inject CWR. */
if (skb->len != tcp_header_len &&
!before(TCP_SKB_CB(skb)->seq, tp->snd_nxt)) {
INET_ECN_xmit(sk);
if (tp->ecn_flags & TCP_ECN_QUEUE_CWR) {
tp->ecn_flags &= ~TCP_ECN_QUEUE_CWR;
tcp_hdr(skb)->cwr = ;
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 = ;
}
}
  static inline void INET_ECN_xmit(struct sock *sk)
{
inet_sk(sk)->tos |= INET_ECN_ECT_0;
if (inet6_sk(sk) != NULL)
inet6_sk(sk)->tclass |= INET_ECN_ECT_0;
}

2.3 路由器处理ECT报文

根据设计思路,路由器在认为发生拥塞时,给所有支持ECN协议的流打上CE标记,然而路由器如何判断拥塞发生并没有一个统一的标准,一般来说为平滑后的队列长度超过一定阈值,以RED队列为例,它维护一个队列长度的移动平均值,在该值大于设置的阈值,之后以一定概率给过往的报文打上CE标记(没有启用ECN时为以一定概率丢弃报文)。

  static int red_enqueue(struct sk_buff *skb, struct Qdisc *sch)
{
       ...
switch (red_action(&q->parms, &q->vars, q->vars.qavg)) {    //根据平均队列长度决定如何处理报文
case RED_DONT_MARK:
break; case RED_PROB_MARK:                          //标记报文
sch->qstats.overlimits++;
if (!red_use_ecn(q) || !INET_ECN_set_ce(skb)) {     //没有启用ECN,或者打CE标记失败则丢弃报文
q->stats.prob_drop++;
goto congestion_drop;            
} q->stats.prob_mark++;
break; case RED_HARD_MARK:
sch->qstats.overlimits++;
if (red_use_harddrop(q) || !red_use_ecn(q) ||
!INET_ECN_set_ce(skb)) {
q->stats.forced_drop++;
goto congestion_drop;
} q->stats.forced_mark++;
break;
}
       ... }

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状态。

 static inline void TCP_ECN_check_ce(struct tcp_sock *tp, const struct sk_buff *skb)
{
if (!(tp->ecn_flags & TCP_ECN_OK))
return; switch (TCP_SKB_CB(skb)->ip_dsfield & INET_ECN_MASK) {
case INET_ECN_NOT_ECT:
/* 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 */
if (tp->ecn_flags & TCP_ECN_SEEN)
tcp_enter_quickack_mode((struct sock *)tp);
break;
case INET_ECN_CE:
if (!(tp->ecn_flags & TCP_ECN_DEMAND_CWR)) {
/* Better not delay acks, sender can have a very low cwnd */
tcp_enter_quickack_mode((struct sock *)tp);    //进入quick ack模式,立即构造ack报文
tp->ecn_flags |= TCP_ECN_DEMAND_CWR;        //在ecn_flags中添加TCP_ECN_DEMAND_CWR状态
}
/* fallinto */
default:
tp->ecn_flags |= TCP_ECN_SEEN;
}
}

STEP2 :构造ECE - ACK。在构造ACK报文时,tcp_transmit_skb 调用 TCP_ECN_send来判断是否处于TCP_ECN_DEMAND_CWR状态,并决定是否在ACK报文中添加ECE标志。

 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 (tp->ecn_flags & TCP_ECN_OK) {
/* Not-retransmitted data segment: set ECT and inject CWR. */
if (skb->len != tcp_header_len &&
!before(TCP_SKB_CB(skb)->seq, tp->snd_nxt)) {
INET_ECN_xmit(sk);
if (tp->ecn_flags & TCP_ECN_QUEUE_CWR) {
tp->ecn_flags &= ~TCP_ECN_QUEUE_CWR;
tcp_hdr(skb)->cwr = ;
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)       //在CWR状态时,给ACK报文添加ece标志 
tcp_hdr(skb)->ece = ;
}
}

STEP3 :退出TCP_ECN_DEMAND_CWR状态。具体流程为tcp_rcv_established -> tcp_data_queue -> TCP_ECN_accept_cwr,在TCP_ECN_accept_cwr中,判断接收到的报文中是否有CWR标志,并决定是否退出TCP_ECN_DEMAND_CWR状态。

 static inline void TCP_ECN_accept_cwr(struct tcp_sock *tp, const struct sk_buff *skb)
{
if (tcp_hdr(skb)->cwr)  //退出TCP_ECN_DEMAND_CWR状态
tp->ecn_flags &= ~TCP_ECN_DEMAND_CWR;
}

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 。

 static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
        ...
if (!(flag & FLAG_SLOWPATH) && after(ack, prior_snd_una)) {
          ...
} else {
if (ack_seq != TCP_SKB_CB(skb)->end_seq)
flag |= FLAG_DATA;
else
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPPUREACKS); flag |= tcp_ack_update_window(sk, skb, ack, ack_seq); if (TCP_SKB_CB(skb)->sacked)
flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una,
&sack_rtt_us); if (TCP_ECN_rcv_ecn_echo(tp, tcp_hdr(skb)))  //确定ACK报文是否含有ECE标志
flag |= FLAG_ECE;         tcp_ca_event(sk, CA_EVENT_SLOW_ACK);
}
        ...
}

 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。

 static void tcp_try_to_open(struct sock *sk, int flag, const int prior_unsacked)
{
struct tcp_sock *tp = tcp_sk(sk); tcp_verify_left_out(tp); if (!tcp_any_retrans_done(sk))
tp->retrans_stamp = ; if (flag & FLAG_ECE)        //将拥塞控制状态机从OPEN或REORDER状态调整为CWR状态
tcp_enter_cwr(sk, ); if (inet_csk(sk)->icsk_ca_state != TCP_CA_CWR) {
tcp_try_keep_open(sk);
} else {
tcp_cwnd_reduction(sk, prior_unsacked, );
}
}

STEP3 : 退出CWR状态。上面介绍过Linux的拥塞控制状态机主要由tcp_fastretrans_alert 控制,在STEP2中记录的snd_nxt之前的报文都被确认后,拥塞状态机也将退出TCP_CA_CWR状态,并转入TCP_CA_Open状态。

 static void tcp_fastretrans_alert(struct sock *sk, const int acked,
const int prior_unsacked,
bool is_dupack, int flag)
{
        ...
/* D. Check state exit conditions. State can be terminated
2802 * when high_seq is ACKed. */
if (icsk->icsk_ca_state == TCP_CA_Open) {
WARN_ON(tp->retrans_out != );
tp->retrans_stamp = ;
} else if (!before(tp->snd_una, tp->high_seq)) {
switch (icsk->icsk_ca_state) {
case TCP_CA_CWR:
/* CWR is to be held something *above* high_seq
2810 * is ACKed for CWR bit to reach receiver. */
if (tp->snd_una != tp->high_seq) {      //退出TCP_CA_Cwr
tcp_end_cwnd_reduction(sk);
tcp_set_ca_state(sk, TCP_CA_Open);
}
break;                 ...
}
}
        ...
}

显式拥塞通告(ECN)及其在Linux上的实现的更多相关文章

  1. TCP/IP网络中的显式拥塞通告(ECN)

    当前的TCP 实现将TCP 端节点之间的中间网络视为一个不透明的"黑盒".TCP 包进入和流出这个盒子.有些时候进入盒子的包被丢失了.因为今天的数字和光媒体上出现比特级错误的机会非 ...

  2. 浅析SQL查询语句未显式指定排序方式,无法保证同样的查询每次排序结果都一致的原因

    本文出处:http://www.cnblogs.com/wy123/p/6189100.html 标题有点拗口,来源于一个开发人员遇到的实际问题 先抛出问题:一个查询没有明确指定排序方式,那么,第二次 ...

  3. C++中的显式类型转化

    类型转化也许大家并不陌生,int i; float j; j = (float)i; i = (int)j; 像这样的显式转化其实很常见,强制类型转换可能会丢失部分数据,所以如果不加(int)做强制转 ...

  4. 《Entity Framework 6 Recipes》中文翻译系列 (28) ------ 第五章 加载实体和导航属性之测试实体是否加载与显式加载关联实体

    翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 5-11  测试实体引用或实体集合是否加载 问题 你想测试关联实体或实体集合是否已经 ...

  5. 当 IDENTITY_INSERT 设置为 OFF 时,不能向表 'OrderList' 中的标识列插入显式值

    问题描述:在SQL SERVER 2008中,向数据表中字段插入数据时,会报错,错误如下: 当 IDENTITY_INSERT 设置为 OFF 时,不能向表 'OrderList' 中的标识列插入显式 ...

  6. 显式意图启动一个Activity

    显式意图主要是通过指定包名和类名开启一个组件,主要用于安全性要求高的,且不想被其他应用开启,可以不配置应用过滤器. 1.创建意图对象 Intent intent = new Intent(); 2.指 ...

  7. 转】C#接口-显式接口和隐式接口的实现

    [转]C#接口-显式接口和隐式接口的实现 C#中对于接口的实现方式有隐式接口和显式接口两种: 类和接口都能调用到,事实上这就是“隐式接口实现”. 那么“显示接口实现”是神马模样呢? interface ...

  8. sqlserver 插入数据时异常,仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'XXXXX.dbo.XXXXXXXXX'中的标识列指定显式值。

    INSERT INTO XXXXXXXXX.dbo.XXXXXXXXX select * from XXXXXXXXX 仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'X ...

  9. 当 IDENTITY_INSERT 设置为 OFF 时,不能向表 '#TT' 中的标识列插入显式值。 sql server 临时表

    当 IDENTITY_INSERT 设置为 OFF 时,不能向表 '#TT' 中的标识列插入显式值.我是在SqlServer写存储过程中遇到的这个错误,当时就心想:临时表怎么会有主键呢,我也没有设置主 ...

随机推荐

  1. Python模块1

    序列化模块: 将原本的字典.列表等内容转换成一个字符串的过程就叫做序列化. 序列化的目的 1.以某种存储形式使自定义对象持久化: 2.将对象从一个地方传递到另一个地方. 3.使程序更具维护性. jso ...

  2. js中call()的用法

    A.call(B,x,y) 1`改变函数A的this指向,使之指向B; 2` 把A函数放到B中运行,x和y是A函数的参数. //父类 Person     function Person() {   ...

  3. 让Visualstudio在win10下使用管理员方式运行

    https://www.cnblogs.com/zhaogaojian/p/10124629.html vs右键高级设置管理员运行后,每次直接运行使用的是管理员方式,但是如果直接在sln文件上点击使用 ...

  4. 20175312 2018-2019-2 《Java程序设计》第8周学习总结

    20175312 2018-2019-2 <Java程序设计>第8周学习总结 教材学习内容总结 已依照蓝墨云班课的要求完成了第十章的学习,主要的学习渠道是PPT,和书的课后习题. 总结如下 ...

  5. Spark机器学习基础二

    无监督学习 0.K-means from __future__ import print_function from pyspark.ml.clustering import KMeans #from ...

  6. GAN 旧照上色

    https://www.jiqizhixin.com/articles/2018-11-03-3

  7. linux下ifconfig命令看不到IP centos7——ens33

    当前环境VMware15+centos7  在终端输入ifconfig后没有开到IP地址: 解决方法:root用户执行命令 cd /etc/sysconfig/network-scripts/ vi ...

  8. Visual Studio 2017/2019 企业版 Enterprise 激活码

    VS2017 Enterprise: NJVYC-BMHX2-G77MM-4XJMR-6Q8QF VS2017 Professional: KBJFW-NXHK6-W4WJM-CRMQB-G3CDH ...

  9. js 克隆数组

    js克隆数组 1.遍历push 2.slice const a1 = [1, 2];const a2 = a1.slice() 3.concat() const a2 = a1.concat(); a ...

  10. Vuex状态管理模式

    Store:类似容器,包含应用的大部分状态,一个页面只能有一个store,状态存储是响应式的 State : 包含所有应用级别状态的对象 Getters : 在组件内部获取store中状态的函数 Mu ...