TCP ACK 方式

TCP ACK机制详解

TCP 有两种确认方式,Delay ACK和quick ACK

Quick ACK,本端接收到数据包后,会立即发送ACK给对端。

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

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

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

所以连续收到两个数据包时,就会发送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中的变量,用于控制快速确认和延迟确认。

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

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

 

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

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

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

 

以下是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需要发送。

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

  1. static inline void inet_csk_schedule_ack (struct sock *sk)  
  2. {  
  3.     inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_SCHED;  
  4. }  

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

  1. static void tcp_event_data_recv (struct sock *sk, struct sk_buff *skb)  
  2. {  
  3.     struct tcp_sock *tp = tcp_sk(sk);  
  4.     struct inet_connection_sock *icsk = inet_csk(sk);  
  5.     u32 now;  
  6.   
  7.     inet_csk_schedule_ack(sk); /* 设置有ACK需要发送的标志 */  
  8.   
  9.     /* 通过接收到的数据段,来估算对端的MSS。 
  10.      * 如果接收到了小包,则设置ICSK_ACK_PUSHED标志。 
  11.      * 如果之前接收过小包,本次又接收到了小包,则设置ICSK_ACK_PUSHED2标志。 
  12.      */  
  13.     tcp_measure_rcv_mss(sk, skb);   
  14.   
  15.     tcp_rcv_rtt_measure(tp); /* 没有使用时间戳选项时的接收端RTT计算 */  
  16.   
  17.     now = tcp_time_stamp;  
  18.   
  19.     /* 如果是第一次接收到带负荷的报文 */  
  20.     /* The first data packet received, initialize delayed ACK engine. */  
  21.     if (! icsk->icsk_ack.ato) {  
  22.         tcp_incr_quickack(sk); /* 设置在快速确认模式中可以发送的ACK数量 */  
  23.         icsk->icsk_ato.ato = TCP_ATO_MIN; /* ato的初始值,为40ms */  
  24.   
  25.     } else {  
  26.         int m = now - icsk->icsk_ack.lrcvtime; /* 距离上次收到数据报的时间间隔 */  
  27.   
  28.         /* The fastest case is the first. */  
  29.         if (m <= TCP_ATO_MIN / 2) {  
  30.             icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + TCP_ATO_MIN / 2;  
  31.   
  32.         } else if (m < icsk->icsk_ack.ato) {  
  33.             icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + m;  
  34.             /* ato的值不能超过RTO */  
  35.             if (icsk->icsk_ack.ato > icsk->icsk_rto)  
  36.                 icsk->icsk_ack.ato = icsk->icsk_rto;  
  37.   
  38.         } else if (m > icsk->icsk_rto) {  
  39.             /* Too long gap. Apparently sender failed to restart window, 
  40.              * so that we send ACKs quickly. 
  41.              */  
  42.              tcp_incr_quickack(sk); /* 更新在快速确认模式中可以发送的ACK数量 */  
  43.              sk_mem_reclaim(sk);  
  44.         }  
  45.     }  
  46.    
  47.   
  48.     icsk->icsk_ack.lrcvtime = now; /* 更新最后一次接收到数据报的时间 */  
  49.   
  50.     TCP_ECN_check_ce(tp, skb); /* 如果发现显示拥塞了,就进入快速确认模式 */  
  51.   
  52.     /* 当报文段的负荷不小于128字节时,考虑增大接收窗口当前阈值 */  
  53.     if (skb->len >= 128)  
  54.         tcp_grow_window(sk, skb); /* 根据接收到的数据段的大小,来调整接收窗口的阈值rcv_ssthresh */  
  55. }  

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

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

  1. static inline void TCP_ECN_check_ce (struct tcp_sock *tp, const struct sk_buff *skb)  
  2. {  
  3.     /* 如果连接不支持ECN */  
  4.     if (! (tp->ecn_flags & TCP_ECN_OK))  
  5.         return;  
  6.   
  7.     switch (TCP_SKB_CB(skb)->ip_dsfield & INET_ECN_MASK) {  
  8.     case INET_ECN_NOT_ECT: /* IP层不支持ECN */  
  9.         /* If ECT is not set on a segment, and we already seen ECT on a previous segment, 
  10.          * it is probably a retransmit. 
  11.         */  
  12.         if (tp->ecn_flags & TCP_ECN_SEEN)  
  13.             tcp_enter_quickack_mode((struct sock *tp); / 进入快速确认模式 */  
  14.         break;  
  15.   
  16.     case INET_ECN_CE: /* 数据包携带拥塞标志 */  
  17.         if (! (tp->ecn_flags & TCP_ECN_DEMAND_CWR)) {  
  18.             /* Better not delay acks, sender can have a very low cwnd */  
  19.             tcp_enter_quickack_mode((struct sock *) tp); /* 进入快速确认模式 */  
  20.             tp->ecn_flags |= TCP_ECN_DEMAND_CWR; /* 用于让对端感知拥塞的标志 */  
  21.         }  
  22.         /* fallinto */  
  23.     default:  
  24.         tp->ecn_flags |= TCP_ECN_SEEN;  
  25.     }  
  26. }   

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

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

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

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

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

 

ACK的发送状态清除

 

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

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

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

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

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

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

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

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

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

  1. static inline void tcp_dec_quickack_mode (struct sock *sk, const unsigned int pkts)  
  2. {  
  3.     struct inet_connection_sock *icsk = inet_csk(sk);  
  4.   
  5.     if (icsk->icsk_ack.quick) { /* 如果额度不为0 */  
  6.         if (pkts >= icsk->icsk_ack.quick) {  
  7.             icsk->icsk_ack.quick = 0;  
  8.             /* Leaving quickack mode we deflate ATO. */  
  9.             icsk->icsk_ack.ato = TCP_ATO_MIN;  
  10.         } else  
  11.             icsk->icsk_ack.quick -= pkts;  
  12.     }  
  13. }  

 

 

03-26
### TCP Acknowledgment Mechanism Overview The Transmission Control Protocol (TCP) acknowledgment mechanism plays a critical role in ensuring reliable data transmission between two endpoints. When one endpoint sends packets to another, it expects acknowledgments from the receiver indicating that the packet has been successfully received[^1]. This process ensures reliability through retransmission mechanisms when necessary. In scenarios involving slow-draining connections—commonly seen as congestion within lossless networks—the built-in end-to-end flow control feature of TCP manages these situations effectively by adjusting its sending rate dynamically based on feedback provided via ACKs sent back by receivers. Additionally, there exists what's known as 'TCP Slow Start', where initially small amounts are transmitted until more bandwidth availability becomes apparent; fine-tuning parameters like `initcwnd` can significantly enhance performance under certain conditions explained elsewhere thoroughly.[^2] #### Code Example Demonstrating Basic Functionality Of A Simplified TCP Stack Handling ACKS In Python: Below is an illustrative example showing how simple client-server communication might handle basic aspects including acknowledging receipt using pseudo-TCP logic implemented purely for educational purposes only without actual socket operations being performed here: ```python class TCPSession: def __init__(self): self.unacknowledged_packets = {} def send_packet(self, seq_num, payload): # Simulate sending a packet with sequence number and content. print(f"Sending Packet {seq_num}: {payload}") self.unacknowledged_packets[seq_num] = payload def receive_ack(self, ack_seq_num): if ack_seq_num in self.unacknowledged_packets: del self.unacknowledged_packets[ack_seq_num] print(f"Acknowledgement Received For Sequence Number: {ack_seq_num}") # Usage Example session = TCPSession() for i in range(5): session.send_packet(i,"Data Block "+str(i)) acks_received=[0,2,4] for j in acks_received: session.receive_ack(j) if not bool(session.unacknowledged_packets): print("All Packets Have Been Successfully Acknowledged.") else: print("Some Packets Are Still Unacknowledged:",list(session.unacknowledged_packets.keys())) ``` §§Related Questions§§ 1. How does TCP manage lost segments during high network latency? 2. What impact do varying initial congestion window sizes (`initcwnd`) have on different types of applications utilizing TCP? 3. Can you explain the differences between fast recovery and exponential backoff strategies used in conjunction with TCP acknowledgments? 4. Why would tuning TCP settings such as `rto_min`, `tcp_mem`, etc., be important considerations depending upon specific use cases? 5. Describe potential pitfalls associated with improper handling of duplicate ACKs in relation to triggering early detection phases of congestion avoidance algorithms.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值