TCP在收到数据后必须发送ACK给对端,但如果每收到一个包就给一个ACK的话会使得网络中被注入过多报文。TCP的做法是在收到数据时不立即发送ACK,而是设置一个定时器,如果在定时器超时之前有数据发送给对端,则ACK会被携带在数据中捎带过去;超时则由定时器发送ACK。这样就减少了报文的发送,提高了协议的效率。

延迟确认

在tcp_transmit_skb 中如果skb->len != tcp_header_size 也就是有载荷时,就会调用下述函数处理延迟确认事宜

/* Congestion state accounting after a packet has been sent.
如果此时距离最近接收到数据包的时间间隔足够短,
说明双方处于你来我往的双向数据传输中,就进入延迟确认模式
icsk->icsk_ack.ato在ACK的发送过程中扮演了重要角色,作用是??
A:ato为ACK Timeout,指ACK的超时时间。但延迟确认定时器的超时时间为icsk->icsk_ack.timeout,
ato只是计算timeout的一个中间变量,会根接收到的数据包的时间间隔来做动态调整。
一般如果接收到的数据包的时间间隔变小,ato也会相应的变小。
如果接收到的数据包的时间间隔变大,ato也会相应的变大。
ato的最小值为40ms,ato的最大值一般为200ms或一个RTT。
所以在实际传输过程中,我们看到的ACK的超时时间,是处于40ms ~ min(200ms, RTT)之间的
在tcp_send_delayed_ack()中会把ato赋值给icsk->icsk_ack.timeout,用作延迟确认定时器的超时时间。
*/
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 (tcp_packets_in_flight(tp) == 0)
tcp_ca_event(sk, CA_EVENT_TX_START);/* first transmit when no packets in flight */ tp->lsndtime = now;/* 更新最近发送数据包的时间*/ /* If it is a reply for ato after last received
* packet, enter pingpong mode.
*///如果距离上次接收到数据包的时间在ato内,则进入延迟确认模式
if ((u32)(now - icsk->icsk_ack.lrcvtime) < icsk->icsk_ack.ato)
icsk->icsk_ack.pingpong = 1;
}

延时确认定时器

icsk->icsk_delack_timer的激活函数为inet_csk_reset_xmit_timer(),此函数共负责了5个定时器的激活工作。延迟确认定时器的另一个激活函数为tcp_send_delayed_ack(),用于判断发送快速确认还是延迟确认。

else if (what == ICSK_TIME_DACK) {
icsk->icsk_ack.pending |= ICSK_ACK_TIMER;/* 延迟确认定时器启动标志 */
icsk->icsk_ack.timeout = jiffies + when;//Delay ACK定时器超时时刻
sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);
}
/**
* tcp_delack_timer() - The TCP delayed ACK timeout handler
* @data: Pointer to the current socket. (gets casted to struct sock *)
*
* This function gets (indirectly) called when the kernel timer for a TCP packet
* of this socket expires. Calls tcp_delack_timer_handler() to do the actual work.
*
* Returns: Nothing (void)
*/
static void tcp_delack_timer(unsigned long data)
{
struct sock *sk = (struct sock *)data; bh_lock_sock(sk); /* 传输控制块未被锁定 */
// 处于下半部
if (!sock_owned_by_user(sk)) {
tcp_delack_timer_handler(sk);/* 调用超时处理函数 */
} else {
/*
如果延迟确认定时器触发时,发现用户进程正在使用此socket,就把blocked置为1。
* 之后在接收到新数据、或者将数据复制到用户空间之后,会马上发送ACK。
*/
inet_csk(sk)->icsk_ack.blocked = 1;
__NET_INC_STATS(sock_net(sk), LINUX_MIB_DELAYEDACKLOCKED);
/* deleguate our work to tcp_release_cb()
if (flags & TCPF_DELACK_TIMER_DEFERRED) {
        tcp_delack_timer_handler(sk);
        __sock_put(sk);
    }
*///设置标记,等应用程序release_sock的时候调用tcp_delack_timer_handler
if (!test_and_set_bit(TCP_DELACK_TIMER_DEFERRED, &sk->sk_tsq_flags))//如果delack推迟执行,也会处理prequeue队列
sock_hold(sk);
}
bh_unlock_sock(sk);
sock_put(sk);
}
/* Called with BH disabled */
void tcp_delack_timer_handler(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk); sk_mem_reclaim_partial(sk);
/* 关闭或者监听状态 || 未启动延迟ack定时器*/
if (((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN)) ||
!(icsk->icsk_ack.pending & ICSK_ACK_TIMER))
goto out;
/* 尚未达到超时时间,重新设定定时器 */
if (time_after(icsk->icsk_ack.timeout, jiffies)) {
sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);
goto out;
}
icsk->icsk_ack.pending &= ~ICSK_ACK_TIMER; /* 清除延迟ack标记 */ if (!skb_queue_empty(&tp->ucopy.prequeue)) { /* prequeue队列不为空 */
struct sk_buff *skb; __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPSCHEDULERFAILED);
/* 数据包出队,调用接收函数tcp_v4_do_rcv处理包 */
while ((skb = __skb_dequeue(&tp->ucopy.prequeue)) != NULL)
sk_backlog_rcv(sk, skb);
/* 消耗的内存清0 */
tp->ucopy.memory = 0;
}
/* 有ack需要发送 */
if (inet_csk_ack_scheduled(sk)) {
if (!icsk->icsk_ack.pingpong) {//不能延时,但却有ack被推迟了
/* Delayed ACK missed: inflate ATO. 即时ack模式 计算超时时间*/
icsk->icsk_ack.ato = min(icsk->icsk_ack.ato << 1, icsk->icsk_rto);
} else { /* 延迟ack模式 */
/* Delayed ACK missed: leave pingpong mode and
* deflate ATO.
*/ /* 重置超时时间以及设置为快速ack模式 */
icsk->icsk_ack.pingpong = 0;
icsk->icsk_ack.ato = TCP_ATO_MIN;
}
tcp_send_ack(sk); /* 发送ack */
__NET_INC_STATS(sock_net(sk), LINUX_MIB_DELAYEDACKS);
} out:
if (tcp_under_memory_pressure(sk))
sk_mem_reclaim(sk);
}

