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. SAP如何修改表的数据

     修改表: 事务代码:se16n  输入表名字   输入 /h 进入维护模式  修改 GD-EDIT 和 GD-SAPEDIT  内容为大写X.                          se ...

  2. iOS 与 Swift 方法互相调用

    1.OC调用swift 在OC项目中创建一个swift文件时,Xcode会提示 需要创建一个桥接文件,点击确定创建桥接文件,Xcode会自动创建一个桥接文件,名字:工程名-Bridging-Heade ...

  3. HTML与CSS的一些知识(三)

    CSS: 1.三大样式:行内(内嵌).内部(内联).外部(外联):基本都知道. 2.三大特性: a.继承性:父级样式会被子级继承(!important不会被继承,<a></a> ...

  4. [ZOJ 4014] Pretty Matrix

    题目链接:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=5742 AC代码: /* * 反思: * 1.遇到简单题别激动,先把它 ...

  5. MySql中not in的优化

    最近项目上用select查询时使用到了not in来排除用不到的主键id一开始使用的sql如下: select s.SORT_ID, s.SORT_NAME, s.SORT_STATUS, s.SOR ...

  6. Redis5.0+哨兵模式+Keepalived实现高可用

    Redis主备配置 原理: 从服务器向主服务器发出SYNC指令,当主服务器接到此命令后,就会调用BGSAVE指令来创建一个子进程专门进行数据持久化工作,也就是将主服务器的数据写入RDB文件中.在数据持 ...

  7. fiddler -- 一个强大的抓包工具

    一.fiddler常用功能: 1. Fiddler 是位于客户端和服务器端的http代理,也是目前最常用的http抓包工具之一.它能够记录客户端和服务器之间的所有http请求,可以针对特定的http请 ...

  8. document.documentElement.clientHeight和document.body.clientHeight区别

    首先了解下document HTML DOM 节点 在 HTML DOM (Document Object Model) 中 , 每一个元素都是 节点: 文档是一个文档节点. 所有的HTML元素都是元 ...

  9. BP neural network optimized by PSO algorithm on Ammunition storage reliability prediction 阅读笔记

    1.BP neural network optimized by PSO algorithm on Ammunition storage reliability prediction 文献简介文献来源 ...

  10. cookie与sessionStorage机制

    sessionStorage.localStorage和cookie的区别 共同点:都是保存在浏览器端.且同源的 区别: 1.cookie数据始终在同源的http请求中携带(即使不需要),即cooki ...