探讨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):
OK,现在我们开始来看看内核中是如何处理这样的数据包,为什么会接受这样的ACK包。
TCP协议的接收函数为tcp_v4_rcv(),该函数SKB进行必要的检查,如数据的长度、校验和初始化等,初始化TCP控制块中的值;接着会调用__inet_lookup_skb()函数在tcp_hashinfo散列表中来查找是否存在对应的传输控制块。如果找到,则调用tcp_v4_do_rcv()来处理(这里我们忽略了Netfilter、IPSec、DMA等细节,这些跟我们探讨的内容无关),基本的代码流程如下图所示:
我们的ACK包的校验和、首部长度是没有问题,所以它可以轻松的通过tcp_v4_rcv()的检查,接下来看tcp_v4_do_rcv()中处理。tcp_v4_do_rcv()中会首先检查SKB包对应的sock结构的状态,如果是ESTABLISHED状态,则会调用tcp_rcv_established()来处理,代码片段如下:
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
......
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
TCP_CHECK_TIMER(sk);
if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
TCP_CHECK_TIMER(sk);
return 0;
}
......
}