TCP定义了几个拥塞事件,当这些事件发生时,我们可以通过TCP的拥塞控制算法,调用自定义的处理函数,

来做一些额外的事情的。也就是说,我们可以很简便的参与到TCP对拥塞事件的处理过程中。

Author:zhangskd @ csdn blog

TCP的拥塞事件集:

/* Events passed to congestion control interface */

enum tcp_ca_event {
    CA_EVENT_TX_START, /* first transmit when no packets in flight */
    CA_EVENT_CWND_RESTART, /* congestion window restart */
    CA_EVENT_COMPLETE_CWR, /* end of congestion recovery */
    CA_EVENT_FRTO, /* fast recovery timeout */
    CA_EVENT_LOSS, /* loss timeout */
    CA_EVENT_FAST_ACK, /* in sequence ack */
    CA_EVENT_SLOW_ACK, /* other ack */
};

钩子函数定义:

struct tcp_congestion_ops {
    ...

    /* call when cwnd event occurs (optional) */
    void (*cwnd_event) (struct sock *sk, enum tcp_ca_event ev);

    ...
};

封装调用:

static inline void tcp_ca_event (struct sock *sk, const enum tcp_ca_event event)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);

    if (icsk->icsk_ca_ops->cwnd_event)
        icsk->icsk_ca_ops->cwnd_event(sk, event);
}

CA_EVENT_TX_START

当发送一个数据包时,如果网络中无发送且未确认的数据包,则触发此事件。

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask)
{
    ...

    if (tcp_packets_in_flight(tp) == 0) {
        tcp_ca_event(sk, CA_EVENT_TX_START);
        skb->ooo_okay = 1; /*此时发送队列可以改变,因为上面没有数据包 */

    } else
        skb->ooo_okay = 0;
    ...
}

CA_EVENT_CWND_RESTART

发送方在发送数据包时,如果发送的数据包有负载,则会检测拥塞窗口是否超时。

如果超时,则会使拥塞窗口失效并重新计算拥塞窗口,同时触发CA_EVENT_CWND_RESTART事件。

/* Congestion state accounting after a packet has been sent. */

static void tcp_event_data_sent(struct tcp_sock *tp, struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    const u32 now = tcp_time_stamp;

    if (sysctl_tcp_slow_start_after_idle &&
        (! tp->packets_out && (s32) (now - tp->lsndtime) > icsk->icsk_rto))
        tcp_cwnd_restart(sk, __sk_dst_get(sk)); /* 重置cwnd */

    tp->lsndtime = now; /* 更新最近发包的时间*/

    /* If it is a reply for ato after last received packet, enter pingpong mode. */
    if ((u32) (now - icsk->icsk_ack.lrcvtime) < icsk->icsk_ack.ato)
        icsk->icsk_ack.pingpong = 1;
}

tcp_event_data_sent()中,符合三个条件才重置cwnd:

(1)tcp_slow_start_after_idle选项设置,这个内核默认置为1

(2)tp->packets_out == 0,表示网络中没有未确认数据包

(3)now - tp->lsndtime > icsk->icsk_rto,距离上次发送数据包的时间超过了RTO

/* RFC2861. Reset CWND after idle period longer than RTO to "restart window".
 * This is the first part of cwnd validation mechanism.
 */

static void tcp_cwnd_restart(struct sock *sk, const struct dst_entry *dst)
{
    struct tcp_sock *tp = tcp_sk(sk);
    s32 delta = tcp_time_stamp - tp->lsndtime; /* 距离上次发包的时间*/
    u32 restart_cwnd = tcp_init_cwnd(tp, dst);
    u32 cwnd = tp->snd_cwnd;

    tcp_ca_event(sk, CA_EVENT_CWND_RESTART); /* 在这里!触发拥塞窗口重置事件*/
    tp->snd_ssthresh = tcp_current_ssthresh(sk); /* 保存阈值,并没有重置*/
    restart_cwnd = min(restart_cwnd, cwnd);

    /* 闲置时间每超过一个RTO且cwnd比重置后的大时,cwnd减半。*/
    while((delta -= inet_csk(sk)->icsk_rto) > 0 && cwnd > restart_cwnd)
        cwnd >>= 1;

    tp->snd_cwnd = max(cwnd, restart_cwnd);
    tp->snd_cwnd_stamp = tcp_time_stamp;
    tp->snd_cwnd_used = 0;
}

