首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 操作系统 > UNIXLINUX >

研讨Linux kernel中对序列号超前的ACK包的处理

2013-03-22 
探讨Linux kernel中对序列号超前的ACK包的处理在开发的内核模块中遇到这样一个问题:一个数据包有多个请求,

探讨Linux kernel中对序列号超前的ACK包的处理

  在开发的内核模块中遇到这样一个问题:一个数据包有多个请求,每次只让服务器处理一个请求,所以在将请求交到上层的时候需要拆包,只将部分数据交到上层。为了防止客户端重传数据包,要预先给客户端发送一个对完整数据包的确认。这样就会造成一个问题,客户端发送的ACK包的序列号,会比协议栈中期望的序列号大。

  假设完整数据包的起始序列号分别为1883458390、1883458821,上层协议栈拿到的数据包的起始序列号为1883458390、1883458476,这时服务器端sock结构的rcv_nxt应该为1883458476,但是由于我们事先多发送了一个ACK,所以这时客户端的ACK包的序列号为1883458821,而不是1883458476。下面是抓包的部分截图,根据抓包情况来看,这样的ACK包,内核接受了这样的确认包,如图所示(客户端:192.168.9.188;服务器端:192.168.9.191):

研讨Linux kernel中对序列号超前的ACK包的处理

OK,现在我们开始来看看内核中是如何处理这样的数据包,为什么会接受这样的ACK包。

  TCP协议的接收函数为tcp_v4_rcv(),该函数SKB进行必要的检查,如数据的长度、校验和初始化等,初始化TCP控制块中的值;接着会调用__inet_lookup_skb()函数在tcp_hashinfo散列表中来查找是否存在对应的传输控制块。如果找到,则调用tcp_v4_do_rcv()来处理(这里我们忽略了Netfilter、IPSec、DMA等细节,这些跟我们探讨的内容无关),基本的代码流程如下图所示:

研讨Linux kernel中对序列号超前的ACK包的处理

  我们的ACK包的校验和、首部长度是没有问题,所以它可以轻松的通过tcp_v4_rcv()的检查,接下来看tcp_v4_do_rcv()中处理。tcp_v4_do_rcv()中会首先检查SKB包对应的sock结构的状态,如果是ESTABLISHED状态,则会调用tcp_rcv_established()来处理,代码片段如下:

