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

主要内容:TCP的快速确认、TCP_QUICKACK选项的实现。

内核版本:3.15.2

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

快速确认模式

(1) 进入快速确认模式

设置快速确认模式标志,设置在快速确认模式中可以发送的ACK数量。

static void tcp_enter_quickack_mode (struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);

    tcp_incr_quickack(sk); /* 设置在快速确认模式中可以发送的ACK数量 */
    icsk->icsk_ack.pingpong = 0; /* 快速确认模式的标志 */
    icsk->icsk_ack.ato = TCP_ATO_MIN; /* ACK超时时间 */
}

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

所以进入快速确认模式时,需要设置可以快速发送的ACK数量,一般允许快速确认半个接收窗口的数据量,

但最多不能超过16个,最少为2个。

static void tcp_incr_quickack (struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);

    /* 可以快速确认半个接收窗口的数据量 */
    unsigned int quickacks = tcp_sk(sk)->rcv_wnd / (2 * icsk->icsk_ack.rcv_mss);

    if (quickacks == 0)
        quckacks = 2; /* 最少为2个 */

    if (quickacks > icsk->icsk_ack.quick)
        icsk->icsk_ack.quick = min(quickacks, TCP_MAX_QUICKACKS); /* 最多不能超过16个 */
}

/* Maximal number of ACKs sent quickly to accelerate slow-start. */
#define TCP_MAX_QUICKACKS 16U

(2) 检查是否处于快速确认模式。

如果设置了快速确认标志,且快速确认模式中可以发送的ACK数量不为0,就判断连接处于快速确认模式中,

允许立即发送ACK。

/* Send ACKs quickly, if "quick" count is not exhausted and the session is not interactive. */
static inline bool tcp_in_quickack_mode (const struct sock *sk)
{
    const struct inet_connectionsock *icsk = inet_csk(sk);

    /* 如果快速确认模式中可以发送的ACK数量不为0,且设置了快速确认标志 */
    return icsk->icsk_ack.quick && ! icsk->icsk_ack.pingpong;
}

快速ACK的发送

在tcp_rcv_established()中,如果没有通过TCP的首部预测,就会执行慢速路径来处理接收到的报文。

处理完接收到的报文之后,会调用tcp_data_snd_check()来检查是否需要发送数据,以及是否需要扩大发送缓存。

然后调用tcp_ack_snd_check()来检查是否需要发送ACK,以及是使用快速确认还是延迟确认。

同样的在通过TCP首部预测的快速路径中,也会调用__tcp_ack_snd_check()来发送快速确认或延迟确认。

static inline void tcp_ack_snd_check(struct sock *sk)
{
    /* 如果没有ACK需要发送 */
    if (! inet_csk_ack_scheduled(sk)) {
        /* We sent a data segment already. */
        return;
    }

    __tcp_ack_snd_check(sk, 1); /* 决定要发送快速确认还是延迟确认 */
}

如果此时符合以下任一条件,可以立即发送ACK,即进行快速确认:

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

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

2.  此时处于快速确认模式中。

3. 乱序队列不为空。

/* Check if sending an ack is needed. */

static void __tcp_ack_snd_check (struct sock *sk, int ofo_possible)
{
    struct tcp_sock *tp = tcp_sk(sk);

    /* 符合以下任一条件,可以立即发送ACK:
     * 1. 接收缓冲区中有一个以上的全尺寸数据段仍然是NOT ACKed,并且接收窗口变大了。
     * 2.  此时处于快速确认模式中。
     * 3. 乱序队列不为空。
     */
    /* More than one full frame received... */
    if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss &&
        /* ... and right edge of window advances far enough.
         * (tcp_recvmsg() will send ACK otherwise). Or ...
         */
         __tcp_select_window(sk) >= tp->rcv_wnd) || 

         /* We ACK each frame or ... */
         tcp_in_quickack_mode(sk) ||

         /* We have out of order data. */
         (ofo_possible && skb_peek(&tp->out_of_order_queue))) {

        /* Then ack it now. */
        tcp_send_ack(sk); /* 立即发送ACK */

    } else {
        /* Else, send delayed ack. */
        tcp_send_delayed_ack(sk); /* 延迟ACK的发送,见下一篇blog:) */
    }
}

ACK的发送函数为tcp_send_ack(),如果发送失败会启动ACK延迟定时器。

