主要内容:TCP的ACK发送方式,以及ACK发送状态转换机的实现。

内核版本:3.15.2

我的博客:http://blog.csdn.net/zhangskd

概述

TCP采用两种方式来发送ACK:快速确认和延迟确认。

在快速确认模式中,本端接收到数据包后,会立即发送ACK给对端。

在延迟确认模式中,本端接收到数据包后,不会立即发送ACK给对端,而是等待一段时间,如果在此期间:

1. 本端有数据包要发送给对端。就在发送数据包的时候捎带上此ACK,如此一来就节省了一个报文。

2. 本端没有数据包要发送给对端。延迟确认定时器会超时,然后发送纯ACK给对端。

在具体实现中,用pingpong来区分这两种模式:

icsk->icsk_ack.pingpong == 0,表示使用快速确认。

icsk->icsk_ack.pingpong == 1,表示使用延迟确认。

Q:那么问题来了,为什么用“乒乓球”来标志这两种模式?

A:我们知道打乒乓球时,球是双向来回跳动的。这比喻传输是双向的,你发送数据给我,我也发送数据给你,应用是交互型的。

在这种情况下,可以让数据包捎带ACK,以减少纯ACK的发送,降低不必要的流量开销。

如果不是“乒乓球模式”,即传输是单向的,一方只发送数据,另一方只接收数据。这种情况下,接收方因为没有数据要发送,

不能够捎带ACK,所以不能使用延迟确认,应该使用快速确认。

以上只是为了说明pingpong的含义,在实际中到底使用哪种模式,还会受到其它因素的影响。

总的来说,快速确认模式是用于比较紧急的场景,此时需要立即通知对端,比如收到异常的数据报、接收窗口显著增大了。

延迟确认模式则希望通过减少纯ACK的发送,来降低不必要的流量开销,所以此时要求数据的传输是双向的。

在实际的传输过程中,会根据当时的场景来判断是使用快速确认模式还是延迟确认模式,因此ACK的发送模式并不是

固定的,而是在这两种模式之间动态切换。

Q:什么时候进行快速确认?

(1) 接收到数据包,检查是否需要发送ACK时 (__tcp_ack_snd_check):

1. 接收缓冲区中有一个以上的全尺寸数据段仍然是NOT ACKed,并且接收窗口变大了。

所以一般收到了两个数据包后,会发送ACK,而不是对每个数据包都进行确认。

2.  接收到数据包时,处于快速确认模式中。

3. 接收到数据包时,乱序队列不为空。

(2) 当接收队列中有数据复制到用户空间时,会判断是否要立即发送ACK (tcp_clean_rbuf):

如果现在有ACK需要发送,满足以下条件之一,就可以立即发送:

1. icsk->icsk_ack.blocked为1,之前有Delayed ACK被用户进程阻塞了。

2. 接收缓冲区中有一个以上的全尺寸数据段仍然是NOT ACKed (所以经常是收到2个全尺寸段后发送ACK)

3. 本次复制到用户空间的数据量大于0,且满足以下条件之一:

3.1 设置了ICSK_ACK_PUSHED2标志

3.2 设置了ICSK_ACK_PUSHED标志,且处于快速确认模式中

如果原来没有ACK需要发送,但是现在的接收窗口显著增大了,也需要立即发送ACK通知对端。

这里的显著增大是指:新的接收窗口大小不为0,且比原来接收窗口的剩余量增大了一倍。

(3) 接收到数据包的事件处理 (tcp_event_data_recv):数据包含有路由器的显式拥塞通知,进入快速确认模式。

(4) 设置TCP_QUICKACK选项之后:进入快速确认模式,并立即发送一个ACK。

(5) 如果接收到的段有负荷,且其中一部分之前已经接收过了,则认为是Delayed ACK丢失,进入快速确认模式。

Q:什么时候进行延迟确认?

1. 快速确认模式中的ACK额度用完了,一般在快速确认了半个接收窗口的数据后,进入延迟确认模式。

2. 发送ACK时,因为内存分配失败,启动延迟确认定时器。

3. 接收到数据包,检查是否需要发送ACK时(__tcp_ack_snd_check),如果无法进行快速确认。