如果需要更详细了解TCP拥塞控制窗口的有效性验证机制,可见之前的blog。

CA_EVENT_COMPLETE_CWR

当退出CWR状态,或者退出Recovery状态时,会调用tcp_complete_cwr()来设置拥塞窗口,这个时候

会触发CA_EVENT_COMPLETE_CWR来通知拥塞控制模块:“我已经停止减小拥塞窗口了!如果你想

再做点什么补充,就是现在!”

static inline void tcp_complete_cwr(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    /* Do not moderate cwnd if it's already undone in cwr or recovery. */
    if (tp->undo_marker) {
        if (inet_csk(sk)->icsk_ca_state == TCP_CA_CWR)
            tp->snd_cwnd = min(tp->snd_cwnd, tp->snd_ssthresh);
        else /* PRR */
            tp->snd_cwnd = tp->snd_ssthresh;

        tp->snd_cwnd_stamp = tcp_time_stamp;
    }

    /* 在这里设置拥塞窗口和慢启动阈值会覆盖掉ssthresh()的设置*/
    tcp_ca_event(sk, CA_EVENT_COMPLETE_CWR);
}

CA_EVENT_FRTO

启用F-RTO时,发生超时后,首先会进行F-RTO处理,看看这个超时是不是虚假的,如果不是的话

再进行传统的超时重传。这时候会减小慢启动阈值,而拥塞窗口暂时保持不变。

/* RTO occurred, but do not yet enter Loss state. Instead, defer RTO recovery a bit and use
 * heuristics in tcp_process_frto() to detect if the RTO was spurious.
 */

void tcp_enter_frto(struct sock *sk)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;

    if ((! tp->frto_counter && icsk->icsk_ca_state <= TCP_CA_Disorder) ||
        tp->snd_una == tp->high_seq ||
        ((icsk->icsk_ca_state == TCP_CA_Loss || tp->frto_counter) &&
         ! icsk->icsk_retransmits)) {

        tp->prior_ssthresh = tcp_current_ssthresh(sk); /* 保留旧阈值*/

        if (tp->frto_counter) { /* 这种情况非常罕见*/
            u32 stored_cwnd;
            stored_cwnd = tp->snd_cwnd;
            tp->snd_cwnd = 2;
            tp->snd_ssthresh = icsk->icsk_ca_ops->ssthresh(sk);
            tp->snd_cwnd = stored_cwnd;

        } else {
            tp->snd_ssthresh = icsk->icsk_ca_ops->ssthresh(sk); /* 重新设置慢启动阈值*/
        }

        tcp_ca_event(sk, CA_EVENT_FRTO); /* 这里设置慢启动阈值会覆盖掉ssthresh()的设置*/
    }
    ...
}

关于F-RTO的机制可参考之前的blog。

CA_EVENT_LOSS

上面我们说到,如果超时不是虚假的话,就会进入超时重传,也就是TCP_CA_Loss状态。

/* Enter Loss state. If "how" is not zero, forget all SACK information and reset tags completely,
 * otherwise preserve SACKs. If receiver dropped its ofo queue, we will know this due to
 * reneging detection.
 */

void tcp_enter_loss(struct sock *sk, int how)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;

    /* Reduce ssthresh if it has not yet been made inside this window.
     * 要么是从Open或Disorder状态进入Loss状态,要么是在Loss状态又发生了超时:)
     * 我们知道在CWR或Recovery状态中可以以进入Loss,但在那两个状态中阈值已经被重置过了。
     */
    if (icsk->icsk_ca_state <= TCP_CA_Disorder || tp->snd_una == tp->high_seq ||
        (icsk->icsk_ca_state == TCP_CA_Loss && ! icsk->icsk_retransmits)) {

        tp->prior_ssthresh = tcp_current_ssthresh(sk); /* 保存旧阈值*/
        tp->snd_ssthresh = icsk->icsk_ca_ops->ssthresh(sk); /* 重新设置慢启动阈值*/

        tcp_ca_event(sk, CA_EVENT_LOSS); /* 这里设置慢启动阈值会覆盖掉ssthresh()的设置*/
    }

    tp->snd_cwnd = 1;
    tp->snd_cwnd_cnt = 0;
    tp->snd_cwnd_stamp = tcp_time_stamp;
    ...
}