/* This routine sends an ack and also updates the window. */

void tcp_send_ack (struct sock *sk)
{
    struct sk_buff *buff;

    /* If we have been reset, we may not send again. */
    if (sk->sk_state == TCP_CLOSE)
        return;

    /* We are not putting this on the write queue, so tcp_transmit_skb()
     * will set the ownership to this sock.
     */
    buff = alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));

    if (buff == NULL) { /* 分配skb失败 */
        inet_csk_schedule_ack(sk); /* 设置标志位,表示有ACK需要发送 */
        inet_csk(sk)->icsk_ack.ato = TCP_ATO_MIN; /* 重置ATO */

        /* 设置延迟确认定时器,超时时间为200ms */
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK, TCP_DELACK_MAX, TCP_RTO_MAX);
        return;
    }

    /* Reserve space for headers and prepare control bits. */
    skb_reserve(buff, MAX_TCP_HEADER); /* 设置报文头部的空间 */

    /* 初始化不携带数据的skb的一些控制字段 */
    tcp_init_nondata_skb(buff, tcp_acceptable_seq(sk), TCPHDR_ACK);

    /* Send it off, this clears delayed acks for us. */
    TCP_SKB_CB(buff)->when = tcp_time_stamp;

    tcp_transmit_skb(sk, buff, 0, sk_gfp_atomic(sk, GFP_ATOMIC));
}

/* 设置标志位,表示有ACK需要发送。*/
static inline void inet_csk_schedule_ack (struct sock *sk)
{
    inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_SCHED;
}

/* Maximal time to delay before sending an ACK.
 * Delayed ACK的最大延迟时间,一般为200ms
 */
#define TCP_DELACK_MAX ((unsigned) (HZ/5))

/* Delayed ACK的最小延迟时间,一般为40ms */
#define TCP_DELACK_MIN ((unsigned) (HZ/25))

TCP_QUICKACK选项

TCP_QUICKACK用于让本端立即发送ACK,而不进行延迟确认。

需要注意的是,这个选项并不是持久的,之后还是有可能进入延迟确认模式的。

所以如果需要一直进行快速确认,要在每次调用接收函数后都进行选项设置。

int quickack = 1; /* 启用快速确认,如果赋值为0表示使用延迟确认 */

setsockopt(fd, SOL_TCP, TCP_QUICKACK, &quickack, sizeof(quickack));

#define TCP_QUICKACK 12 /* Block / reenable quick acks */

static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval,
    unsigned int optlen)
{
    ...
    case TCP_QUICKACK:
        if (! val) {
            icsk->icsk_ack.pingpong = 1; /* 禁用快速确认模式 */

        } else {
            icsk->icsk_ack.pingpong = 0; /* 启用快速确认模式 */

            /* 如果当前有ACK需要发送,就立即发送 */
            if (1 << sk->sk_state) & (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT) &&
                inet_csk_ack_scheduled(sk)) {

                icsk->icsk_ack.pending |= ICSK_ACK_PUSHED; /* 允许在快速模式中立即发送 */

                /* 通常当接收队列中有数据复制到用户空间时,会调用此函数来判断是否需要立即发送ACK。
                 * 这里的用法比较特殊,由于设置了ICSK_ACK_PUSHED标志,且处于快速确认模式中,
                 * 必然会立即发送ACK。
                 */
                tcp_cleanup_rbuf(sk, 1);             

                /* 如果选项的值为偶数,那么立即退出快速确认模式。
                 * 原来选项值不限于0和1,还分奇偶的:)
                 */
                if (! (val & 1))
                    icsk->icsk_ack.pingpong = 1;
            }
        }
        break;
        ...
}

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

(1) 如果现在有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标志,且处于快速确认模式中

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

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

/* Clean up the receive buffer for full frames taken by the user,
 * then send an ACK if necessary. COPIED is the number of bytes
 * tcp_recvmsg has given to the user so far, it speeds up the calculation
 * of whether or not we must ACK for the sake of a window update.
 */