4. 使用TCP_QUICKACK选项禁用快速确认,设置的值为0。

数据结构

icsk->icsk_ack中的变量,用于控制快速确认和延迟确认。

struct inet_connection_sock {
...
struct {
/* ACK is pending.
* ACK的发送状态标志,可以表示四种情况:
* 1. ICSK_ACK_SCHED:目前有ACK需要发送
* 2. ICSK_ACK_TIMER:延迟确认定时器已经启动
* 3. ICSK_ACK_PUSHED:如果处于快速确认模式,允许立即发送ACK
* 4. ICSK_ACK_PUSHED2:无论是否处于快速确认模式,都可以立即发送ACK
*/
__u8 pending; /* Scheduled number of quick acks.
* 快速确认模式下,最多能够发送多少个ACK,额度用完以后就退出快速确认模式。
*/
__u8 quick; /* The session is interactive.
* 值为1时,为延迟确认模式;值为0时,为快速确认模式。
* 注意这个标志是不是永久性的,而是动态变更的。
*/
__u8 pingpong; /* Delayed ACK was blocked by socket lock.
* 如果延迟确认定时器触发时,发现socket被用户进程锁住,就把blocked置为1。
* 之后在接收到新数据、或者将数据复制到用户空间之后、或者再次超时时,会马上发送ACK。
*/
__u8 blocked; /* Predicted tick of soft clock.
* ACK的超时时间,是一个中间变量,根据接收到数据包的时间间隔来动态调整。
* 用来计算延迟确认定时器的超时时间timeout。
*/
__u32 ato; /* Currently scheduled timeout.
* 延迟确认定时器的超时时刻。
*/
unsigned long timeout; /* timestamp of last incoming segment.
* 最后一次收到带负荷的报文的时间点。
*/
__u32 lrcvtime;
__u16 last_seg_size; /* Size of last incoming segment */
__u16 rcv_mss; /* MSS used for delayed ACK decisions */
} icsk_ack;
...
};

icsk.icsk_ack.pending是ACK的发送状态标志,用于表示是否有ACK需要发送,以及发送的紧急程度。

enum inet_csk_ack_state_t {
ICSK_ACK_SCHED = 1, /* 有ACK需要发送 */
ICSK_ACK_TIMER = 2, /* 延迟确认定时器已经启动 */
ICSK_ACK_PUSHED = 4, /* 如果处于快速发送模式,允许立即发送ACK */
ICSK_ACK_PUSHED2 = 9 /* 无论是否处于快速发送模式,都可以立即发送ACK */
};

以下是ACK发送状态的转换图:

ACK的发送状态转换

接收到数据报后,会调用tcp_event_data_recv(),设置ICSK_ACK_SCHED标志来表明有ACK需要发送。

如果接收到了小包,说明对端很可能暂时没有数据需要发送了,此时会设置ICSK_ACK_PUSHED标志,

如果处于快速路径中,就允许马上发送ACK。如果不止一次接收到小包,就设置ICSK_ACK_PUSHED2

标志,不管是否处于快速路径中,都允许立即发送ACK,以强调发送ACK的紧急程度。

同时根据距离上次接收到数据报的时间间隔,来动态调整icsk->icsk_ack.ato:

1. delta <= TCP_ATO_MIN /2时,ato = ato / 2 + TCP_ATO_MIN / 2。

2. TCP_ATO_MIN / 2 < delta <= ato时,ato = min(ato / 2 + delta, rto)。

3. delta > ato时,ato值不变。

如果接收到的数据包的时间间隔变小,ato也会相应的变小。

如果接收到的数据包的时间间隔变大,ato也会相应的变大。

inet_csk_schedule_ack()用于设置ICSK_ACK_SCHED标志位,表示有ACK需要发送。