CA_EVENT_FAST_ACK

如果我们收到符合预期的ACK,那么就进入快速路径的处理流程,在tcp_ack()中进行负荷无关的处理,

同时触发CA_EVENT_FAST_ACK事件。

static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
    ...
    /* 如果处于快速路径中*/
    if (! (flag & FLAG_SLOWPATH) && after(ack, prior_snd_una)) {

        /* Window is constant, pure forward advance.
         * No more checks are required.
         */
        tcp_update_w1(tp, ack_seq); /*记录更新发送窗口的ACK段序号*/
        tp->snd_una = ack; /* 更新发送窗口左端 */
        flag |= FLAG_WIN_UPDATE; /* 设置发送窗口更新标志 */

        tcp_ca_event(sk, CA_EVENT_FAST_ACK); /* 快速路径拥塞事件钩子*/

        NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPACKS);
    }
   ...
}

CA_EVENT_SLOW_ACK

如果我们收到不符合预期的ACK,那么就不能走快速路径,而必须经过全面的检查,即进入慢速路径的

处理流程。同样在tcp_ack()中进行负荷无关的处理,同时触发CA_EVENT_SLOW_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; /* 此ACK携带负荷*/
        else
            NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPPUREACKS);

        flag |= tcp_ack_update_window(sk, skb, ack, ack_seq); /* 更新发送窗口*/

         /* 根据SACK选项标志重传队列中SKB的记分牌状态*/
        if (TCP_SKB_CB(skb)->sacked)
            flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una);

        /* 查看ACK是否携带ECE标志 */
        if (TCP_ECN_rcv_ecn_echo(tp, tcp_hdr(skb)))
            flag |= FLAG_ECE;

        tcp_ca_event(sk, CA_EVENT_SLOW_ACK); /* 慢速路径拥塞事件钩子*/
    }
    ...
}

阈值的设置

用拥塞算法的ssthresh()来设置慢启动阈值tp->snd_ssthresh。

(1)tcp_enter_cwr

进入CWR状态时。

Set slow start threshold and cwnd not falling to slow start.

(2)tcp_enter_frto

进入FRTO处理时。

(3)tcp_enter_loss

进入Loss状态时。

(4)tcp_fastretrans_alert

进入Recovery状态时。

可见ssthresh()的调用时机是在进入CWR、FRTO、Loss、Recovery这几个异常状态时。

tp->snd_ssthresh的使用:

(1)在进入CWR、FRTO、Loss、Recovery时调用ssthresh()重新设置,在退出这些状态时,作为慢启动阈值。

(2)作为tcp_cwnd_min()的返回值,在tcp_cwnd_down()中被调用,而tcp_cwnd_down()在CWR和Recovery

状态中被调用。

(3)退出CWR、Recovery状态时,赋值给tp->snd_cwnd,避免进入慢启动。