注意 tcp_recvmsg读完sk_receive_queue之后,如果没有读满应用程序缓存,则会处理prequeue和backlog队列,并从中读取。
另外,及时读满了缓存,也会在函数退出前,处理prequeue和backlog队列。

  • 为什么不在协议栈中直接处理?
    1. cache的角度
      prequeue和backlog都是为了在应用程序上下文去处理数据包。
      因为softirq上下文和应用程序上下文之间切换,会造成cache刷新。
      比如ksoftirqd和应用程序不在一个cpu上; 或是协议栈处理完后通知应用程序,应用程序被唤醒,同样会造成造成cache刷新

    2. ack的角度
      如果直接在协议栈快速处理包,则很可能导致快速ack,使对方快速达到很大的发送速率,但是本地用户进程可能并不活跃,就会导致接收缓存满了,对方瞬间停止发送。 造成很明显的抖动。
      因此在应用程序处理数据包和ack的发送,可以反映用户进程的实时情况。
      但同时对突发的小包和需要低延迟的消息,放入prequeue中,也会造成非常不好的影响

    3. 锁的角度
      softirq中不能睡眠,也不应该跟应用程序抢锁, 如果tcp_recvmsg读取一大块内存,tcp_v4_rcv就需要很久才能抢到锁。
      因此使用prequeue就可以避免这样的情况。

http://www.cnhalo.net/2016/07/13/linux-tcp-prequeue-backlog/

综上所述:应用层也会支持处理数据包,处理数据包后reales_sock时 也会调用 延时确认定时器

if (flags & TCPF_DELACK_TIMER_DEFERRED) {
tcp_delack_timer_handler(sk);
__sock_put(sk);
}

设置延迟ACK的时机主要有以下几个:

(1)发送SYN后收到SYN|ACK时:tcp_rcv_synsent_state_process==》inet_csk_reset_xmit_timer 设置延迟ACK定时器,超时时间200ms

(2)发送ACK时无法申请skb:tcp_send_ack---》

(3)有数据放入prequeue队列中时:

(4)调用__tcp_ack_snd_check函数发送ACK时满足一下条件

(1)收到少于一个MSS的数据或通告窗口缩小

(2)没有处于快速ACK模式

(3)无乱序数据