static inline void inet_csk_schedule_ack (struct sock *sk)
{
inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_SCHED;
}
static void tcp_event_data_recv (struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
u32 now; inet_csk_schedule_ack(sk); /* 设置有ACK需要发送的标志 */ /* 通过接收到的数据段,来估算对端的MSS。
* 如果接收到了小包,则设置ICSK_ACK_PUSHED标志。
* 如果之前接收过小包,本次又接收到了小包,则设置ICSK_ACK_PUSHED2标志。
*/
tcp_measure_rcv_mss(sk, skb); tcp_rcv_rtt_measure(tp); /* 没有使用时间戳选项时的接收端RTT计算 */ now = tcp_time_stamp; /* 如果是第一次接收到带负荷的报文 */
/* The first data packet received, initialize delayed ACK engine. */
if (! icsk->icsk_ack.ato) {
tcp_incr_quickack(sk); /* 设置在快速确认模式中可以发送的ACK数量 */
icsk->icsk_ato.ato = TCP_ATO_MIN; /* ato的初始值,为40ms */ } else {
int m = now - icsk->icsk_ack.lrcvtime; /* 距离上次收到数据报的时间间隔 */ /* The fastest case is the first. */
if (m <= TCP_ATO_MIN / 2) {
icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + TCP_ATO_MIN / 2; } else if (m < icsk->icsk_ack.ato) {
icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + m;
/* ato的值不能超过RTO */
if (icsk->icsk_ack.ato > icsk->icsk_rto)
icsk->icsk_ack.ato = icsk->icsk_rto; } else if (m > icsk->icsk_rto) {
/* Too long gap. Apparently sender failed to restart window,
* so that we send ACKs quickly.
*/
tcp_incr_quickack(sk); /* 更新在快速确认模式中可以发送的ACK数量 */
sk_mem_reclaim(sk);
}
} icsk->icsk_ack.lrcvtime = now; /* 更新最后一次接收到数据报的时间 */ TCP_ECN_check_ce(tp, skb); /* 如果发现显示拥塞了,就进入快速确认模式 */ /* 当报文段的负荷不小于128字节时,考虑增大接收窗口当前阈值 */
if (skb->len >= 128)
tcp_grow_window(sk, skb); /* 根据接收到的数据段的大小,来调整接收窗口的阈值rcv_ssthresh */
}

如果接收到路由器的显式拥塞通知,就进入快速确认模式。

static inline void TCP_ECN_check_ce (struct tcp_sock *tp, const struct sk_buff *skb)
{
/* 如果连接不支持ECN */
if (! (tp->ecn_flags & TCP_ECN_OK))
return; switch (TCP_SKB_CB(skb)->ip_dsfield & INET_ECN_MASK) {
case INET_ECN_NOT_ECT: /* IP层不支持ECN */
/* If ECT is not set on a segment, and we already seen ECT on a previous segment,
* it is probably a retransmit.
*/
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); /* 进入快速确认模式 */
tp->ecn_flags |= TCP_ECN_DEMAND_CWR; /* 用于让对端感知拥塞的标志 */
}
/* fallinto */
default:
tp->ecn_flags |= TCP_ECN_SEEN;
}
}

通过接收到的数据段长度,来估算对端的MSS。

如果接收到了小包,则设置ICSK_ACK_PUSHED标志。

如果之前接收过小包,本次又接收到了小包,则设置ICSK_ACK_PUSHED2标志。