使用钩子参与到TCP拥塞事件的处理中的更多相关文章

  1. TCP拥塞控制算法 优缺点 适用环境 性能分析

    [摘要]对多种TCP拥塞控制算法进行简要说明,指出它们的优缺点.以及它们的适用环境. [关键字]TCP拥塞控制算法 优点    缺点   适用环境公平性 公平性 公平性是在发生拥塞时各源端(或同一源端 ...

  2. 网络拥塞控制(三) TCP拥塞控制算法

    为了防止网络的拥塞现象,TCP提出了一系列的拥塞控制机制.最初由V. Jacobson在1988年的论文中提出的TCP的拥塞控制由“慢启动(Slow start)”和“拥塞避免(Congestion  ...

  3. TCP拥塞控制算法内核实现剖析(十)

    内核版本:3.2.12 主要源文件:linux-3.2.12/ net/ ipv4/ tcp_veno.c 主要内容:Veno的原理和实现 Author:zhangskd @ csdn blog 概要 ...

  4. TCP拥塞处理—Congestion Handing

      TCP拥塞处理-Congestion Handing 1 慢启动 2 拥塞避免 3 快重传/拥塞发生(拥塞发生时的快速重传) 4 快恢复

  5. “net.tcp://localhost:9000/ObtainData”处带有协定“&quot;IObtainData&quot;”的 ChannelDispatcher 无法打开其 IchannelListener。

    http://stackoverflow.com/questions/1252791/how-to-solve-the-channeldispatcher-is-unable-to-open-its- ...

  6. 【Win 10 应用开发】TCP通信过程

    基于TCP协议的通信,估计大伙儿都不陌生的,以前玩.net或玩C++的时候应该玩得很多吧.现在老周简单介绍一下在RT中如何用. TCP是基于连接的,所以,肯定有一方是监听者,通常称服务端或服务器,它负 ...

  7. js 停止事件冒泡 阻止浏览器的默认行为(阻止超连接 # )

    在前端开发工作中,由于浏览器兼容性等问题,我们会经常用到“停止事件冒泡”和“阻止浏览器默认行为”. 1..停止事件冒泡 JavaScript代码 //如果提供了事件对象,则这是一个非IE浏览器if ( ...

  8. jQuery停止事件冒泡

    event.stopPropagation(); 在jQuery中提供了stopPropagation()方法来停止事件冒泡.终止事件在传播过程的捕获.目标处理或起泡阶段进一步传播.调用该方法后,该节 ...

  9. SpingMVC 核心技术帮助文档

    声明:本篇文档主要是用于参考帮助文档,没有实例,但几乎包含了SpringMVC 4.2版本的所有核心技术,当前最新版本是4.3,4.2的版本已经经是很新的了,所以非常值得大家一读,对于读完这篇文档感觉 ...

随机推荐

  1. 由于log太多导致ubuntu硬盘空间满了,进入不了系统解决办法

    具体现象是在图形界面输入用户名和密码之后,再次提示需要输入用户名和密码. 步骤一:按快捷键进入命令行界面.ctrl+alt+f1. 步骤二:清空文件 clear log cd /var/log sud ...

  2. oralce 密码长度

    Oracle 11G的新特性所致, Oracle 11G创建用户时缺省密码过期限制是180天, 如果超过180天用户密码未做修改则该用户无法登录. Oracle提示错误消息ORA-28001: the ...

  3. C++11新特性总结 (一)

    1. 概述 最近在看C++ Primer5 刚好看到一半,总结一下C++11里面确实加了很多新东西,如果没有任何了解,别说自己写了,看别人写的代码估计都会有些吃力.C++ Primer5是学习C++1 ...

  4. 简单的解释XSS攻击

    XSS 跨站点脚本 cross site script 怎么造成攻击? 举例:有一个公共的页面,所有用户都可以访问且可以保存内容,输入的时候若输入<script>alert('I am h ...

  5. 关于实现自定义Dialog和实现Dialog里view的事件监听的两种方法

    一.自定义dialog. 二.实现dialog里view的事件监听 1.自定义dialog比较简单.在实例化new的时候,加入样式,布局就行了.或者重写dialog. 2.实现dialog里view的 ...

  6. windows定时执行百度新闻爬虫

    想要做个新闻文本识别分类的项目,就先写了个爬取百度新闻的爬虫. 环境:win7 32 bit python3.4 若干第三方库 可以实现的功能:定期按照百度新闻的分类抓取新闻的标题,所属类别及文本内容 ...

  7. Android打包签名

    Ⅰ.用jdk和sdk自带工具打包签名 a.把jdk下的keytool.exe和jarsigner.exe所在目录(两个工具在同一目录) 添加到环境变量path 1)新建环境变量package,pack ...

  8. javascript 自定义类型 属性,方法

    <html> <head> <script type="text/javascript"> function member(name,gende ...

  9. BIP_开发案例07_将原有Report Builer报表全部转为XML Publisher形式(案例)

    2014-05-31 Created By BaoXinjian

  10. csu oj 1804: 有向无环图 (dfs回溯)

    题目链接:http://acm.csu.edu.cn/OnlineJudge/problem.php?id=1804 中文题意就不说了. dfs从底到根回溯即可,看代码应该能清楚. //#pragma ...