tcpack--4延时ack的更多相关文章

  1. tcp syn-synack-ack 服务端接收ack

    TCP 服务端 接收到ack tcp_v4_rcv() -> tcp_v4_do_rcv() -> tcp_v4_hnd_req() + tcp_child_process()tcp_v4 ...

  2. tcp 输入 prequeue以及backlog队列

    /*ipv4_specific是TCP传输层到网络层数据发送以及TCP建立过程的真正OPS, 在tcp_prot->init中被赋值给inet_connection_sock->icsk_ ...

  3. Nginx 教程(2):性能

    tcp_nodelay, tcp_nopush 和 sendfile tcp_nodelay 在 TCP 发展早期,工程师需要面对流量冲突和堵塞的问题,其中涌现了大批的解决方案,其中之一是由 John ...

  4. TCP/IP 笔记 - TCP数据流和窗口管理

    TCP流量控制机制通过动态调整窗口大小来控制发送端的操作,确保路由器/接收端消息不会溢出. 交互式TCP连接 交互式TCP连接指该连接需要在客户端和服务器之间传输用户输入信息,如按键操作.短消息.操作 ...

  5. Nginx三部曲(2)性能

    我们会告诉你 Nginx 如何工作及其背后的理念,还有如何优化以加快应用的性能,如何安装启动和保持运行. 这个教程有三个部分: 基本概念 —— 这部分需要去了解 Nginx 的一些指令和使用场景,继承 ...

  6. tcp中delay_ack的理解

    内核版本,3.10. 首先,我们需要知道,在一个sock中,维护ack的就有很多变量,多种状态: struct inet_connection_sock { .... __u8 icsk_ca_sta ...

  7. Prometheus Node_exporter 之 Network Netstat TCP Linux MIPs

    Network Netstat TCP Linux MIPs1. TCP Aborts / Tiemouts type: GraphUnit: shortLabel: ConnectionsTCPAb ...

  8. nginx参数优化

    大家好,分享即关爱,我们很乐意和你分享一些新的知识,我们准备了一个 Nginx 的教程,分为三个系列,如果你对 Nginx 有所耳闻,或者想增进 Nginx 方面的经验和理解,那么恭喜你来对地方了. ...

  9. 可靠UDP设计

    最近加入了一个用帧同步的项目,帧同步方案对网络有着极大的影响,于是采用了RUDP(可靠UDP),那么为什么要摒弃TCP,而费尽心思去采用UDP呢?要搞明白这个问题,首先要了解TCP和UDP的区别 , ...

随机推荐

  1. ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)解决方案

    在Win7下使用MySQL5.6.35创建用户时,提示权限不足,具体解决方案如下: 1 停止mysql服务 net stop mysql 2 打开新的cmd窗口,切换到bin目录,运行如下命令,cmd ...

  2. nginx的脚本引擎(二)rewrite

    其实rewrite指令和上一篇说的if/set/return/break之类的没多大差别,但是rewrite用起来相对复杂,我就把他单独放到了这里.想要弄懂nginx的脚本引擎需要先明白处理reque ...

  3. EDI模拟实验

    EDI模拟实验 [实验目的] ⑴.了解EDI报文的格式和特点. ⑵.掌握EDI报文生成和发送流程. [实验条件] ⑴.个人计算机一台,预装Windows XP操作系统和浏览器 ⑵.计算机通过局域网形式 ...

  4. c# 常用帮助类

    C#常用帮助类 因为小土现在还是处于小白阶段,所以自己的知识技术还达不到要求,但是小土在网上找到一个大神的,等以后小土技术有了一定提升以后,在走自己的路,啥也不说了上货. 地址 :https://gi ...

  5. kali linux 换国内源

    输入命令 vim /etc/apt/sources.list 添加国内源 #中科大deb http://mirrors.ustc.edu.cn/kali kali-rolling main non-f ...

  6. lumen路由

    $router->get('/', function () use ($router) { return config('options.author'); }); $router->ge ...

  7. 通过jQuery来获取DropDownList的Text/Value属性值

    脚本代码: <script src="Scripts/jquery-1.4.1-vsdoc.js" type="text/javascript">& ...

  8. OpenCV计算机视觉学习(7)——图像金字塔(高斯金字塔,拉普拉斯金字塔)

    如果需要处理的原图及代码,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice 本节 ...

  9. 函数-深入JS笔记

    代码特点:高内聚,低耦合 耦合 存在执行多个相同作用代码时,这就叫耦合 if (1 > 0) { console.log('a'); } if (2 > 0) { console.log( ...

  10. vue学习第一部

    目录 基础操作 vue基础使用 步骤 vue的框架思想(mvvm) 显示数据 vue 常用指令 属性操作 事件绑定 操作样式 条件渲染指令 列表渲染指令 vue对象提供的属性功能 过滤器 计算和侦听属 ...