static void tcp_measure_rcv_mss (struct sock *sk, const struct sk_buff *skb)
{
struct inet_connection_sock *icsk = inet_csk(sk);
const unsigned int lss = icsk->icsk_ack.last_seg_size; /* 上次收到的数据段大小 */
unsigned int len; icsk->icsk_ack.last_seg_size = 0; len = skb_shinfo(skb)->gso_size ?: skb->len; /* 本次接收到数据的长度 */ /* 如果本次接收到数据的长度,大于当前发送方的MSS */
if (len >= icsk->icsk_ack.rcv_mss) {
icsk->icsk_ack.rcv_mss = len; /* 更新发送方的MSS */ } else {
/* Otherwise, we make more careful check taking into account,
* that SACKs block is variable.
* "len" is invariant segment length, including TCP header.
*/
/* 之前的len表示数据的长度,现在加上TCP首部的长度,这才是总的长度 */
len += skb->data - skb_transport_header(skb); /* 满足以下条件时,说明接收到的数据段还是比较正常的,尝试更精确的计算MSS,
* 排除SACK块的影响,更新last_seg_size和rcv_mss。
*/
/* If PSH is not set, packet should be full sized, provided peer TCP is not badly broken.
* This observation (if it is correct 8)) allows to handle super-low mtu links fairly.
*/
if (len >= TCP_MSS_DEFAULT + sizeof(struct tcphdr) ||
(len >= TCP_MIN_MSS + sizeof(struct tcphdr) &&
! (tcp_flag_word(tcp_hdr(skb)) & TCP_PEMNANT))) {
/* Subtract also invariant (if peer is RFC compliant),
* tcp header plus fixed timestamp option length.
* Resulting len is MSS free of SACK jitter.
*/
/* 减去报头和时间戳选项的长度,剩下的就是数据和SACK块(如果有的话) */
len -= tcp_sk(sk)->tcp_header_len; icsk->icsk_ack.last_seg_size = len; /* 更新最近一次接收到的数据段的长度 */ /* 说明这次收到的还是full-sized,而不是小包 */
if (len == lss) {
icsk->icsk_ack.rcv_mss = len;
return;
}
} /* 如果之前已经收到了小包,则进入更紧急的ACK发送模式,接下来无论是否处于快速确认模式,
* 都可以马上发送ACK。
*/
if (icsk->icsk_ack.pending & ICSK_ACK_PUSHED)
icsk->icsk_ack.pending |= ICSK_ACK_PUSHED2; /* 如果收到小包,就允许在快速确认模式中,直接发送ACK */
icsk->icsk_ack.pending |= ICSK_ACK_PUSHED;
}
} #define TCP_MSS_DEFAULT 536U
#define TCP_MIN_MSS 88U /* Minimal accepted MSS. It is (60+60+8) - (20+20). */

ACK的发送状态清除

当成功发送ACK时,会删除延迟确认定时器,同时清零ACK的发送状态标志icsk->icsk_ack.pending。

static int tcp_transmit_skb (struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask)
{
...
if (likely(tcb->tcp_flags & TCPHDR_ACK))
tcp_event_ack_sent(sk, tcp_skb_pcount(skb)); /* ACK发送事件的处理 */
...
}

ACK发送事件主要做了:更新快速确认模式中的ACK额度,删除ACK延迟定时器,清零icsk->icsk_ack.pending。

/* Account for an ACK we sent. */
static inline void tcp_event_ack_sent (struct sock *sk, unsigned int pkts)
{
tcp_dec_quickack_mode(sk, pkts); /* 更新快速确认模式的ACK额度 */
inet_csk_clear_xmit_timer(sk, ICSK_TIME_DACK); /* 删除ACK延迟定时器 */
}

在快速确认模式中,可以发送的ACK数量是有限制的,具体额度为icsk->icsk_ack.quick。

当额度用完时,就进入延迟确认模式。

static inline void tcp_dec_quickack_mode (struct sock *sk, const unsigned int pkts)
{
struct inet_connection_sock *icsk = inet_csk(sk); if (icsk->icsk_ack.quick) { /* 如果额度不为0 */
if (pkts >= icsk->icsk_ack.quick) {
icsk->icsk_ack.quick = 0;
/* Leaving quickack mode we deflate ATO. */
icsk->icsk_ack.ato = TCP_ATO_MIN;
} else
icsk->icsk_ack.quick -= pkts;
}
}