void tcp_cleanup_rbuf (struct sock *sk, int copied)
{
    struct tcp_sock *tp = tcp_sk(sk);
    bool time_to_ack = false;

    /* 获取接收队列的头一个数据段 */
    struct sk_buff *skb = skb_peek(&sk->sk_receive_queue);

    /* copied_seq: Head of yet unread data,应用程序下次从这里开始复制数据。
     * 这里检查在发送队列中,已复制到用户空间的数据段是否被清理了。
     */
    WARN(skb && !before(tp->copied_seq, TCP_SKB_CB(skb)->end_seq),
        "cleanup rbuf bug: copied %X seq %X rcvnxt %X\n", tp->copied_seq,
        TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt);

    /* 如果现在有ACK需要发送,满足以下条件之一则可以立即发送 */
    if (inet_csk_ack_scheduled(sk)) {
        const struct inet_connection_sock *icsk = inet_csk(sk);

        /* 1. Delayed ACKs frequently hit locked sockets during bulk receive.
         * 2. Once-per-two-segments ACK was not sent by tcp_input.c.
         * 3. copied >0 and ICSK_ACK_PUSHED2 set.
         * 4. copied > 0 and ICSK_ACK_PUSHED and in quickack mode.
         */
        if (icsk->icsk_ack.blocked || tp->rcv_nxt - tp->rcv_wup > icsk->icsk_ack.rcv_mss ||
            (copied > 0 && ((icsk->icsk_ack.pending & ICSK_ACK_PUSHED2) ||
             ((icsk->icsk_ack.pending & ICSK_ACK_PUSHED) && ! icsk->icsk_ack.pingpong)) &&
               ! atomic_read(&sk->sk_rmem_alloc)))
            time_to_ack = true;
    } 

    /* We send an ACK if we can now advertise a non-zero window which has
     * been raised "significantly" - at least twice bigger.
     * Even if window raised up to infinity, do not send window open ACK in states,
     * where we will not receive more. It is useless.
     */
    if (copied > 0 && ! time_to_ack && ! (sk->sk_shutdown & RCV_SHUTDOWN)) {
        __u32 rcv_window_now = tcp_receive_window(tp); /* 当前接收窗口的剩余量 */

        /* 如果当前接收窗口的剩余量小于最大值的一半 */
        if (2 * rcv_window_now <= tp->window_clamp) {

           /* 根据剩余的接收缓存,计算新的接收窗口的大小。
            * 因为这次复制,很可能空出不少接收缓存,所以新的接收窗口也会相应增大。
            */
            __u32 new_window = __tcp_select_window(sk);

           /* Send ACK now, if this read freed lots of space in our buffer.
            * Certainly, new_window is new window.
            * We can advertise it now, if it is not less than current one.
            * "Lots" means "at least twice" here.
            */

            /* 如果新的接收窗口不为0,且比原来接收窗口的剩余量大了一倍以上,就说接收窗口显著增大了。
             * 而当接收窗口显著增大时,也需要立即发送ACK告知对端。
             */
            if (new_window && new_window >= 2 * rcv_window_now)
                time_to_ack = true;
        }
    }

    if (time_to_ack)
        tcp_send_ack(sk); /* 发送ACK给对端 */
}
/* 计算当前接收窗口的剩余量 */
/* Compute the actual receive window we are currently advertising.
 * Rcv_nxt can be after the window if our peer push more data than
 * the offered window.
 */
static inline u32 tcp_receive_window (const struct tcp_sock *tp)
{
    s32 win = tp->rcv_wup + tp->rcv_wnd - tp->rcv_nxt;

    if (win < 0)
        win = 0;

    return (u32) win;
}
时间: 2024-10-10 10:23:17

TCP的ACK确认系列 — 快速确认的相关文章

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

