赞
踩
在TCP输入数据段的处理过程中,如果发现输入段携带了ACK信息,则会调用tcp_ack()进行ACK相关的处理。实际中,ACK信息总是会携带的,因为携带ACK不需要任何的附加开销,所以对于输入的每一个段(输了RST等特殊段),这个过程总是要执行的,这篇笔记就来看看TCP对ACK确认的处理过程。
收到确认后,一项重要的工作就是更新发送窗口,所以这里再来认识一下发送窗口相关的几个关键字段,如下图所示:
注意,图中的发送窗口,指的是接收方通知给发送方的接收窗口大小,即流量控制窗口,并非拥塞窗口。
tcp_ack()有个非常重要的参数flag,其贯穿整个ACK的处理过程,它记录了从输入段中能够获取到的任何信息(比如是否携带了数据、是否重复ACK、是否是SACK等),供后面的拥塞控制、RTT采样等操作参考。flag可能是如下值的组合。
flag | 值 | 描述 |
---|---|---|
FLAG_DATA | 0x01 | ACK段中携带了数据 |
FLAG_WIN_UPDATE | 0x02 | 收到ACK段后更新了发送窗口,可能更新了左边界,也有可能更新了右边界(通告窗口变大) |
FLAG_DATA_ACKED | 0x04 | ACK段确认了新数据 |
FLAG_RETRANS_DATA_ACKED | 0x08 | ACK段携带的数据已经收到过了 |
FLAG_SYN_ACKED | 0x10 | ACK段确认了SYN段 |
FLAG_DATA_SACKED | 0x20 | ACK段确认了新的数据 |
FLAG_ECE | 0x40 | 该ACK段携带了ECE标志 |
FLAG_DATA_LOST | 0x80 | SACK检测到了数据丢失 |
FLAG_SLOWPATH | 0x100 | 该ACK段是由慢速路径处理的 |
FLAG_ONLY_ORIG_SACKED | 0x200 | |
FLAG_SND_UNA_ADVANCED | 0x400 | ACK段更新了snd_una,即收到ACK后,发送窗口左边界可以右移 |
FLAG_DSACKING_ACK | 0x800 | ACK段中包含有DSACK信息 |
FLAG_NONHEAD_RETRANS_ACKED | 0x1000 | |
FLAG_SACK_RENEGING | 0x2000 | 检测到之前SACK确认过的数据段被对端丢弃了(这是协议允许的) |
此外,还定义了一些基本flag的组合:
#define FLAG_ACKED (FLAG_DATA_ACKED|FLAG_SYN_ACKED)
//用于判断输入的数据段是否为重复段
#define FLAG_NOT_DUP (FLAG_DATA|FLAG_WIN_UPDATE|FLAG_ACKED)
#define FLAG_CA_ALERT (FLAG_DATA_SACKED|FLAG_ECE)
#define FLAG_FORWARD_PROGRESS (FLAG_ACKED|FLAG_DATA_SACKED)
#define FLAG_ANY_PROGRESS (FLAG_FORWARD_PROGRESS|FLAG_SND_UNA_ADVANCED)
/* This routine deals with incoming acks, but not outgoing ones. */ static int tcp_ack(struct sock *sk, struct sk_buff *skb, int flag) { struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); //TCB中尚未被确认的最小序号 u32 prior_snd_una = tp->snd_una; //ACK段中的序号 u32 ack_seq = TCP_SKB_CB(skb)->seq; //ACK段中的确认号 u32 ack = TCP_SKB_CB(skb)->ack_seq; u32 prior_in_flight; u32 prior_fackets; int prior_packets; int frto_cwnd = 0; /* If the ack is newer than sent or older than previous acks * then we can probably ignore it. */ //确认的是还没有发送的数据,这是无意义的确认,直接返回 if (after(ack, tp->snd_nxt)) goto uninteresting_ack; //该确认号已经收到过了。这种可能是重复ACK,也有可能是正常的,比如该AC段有延时。 //这种ACK有可能还携带了有效的SACK信息 if (before(ack, prior_snd_una)) goto old_ack; //到这里,说明确认号在期望的范围内[snd_una, snd_nxt], //确认号确认了新数据,设置FLAG_SND_UNA_ADVANCED标记。 //if判断是为了排除ack==prior_snd_una的情况 if (after(ack, prior_snd_una)) flag |= FLAG_SND_UNA_ADVANCED; //tcp_abc特性相关 if (sysctl_tcp_abc) { if (icsk->icsk_ca_state < TCP_CA_CWR) tp->bytes_acked += ack - prior_snd_una; else if (icsk->icsk_ca_state == TCP_CA_Loss) /* we assume just one segment left network */ tp->bytes_acked += min(ack - prior_snd_una, tp->mss_cache); } prior_fackets = tp->fackets_out; prior_in_flight = tcp_packets_in_flight(tp); //下面是更新发送窗口,按照快速路径和慢速路径分别处理 if (!(flag & FLAG_SLOWPATH) && after(ack, prior_snd_una)) { /* Window is constant, pure forward advance. * No more checks are required. * Note, we use the fact that SND.UNA>=SND.WL2. */ //记录最近一次导致发送窗口更新的ACK段的序号,即tp->snd_wl1=ack_seq tcp_update_wl(tp, ack, ack_seq); //更新发送窗口左边界 tp->snd_una = ack; //设置发送窗口更新标记 flag |= FLAG_WIN_UPDATE; //通知拥塞控制算法,发生了CA_EVENT_FAST_ACK事件 tcp_ca_event(sk, CA_EVENT_FAST_ACK); NET_INC_STATS_BH(LINUX_MIB_TCPHPACKS); } else { //慢速路径处理 //ACK段还携带了数据,设置FLAG_DATA标记 if (ack_seq != TCP_SKB_CB(skb)->end_seq) flag |= FLAG_DATA; else NET_INC_STATS_BH(LINUX_MIB_TCPPUREACKS); //更新发送窗口 flag |= tcp_ack_update_window(sk, skb, ack, ack_seq); //SACK相关处理 if (TCP_SKB_CB(skb)->sacked) flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una); //ECN相关处理 if (TCP_ECN_rcv_ecn_echo(tp, tcp_hdr(skb))) flag |= FLAG_ECE; //通知拥塞控制算法,发生了CA_EVENT_SLOW_ACK事件 tcp_ca_event(sk, CA_EVENT_SLOW_ACK); } //清除软件错误 sk->sk_err_soft = 0; //更新最近一次接收到ACK段的时间戳 tp->rcv_tstamp = tcp_time_stamp; //如果之前根本就没有待确认的段,那么无需后续的重传队列以及拥塞控制处理; //这种情况下需要做和持续定时器相关的操作,因为可能之前传送过探测报文 prior_packets = tp->packets_out; if (!prior_packets) goto no_queue; //删除重传队列中已经确认的数据段,并进行时延采样 flag |= tcp_clean_rtx_queue(sk, prior_fackets); //F-RTO算法相关内容 if (tp->frto_counter) frto_cwnd = tcp_process_frto(sk, flag); /* Guarantee sacktag reordering detection against wrap-arounds */ if (before(tp->frto_highmark, tp->snd_una)) tp->frto_highmark = 0; //拥塞控制相关 if (tcp_ack_is_dubious(sk, flag)) { /* Advance CWND, if state allows this. */ if ((flag & FLAG_DATA_ACKED) && !frto_cwnd && tcp_may_raise_cwnd(sk, flag)) tcp_cong_avoid(sk, ack, prior_in_flight); tcp_fastretrans_alert(sk, prior_packets - tp->packets_out, flag); } else { if ((flag & FLAG_DATA_ACKED) && !frto_cwnd) tcp_cong_avoid(sk, ack, prior_in_flight); } if ((flag & FLAG_FORWARD_PROGRESS) || !(flag & FLAG_NOT_DUP)) dst_confirm(sk->sk_dst_cache); return 1; no_queue: //之前没有未被确认的段,收到了ACK,进行持续定时器相关处理 icsk->icsk_probes_out = 0; /* If this ack opens up a zero window, clear backoff. It was * being used to time the probes, and is probably far higher than * it needs to be for normal retransmission. */ if (tcp_send_head(sk)) tcp_ack_probe(sk); return 1; old_ack: //虽然该ACK已经收到过了,但是如果其携带了SACK信息,需要更新确认内容 if (TCP_SKB_CB(skb)->sacked) tcp_sacktag_write_queue(sk, skb, prior_snd_una); uninteresting_ack: SOCK_DEBUG(sk, "Ack %u out of %u:%u\n", ack, tp->snd_una, tp->snd_nxt); return 0; }
可见,tcp_ack()的核心操作干了三件事:
这篇笔记只详细介绍第一件事。
快速路径处理情况,因为此时在接收数据,所以输入段的windows字段一定是没有发生变化的,所以无需更新snd_wnd的值,直接更新snd_una即可。
慢速路径处理情况,情况复杂,需要做更多的判断,调用tcp_ack_update_window()完成发送窗口的更新。
/* Update our send window. * * Window update algorithm, described in RFC793/RFC1122 (used in linux-2.2 * and in FreeBSD. NetBSD's one is even worse.) is wrong. */ static int tcp_ack_update_window(struct sock *sk, struct sk_buff *skb, u32 ack, u32 ack_seq) { struct tcp_sock *tp = tcp_sk(sk); int flag = 0; //ACK段中携带的通告窗口 u32 nwin = ntohs(tcp_hdr(skb)->window); //协议规定,SYN和SYN+ACK段中是不可以携带窗口扩大因子的,所以这里 //判断不带SYN标记位时是否需要根据窗口扩大因子调整通告的新窗口大小 if (likely(!tcp_hdr(skb)->syn)) nwin <<= tp->rx_opt.snd_wscale; if (tcp_may_update_window(tp, ack, ack_seq, nwin)) { //需要更新窗口 flag |= FLAG_WIN_UPDATE; //更新snd_wl tcp_update_wl(tp, ack, ack_seq); if (tp->snd_wnd != nwin) { //更新发送窗口 tp->snd_wnd = nwin; /* Note, it is the only place, where * fast path is recovered for sending TCP. */ //更新了发送窗口大小,需要重新判断是否设置首部预测标记 tp->pred_flags = 0; tcp_fast_path_check(sk); //更新已知最大通告窗口 if (nwin > tp->max_window) { tp->max_window = nwin; //因为MSS和max_window相关,所以max_window发生了变化,需要重新计算MSS tcp_sync_mss(sk, inet_csk(sk)->icsk_pmtu_cookie); } } } //更新发送窗口左边界 tp->snd_una = ack; return flag; }
慢速路径下的核心是判断什么时候应该更新发送窗口,这是由tcp_may_update_window()实现的。
/* Check that window update is acceptable.
* The function assumes that snd_una<=ack<=snd_next.
*/
static inline int tcp_may_update_window(const struct tcp_sock *tp,
const u32 ack, const u32 ack_seq,
const u32 nwin)
{
//cond1: 确认号大于snd_una,说明确认了新数据,可以更新发送窗口左边界;
//cond2: ACK段的序号大于snd_wl1,说明对方有发送新数据,所以需要更新snd_wl1;
//cond3: 通告的接收窗口有变化.
//上面只有有一个条件成立,那么就可以更新发送窗口了(条件2着实没理解...)。
return (after(ack, tp->snd_una) ||
after(ack_seq, tp->snd_wl1) ||
(ack_seq == tp->snd_wl1 && nwin > tp->snd_wnd));
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。