TCP的ACK确认系列 — 发送状态转换机的更多相关文章

  1. TCP的ACK确认系列 — 延迟确认

    主要内容:TCP的延迟确认.延迟确认定时器的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 延迟确认模式 发送方在发送数据包时,如果发送的数据包有 ...

  2. TCP的ACK确认系列 — 快速确认

    主要内容:TCP的快速确认.TCP_QUICKACK选项的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 快速确认模式 (1) 进入快速确认模式 ...

  3. TCP的ACK原理和延迟确认机制

    某天晚上睡觉前突然想到 tcp的ACK确认是单独发的还是和报文一起发的,下面看一下别人的解答 一.ACK定义TCP协议中,接收方成功接收到数据后,会回复一个ACK数据包,表示已经确认接收到ACK确认号 ...

  4. TCP的发送系列 — 发送缓存的管理(二)

    主要内容:从TCP层面判断发送缓存的申请是否合法,进程因缺少发送缓存而进行睡眠等待. 因为有发送缓存可写事件而被唤醒. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zh ...

  5. TCP报文送达确认ACK

    TCP数据包中的序列号(Sequence Number)不是以报文段来进行编号的,而是将连接生存周期内传输的所有数据当作一个字节流,序列号就是整个字节流中每个字节的编号.一个TCP数据包中包含多个字节 ...

  6. TCP报文到达确认(ACK)机制

    TCP数据包中的序列号(Sequence Number)不是以报文段来进行编号的,而是将连接生存周期内传输的所有数据当作一个字节流,序列号就是整个字节流中每个字节的编号.一个TCP数据包中包含多个字节 ...

  7. TCP的发送系列 — 发送缓存的管理(一)

    主要内容:TCP发送缓存的初始化.动态调整.申请和释放. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 数据结构 TCP对发送缓存的管理是在两个层面上进 ...

  8. 【PI系列】SAP IDOC发送状态03,PI没有收到消息的解决办法

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[PI系列]SAP IDOC发送状态03,PI没 ...

  9. TCP/IP网络编程系列之四(初级)

    TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的 ...

随机推荐

  1. 地址四级联动的vue组件

    一.效果图如下: 二.思路 主要在vue中结合 mint-ui组件的Picker和Popup方法,负责对json地址进行展示: 三.代码地址 四.说明 address4.json最好是在点击父组件的地 ...

  2. swiper实现臭美app滑动效果

    一.臭美app效果: 我的需求是这样,上面正常滑动,点击下面的小卡牌,上面的滑动区也随之切换到当前的点击态. 二.实现: css: 主要设置可见区域的几张卡牌的位置,注意的几个位置是,中间的激活态和左 ...

  3. OpenCV3.1.0中调用MHI(Motion History Images, 运动历史图像)

    写在前边: OpenCV3.0+要想使用MHI,就要现安装扩展模块opencv_contrib.安装方法见:ubuntu 14.04 64位 安装Opencv3.1.0 (包含opencv_contr ...

  4. 有趣的冷知识:编程中Foo, Bar 到底什么意思?

    转自:编程中Foo, Bar 到底什么意思? 1 前言 在很多国外计算机书本和一些第三份开源软件的Demo中经常用到两个英文单词Foo,Bar.这到底是什么意思呢?从步入屌丝界的IT生活见到这两个单词 ...

  5. Node.js TLS/SSL

    Stability: 3 - Stable 可以使用 require('tls') 来访问这个模块. tls 模块 使用 OpenSSL 来提供传输层(Transport Layer)安全性和(或)安 ...

  6. 毕业论文内容框架指导-适用于MIS系统

    摘要: 背景.要做什么.选用什么技术.按照什么过程.原理.或者步骤去做.最后做出了什么东西.做出来的东西有什么用. 1. 前言 系统的背景与意义:为什么要做这个系统 ? 现状调查:别人做的怎么样? 系 ...

  7. Jeff Atwood倾情推荐——程序员必读之书

    英文版:<Code Complete 2>中文版:<代码大全(第二版)>作者:Steve McConnell译者:金戈  汤凌  陈硕  张菲出版社:电子工业出版社出版日期:2 ...

  8. ANTLR和StringTemplate实例:自动生成单元测试类

    ANTLR和StringTemplate实例:自动生成单元测试类 1. ANTLR语法 要想自动生成单元测试,首先第一步就是分析被测试类.这里以Java代码为例,用ANTLR对Java代码进行分析.要 ...

  9. Java web文件上传下载

    [版权申明:本文系作者原创,转载请注明出处] 文章出处:http://blog.csdn.net/sdksdk0/article/details/52048666 作者:朱培 ID:sdksdk0 邮 ...

  10. iOS日历中给一个事件添加多个提醒

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) iOS自带的日历应用中,我们最多只能给一个事件设置2个提醒,但 ...