主要内容:TCP的延迟确认.延迟确认定时器的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 延迟确认模式 发送方在发送数据包时,如果发送的数据包有负载,则会检测拥塞窗口是否超时. 如果超时,则会使拥塞窗口失效并重新计算拥塞窗口. 如果此时距离最近接收到数据包的时间间隔足够短,说明双方处于你来我往的双向数据传输中, 就进入延迟确认模式. /* Congestion state accounting after a packet has been

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

TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的套接字.在了解TCP之前,先了解一下TCP所属的TCP/IP协议栈. 如图所示,TCP/IP协议栈共分为4层,可以理解成数据收发分成了4个层次化过程. 链路层 它是物理链接领域标准化结果,也是最基本的领域,专门定义LAN.WAN.MAN等网络标准.若两台计算机通过网络进行数据交换,链路层就负责整个物

TCP/IP网络编程系列之三

TCP/IP网络编程系列之三-地址族与数据序列 分配给套接字的IP地址和端口 IP是Internet Protocol (网络协议)的简写,是为首发网络数据而分配给计算机的值.端口号并非赋予计算机值,而是为了区分程序中创建的套接字而分配给套接字的序号. 网络地址 网络地址分为IPV4和IPV6,分别你别为4个字节地址簇和6个字节地址簇.IPV4标准的4个字节的地址分为网络地址和主机地址,且分为A.B.C.D.E 等类型.一般很少用到E类型.如下图所示:net-id指网络ID,host-id指主机

TCP/IP网络编程系列之二

套接字类型与协议设置 我们先了解一下创建套接字的那个函数 int socket(int domain,int type,int protocol);成功时返回文件描述符,失败时返回-1.其中,domain是套接字使用中的协议族(Protocol Family)信息.type套接字类型里面的数据传输类型信息.protocol计算机通信中使用的协议信息. 协议族(Protocol Family) 协议族类型有: PE_INET IPV4 PE_INET6 IPV6 PF_LOCAL 本地通信的UNI

TCP/IP网络编程系列之二(初级)

套接字类型与协议设置 我们先了解一下创建套接字的那个函数 int socket(int domain,int type,int protocol);成功时返回文件描述符,失败时返回-1.其中,domain是套接字使用中的协议族(Protocol Family)信息.type套接字类型里面的数据传输类型信息.protocol计算机通信中使用的协议信息. 协议族(Protocol Family) 协议族类型有: PE_INET IPV4 PE_INET6 IPV6 PF_LOCAL 本地通信的UNI

TCP/IP网络编程系列之三(初级)

TCP/IP网络编程系列之三-地址族与数据序列 分配给套接字的IP地址和端口 IP是Internet Protocol (网络协议)的简写,是为首发网络数据而分配给计算机的值.端口号并非赋予计算机值,而是为了区分程序中创建的套接字而分配给套接字的序号. 网络地址 网络地址分为IPV4和IPV6,分别你别为4个字节地址簇和6个字节地址簇.IPV4标准的4个字节的地址分为网络地址和主机地址,且分为A.B.C.D.E 等类型.一般很少用到E类型.如下图所示:net-id指网络ID,host-id指主机

HTTP知识普及系列:确认访问用户身份的认证

认证:密码.动态令牌.数字证书.生物认证.IC卡. HTTP所使用的认证方式 BASIC认证(基本认证),BASIC认证是从HTTP/1.0就定义的认证方式.Base64编码方式. DIGEST认证(摘要认证),DIGEST认证同样适用质询/响应的方式,但不会想BASIC认证那样直接发送明文密码. SSL客户端认证,SSL客户端认证是借由HTTPS的客户端证书完成认证的方式.凭借客户端证书认证,服务器可确认访问是否来自己登陆的客户端.SSL客户端认证不会仅依靠证书完成认证,一般会和基于表单认证组

TCP/IP详解系列 --- 概念总结01

UDP协议  .vs.  TCP协议:  原理上:(TCP报文段. vs . UDP用户数据报) TCP协议的特性: TCP是面向连接的运输层协议,应用程序在使用TCP协议之前,必须先建立TCP连接.在传送数据完毕之后,必须释放已建立的TCP连接. 每一条TCP连接只能有两个端点,每一条TCP协议只能是点对点的. TCP提供可靠交付的服务.通过TCP连接传送的数据,无差错.不丢失.不重复并且按序到达. TCP提供全双工通信.TCP允许通信双方的应用进程在任何时候都能发送数据.TCP连接的两端都设

Wireshark抓包实例分析TCP重复ACK与乱序

转载请在文首保留原文出处: EMC 中文支持论坛https://community.emc.com/go/chinese 介绍 TCP 的一大常见问题在于重复 ACK 与快速重传.这一现象的发生也是由于性能问题,本章讨论如何发现这一问题以及他们意味着什么. 另一个常见问题是前一片段丢失以及乱序片段.某些情况下,这一现象喻示着故障发生,可能是由于网络问题或是抓包中断. 更多信息 重复 ACK 与快速重传 : 当网速变慢时,重复 ACK 是可能的原因之一.大多数情况下,重复 ACK 的发生是由于高延