TCP套接口的快速路径开启的先决条件可由函数tcp_fast_path_check一窥究竟,分别如下:乱序out_of_order_queue队列为空、通告的接收窗口非空(rcv_wnd)、接收缓存sk_rmem_alloc小于套接口的限定值、没有urgent紧急数据在传输。
static inline void tcp_fast_path_check(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
if (RB_EMPTY_ROOT(&tp->out_of_order_queue) &&
tp->rcv_wnd &&
atomic_read(&sk->sk_rmem_alloc) < sk->sk_rcvbuf &&
!tp->urg_data)
tcp_fast_path_on(tp);
}
快速路径使能的结果是对套接口结构成员pred_flags的配置,套接口连接的数据报文TCP头部长度保存在pred_flags的第31位到28位,共4个位数中;TCP_FLAG_ACK标志保存在第20比特位(宏TCP_FLAG_ACK定义为网络字节序);发送窗口大小保存在低16个比特位中。
static inline void __tcp_fast_path_on(struct tcp_sock *tp, u32 snd_wnd)
{
tp->pred_flags = htonl((tp->tcp_header_len << 26) | ntohl(TCP_FLAG_ACK) | snd_wnd);
}
static inline void tcp_fast_path_on(struct tcp_sock *tp)
{
__tcp_fast_path_on(tp, tp->snd_wnd >> tp->rx_opt.snd_wscale);
}
最终的pred_flags变量相当于TCP报文头部的第三个32bit字段,只是将Reserved字段和除去ACK位的flags标志字段设置为了零。
一、快速路径的开启时机
在接收到保序的数据包后,函数tcp_data_queue将其加入到接收队列中,并且检查乱序队列中的数据包是否可合并到接收队列中,以上完成后,进行TCP接收的快速路径检查,如条件符合,为后续报文打开快速路径接收。
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
if (tcp_receive_window(tp) == 0)
goto out_of_window;
eaten = tcp_queue_rcv(sk, skb, 0, &fragstolen);
if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
tcp_ofo_queue(sk);
if (RB_EMPTY_ROOT(&tp->out_of_order_queue))
inet_csk(sk)->icsk_ack.pingpong = 0;
}
tcp_fast_path_check(sk);
return;
}
}
在TCP连接三次握手完成之后,内核默认为服务端(被动打开端)开启快速路径接收功能,此时连接刚刚建立,快速路径的开启条件都成立,没有必要使用tcp_fast_path_check函数。
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
switch (sk->sk_state) {
case TCP_SYN_RECV:
tcp_set_state(sk, TCP_ESTABLISHED);
sk->sk_state_change(sk);
tcp_fast_path_on(tp);
break;
}
}
但是对应主动开启连接的TCP客户端,在其接收到服务端的SYN+ACK报文后,在函数tcp_ack处理ACK过程中调用tcp_ack_update_window更新窗口时,根据条件判断是否需要开启快速接收路径。另外,随后的函数tcp_finish_connect函数除了将套接口状态设置为已建立TCP_ESTABLISHED外,还会根据接收到的服务端TCP窗口系数选项是否为0设置快速路径。如果服务端窗口系数选项有值禁用快速路径,否则开启之。
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
{
if (th->ack) {
tcp_ack(sk, skb, FLAG_SLOWPATH);
tcp_finish_connect(sk, skb);
}
}
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
tcp_set_state(sk, TCP_ESTABLISHED);
if (!tp->rx_opt.snd_wscale)
__tcp_fast_path_on(tp, tp->snd_wnd);
else
tp->pred_flags = 0;
}
如果当前TCP套接口处在慢速路径接收状态,内核更新套接口窗口值时,会检查是否可开启快速路径。TCP的对端通过新的窗口通告表明此刻本端为发送端,如果能够开启快速路径,将为之后可能要接收的数据进行快速处理。
static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack, u32 ack_seq)
{
if (tcp_may_update_window(tp, ack, ack_seq, nwin)) {
flag |= FLAG_WIN_UPDATE;
tcp_update_wl(tp, 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);
}
}
}
在应用层接收函数的处理中,以下函数tcp_recvmsg,如果判断套接口中的紧急数据urg_data已经被拷贝给上层应用,消除了一个阻碍快速路径开启的条件,调用tcp_fast_path_check进行开启检查。
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len)
{
do {
last = skb_peek_tail(&sk->sk_receive_queue);
skb_queue_walk(&sk->sk_receive_queue, skb) {
}
skip_copy:
if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) {
tp->urg_data = 0;
tcp_fast_path_check(sk);
}
} while (len > 0);
}
二、快速路径接收
除了以上介绍的快速路径开启条件,在接收函数tcp_rcv_established中内核还要进行一些其它的条件检查。
/*
* It is split into a fast path and a slow path. The fast path is
* disabled when:
* - A zero window was announced from us - zero window probing is only handled properly in the slow path.
* - Out of order segments arrived.
* - Urgent data is expected.
* - There is no buffer space left
* - Unexpected TCP flags/window values/header lengths are received
* (detected by checking the TCP header against pred_flags)
* - Data is sent in both directions. Fast path only supports pure senders or pure receivers (this means either the sequence number or the ack
* value must stay constant)
* - Unexpected TCP option.
*/
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
{
unsigned int len = skb->len;
首先判断TCP报文头部的第三个32bit字段是否与开启快速路径时保存的pred_flags值相同,在比较之前忽略掉TCP头部的TCP_HP_BITS定义的位,即Reserved和PSH位,二者相同意味着TCP的其它位段没有发生改变。TCP数据包的开始序号seq要等于套接口正在等待的序号数据;并且数据包的确认序号ack_seq不能大于(小于或者等于)套接口下一个要发送的序号报文snd_nxt,表明对端没有在接收数据,快速路径不允许两端同时发送接收数据。
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags && TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
int tcp_header_len = tp->tcp_header_len;
其次,尝试解析TCP的timestamp选项,如果TCP头的长度不等于标准长度与timestamp选项长度之和,略去选项解析。否则,判读是否为timestamp选项,如果不是转到慢速路径处理,或者timestamp选项中携带的时间戳小于最近一次接收到的时间戳,PAWS检查失败,同样跳转到慢速路径。注意在此处内核不会更新最近一次接收的时间戳ts_recent的值,因为还没有对数据包进行checksum检验,假如更新成一个混乱的时间戳值,比如非常大的值,将导致后续的报文不能够通过PAWS检测,而全部被丢弃。
if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
if (!tcp_parse_aligned_timestamp(tp, th))
goto slow_path;
if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
goto slow_path;
}
如果数据包的长度与TCP报文头部长度相等,没有数据部分,仅为ACK确认报文。此类型报文已在TCP接收入口处进行了checksum校验,可进行时间戳ts_recent的更新。随后处理此ACK报文,检查是否还有数据包可进行发送。对于长度小于TCP头部长度的数据包,直接丢弃。在快速路径中,如果本端为数据发送端,将接收到对端的大量ACK报文,在此处进行处理。
if (len <= tcp_header_len) {
if (len == tcp_header_len) {
/* Predicted packet is in window by definition. seq == rcv_nxt and rcv_wup <= rcv_nxt. Hence, check seq<=rcv_wup reduces to: */
if (tcp_header_len == (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) && tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);
/* We know that such packets are checksummed on entry. */
tcp_ack(sk, skb, 0);
__kfree_skb(skb);
tcp_data_snd_check(sk);
return;
} else { /* Header too small */
goto discard;
}
对于长度大于TCP报文头部长度的数据包,表明存在数据部分。首先完成checksum校验,丢弃校验失败的报文。如果此时skb的占用空间大于套接口的预分配空间额度值,跳转到慢速路径执行。随后更新时间戳ts_recent,调用tcp_queue_rcv处理接收到的数据报文。此段处理意味着本端在快速路径中为数据接收端。最后,如果发送端数据包的ACK确认序号不等于本端套接口的待确认序号,由于快速路径的单向特性,本端并不发送数据,一旦两者不相等的,表明本端发送了数据,需要处理ACK并且检查是否还有后续数据发送。否则,内核直接进行ACK发送策略检查。
} else {
if (tcp_checksum_complete(skb))
goto csum_error;
if ((int)skb->truesize > sk->sk_forward_alloc)
goto step5;
/* Predicted packet is in window by definition. seq == rcv_nxt and rcv_wup <= rcv_nxt. Hence, check seq<=rcv_wup reduces to: */
if (tcp_header_len == (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) && tp->rcv_nxt == tp->rcv_wup)
tcp_store_ts_recent(tp);
eaten = tcp_queue_rcv(sk, skb, tcp_header_len, &fragstolen);
tcp_event_data_recv(sk, skb);
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
/* Well, only one small jumplet in fast path... */
tcp_ack(sk, skb, FLAG_DATA);
tcp_data_snd_check(sk);
if (!inet_csk_ack_scheduled(sk))
goto no_ack;
}
__tcp_ack_snd_check(sk, 0);
no_ack:
if (eaten)
kfree_skb_partial(skb, fragstolen);
sk->sk_data_ready(sk);
return;
}
}
}
三、快速路径接收的关闭
与以上介绍的快速路径开启的条件判断类型,一旦这些条件不满足,就需要关闭快速路径。例如以下,接收到乱序报文tcp_data_queue_ofo函数,套接口空间不足即使缩减队列空间也没有足够空间tcp_prune_queue函数,以及接收到TCP紧急数据tcp_check_urg时,都要关闭快速路径。
static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb)
{
/* Disable header prediction. */
tp->pred_flags = 0;
}
static int tcp_prune_queue(struct sock *sk)
{
/* Massive buffer overcommit. */
tp->pred_flags = 0;
return -1;
}
static void tcp_check_urg(struct sock *sk, const struct tcphdr *th)
{
tp->urg_data = TCP_URG_NOTYET;
tp->urg_seq = ptr;
/* Disable header prediction. */
tp->pred_flags = 0;
}
最后,在TCP报文发送函数tcp_transmit_skb中,调用函数tcp_select_window选择窗口大小时,如果将要通过的窗口为0,关闭快速路径接收功能。TCP的窗口探测probe只能在慢速路径中处理。
static u16 tcp_select_window(struct sock *sk)
{
/* If we advertise zero window, disable fast path. */
if (new_win == 0) {
tp->pred_flags = 0;
} else if (old_win == 0) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFROMZEROWINDOWADV);
}
return new_win;
}