/* This routine deals with incoming acks, but not outgoing ones. *//* * tcp_ack()用于处理接收到有ACK标志的段,当接到有效的ACK后会更新 * 发送窗口。 * @skb: 接收到的ACK段 * @flag: 标志,取值为FLAG_DATA等 */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);u32 prior_snd_una = tp->snd_una;u32 ack_seq = TCP_SKB_CB(skb)->seq;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 older than previous acks * then we can probably ignore it. *//* * 检验确认的序号是否落在SND.UNA和SND.NXT之间,否则 * 是不合法的序号。 * 如果确认的序号在SND.NXT的右边,则说明该序号的数据 * 发送方还没有发送,直接返回。 * 如果确认的序号在SND.UNA的左边,则说明已经接受过 * 该序号的ACK了。因为每个有负载的TCP段都会顺便 * 携带一个ACK序号,即使这个序号已经确认过。因此 * 如果是一个重复的ACK就无需作处理直接返回即可。但 * 如果段中带有SACK选项,则需对此进行处理 */if (before(ack, prior_snd_una))goto old_ack;/* If the ack includes data we haven't sent yet, discard * this segment (RFC793 Section 3.9). */if (after(ack, tp->snd_nxt))goto invalid_ack;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);/* * 进行更新发送窗口等操作,并根据各种信息获取ACK的 * 各种标志. */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执行的是快速路径,则更新发送窗口的左边界, * 添加FLAG_WIN_UPDATE标记,同时通知拥塞控制算法模块 * 本次ACK是快速路径,如有必要,就作相应的处理 */tcp_update_wl(tp, ack_seq);tp->snd_una = ack;flag |= FLAG_WIN_UPDATE;tcp_ca_event(sk, CA_EVENT_FAST_ACK);NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPACKS);/* * 如果接收ACK执行的是慢速路径,首先判断ACK段中是否有 * 数据负载,如果有,则添加FLAG_DATA标记. */} else {if (ack_seq != TCP_SKB_CB(skb)->end_seq)flag |= FLAG_DATA;elseNET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPPUREACKS);/* * 更新发送窗口,同时添加更新发送窗口后获取的标记. */flag |= tcp_ack_update_window(sk, skb, ack, ack_seq);/* * 如果接收的段中存在SACK选项,则调用tcp_sacktag_write_queue() * 标记重传队列. */if (TCP_SKB_CB(skb)->sacked)flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una);/* * 检测ACK端中是否存在ECE标志,如果有,则添加FLAG_ECE标志. */if (TCP_ECN_rcv_ecn_echo(tp, tcp_hdr(skb)))flag |= FLAG_ECE;/* * 最后通知拥塞控制算法模块本次ACK是慢速路径,如有必要, * 则做相应的处理. */tcp_ca_event(sk, CA_EVENT_SLOW_ACK);}/* We passed data and got it acked, remove any soft error * log. Something worked... */sk->sk_err_soft = 0;icsk->icsk_probes_out = 0;/* * 设置最近一次收到ACK段的时间 */tp->rcv_tstamp = tcp_time_stamp;/* * 检测是否有已发送但未确认的段,如果没有则跳转到 * no_queue处理 */prior_packets = tp->packets_out;if (!prior_packets)goto no_queue;/* See if we can take anything off of the retransmit queue. *//* * 在重传队列中删除删除已确认的段。 */flag |= tcp_clean_rtx_queue(sk, prior_fackets, prior_snd_una);/* * 如果在重传超时后使用FRTO算法,则调用tcp_process_frto() * 进行处理。 */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;/* * 根据ACK的明确与否,更新拥塞窗口,进行 * 拥塞控制。 * 对于判断ACK的明确与否,只要满足如下条件中的任何一项便 * 视为ACK为不明确 * 1)接收到的ACK是重复的 * 2)接收到SACK块或显式拥塞通知 * 3)当前拥塞状态不为Open。 *  * tcp_ack()处理接收到的段时,会检测ACK。如果接收的ACK是不明确 * 的或拥塞状态为Open状态,则进行拥塞状态机状态的迁移。如果 * ACK确认了新的段同时拥塞窗口可以更新,则进行拥塞避免, * 更新拥塞窗口。 */if (tcp_ack_is_dubious(sk, flag)) {/* Advance CWND, if state allows this. *//* * ACK是不明确的,说明接收的ACK不明确或在Open拥塞 * 状态。如果ACK确认了新的段且拥塞窗口可以更新, * 则更新拥塞窗口,迁移拥塞状态。 */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 {/* * ACK是明确的,说明拥塞状态至少在Open状态,如果 * ACK确认了新的段,则更新拥塞窗口 */if ((flag & FLAG_DATA_ACKED) && !frto_cwnd)tcp_cong_avoid(sk, ack, prior_in_flight);}/* * 如果ACK确认新的段(包括新的数据、SYN段,以及接收到新的SACK选项), * 或者接收到的ACK是重复的,则确认该传输控制块的输出路由缓存项 * 是有效的。 */if ((flag & FLAG_FORWARD_PROGRESS) || !(flag & FLAG_NOT_DUP))dst_confirm(sk->sk_dst_cache);return 1;no_queue:/* 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. *//* * 如果还有待发送的数据,则需根据情况确定是否进行零窗口探测。 * 接收到ACK,如果对方的接收窗口没有关闭,需清除持续定时器中 * 的指数退避算法指数,停止持续定时器,否则开启持续定时器。 * tcp_ack_probe()用来确定师傅需要进行零窗口探测。 * 如果接收方的接收窗口已经打开,且足够接收发送方的一个完整的 * 段,则暂时不需要零窗口探测,并停止零窗口探测定时器。否则 * 需进行零窗口的探测,并重新复位零窗口探测定时器。 */if (tcp_send_head(sk))tcp_ack_probe(sk);return 1;invalid_ack:SOCK_DEBUG(sk, "Ack %u after %u:%u\n", ack, tp->snd_una, tp->snd_nxt);return -1;old_ack:/* * 如果是已确认的ACK,且其中带有SACK选项信息,则需标记重传队列中 * 各个段的记分牌。 */if (TCP_SKB_CB(skb)->sacked) {tcp_sacktag_write_queue(sk, skb, prior_snd_una);if (icsk->icsk_ca_state == TCP_CA_Open)tcp_try_keep_open(sk);}SOCK_DEBUG(sk, "Ack %u before %u:%u\n", ack, tp->snd_una, tp->snd_nxt);return 0;}

热点排行