TCP协议是可靠的、快速传递数据的协议,当套接字状态是ESTABLISHED状态表明两端已经建立连接,可以互相传送数据了,tcp_v4_do_rcv接受到数据后首先检查套接字状态,如果是ESTABLISHED就交给tcp_rcv_established函数处理具体数据接受过程。如果是LISTEN就由tcp_v4_hnd_req处理,如果是其他状态就由tcp_rcv_state+process处理,关系图如下。
1、数据走Fast Path路径的条件
linux提供了Fast Path处理路径来加速TCP数据传送,linux使用协议头预定向来选择那些数据包应用放在Fast Path路径处理,条件:
a、数据包接受顺序正确
b、数据包不需要进一步分析,可以直接复制到应用程序的接受缓冲区中。
Fast Path处理过程是在应用程序进程的现场执行,在应用进程现场执行一个套接字的读操作函数,通过Fast Path处理速度是最快的。正确进入Fast Path路径的关键是选择最有可能包含常规数据的数据包,这样的数据包不需要额外的处理,为了完成这个选择,linux内核使用协议头预定向处理,在收到数据包之前跨苏标记这些候选的数据包,协议头预定向处理是计算协议头中预定向标志,该标志决定数据包是走Fast Path处理还是Slow Path处理,协议头预定向值由以下值计算:prediction flags = hlen << 26^ackw^SND.WND,hlen是协议头的长度,ackw是ACK<<<20位后的布尔变量。预定向值保存在struct tcp_sock数据结构的tp->pred_flags数据域中,预定向标志计算函数是__tcp_fast_path_on。虽然是在ESTABLISHED状态中处理但还是要做以下检查
a、TCP连接的接受段接受窗口大小是0,对于窗口0的探测处理走Slow Path处理。
b、数据包的顺序不正确,也会走Slow Path处理。
c、如果用户地址接受缓冲区没有空间也会走Slow Path处理。
d、协议头预定向过程失败也会走Slow Path处理。
e、Fast Path只支持不会在改向的数据传输,如果要把数据包传送到别的路径,就会走Slow Path处理。
f、数据包除了时间戳选项还有其他选项,也会走Slow Path处理。
2、tcp_rcv_established函数
(1)、检查数据包是否满足Fast Path处理条件
主要检查以上Fast Path处理的条件,只要其中一项不满足就走Slow Path处理。
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
struct tcp_sock *tp = tcp_sk(sk);
int res;
tp->rx_opt.saw_tstamp = 0;
//预定向标志和输入数据段的标志比较
//数据段序列号是否正确
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;
/* Check timestamp */
//时间戳选项之外如果还有别的选项就送给Slow Path处理
if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
/* No? Slow path! */
if (!tcp_parse_aligned_timestamp(tp, th))
goto slow_path;
//对数据包做PAWS快速检查,如果检查走Slow Path处理
/* If PAWS failed, check it more carefully in slow path */
if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
goto slow_path;
}
//数据包长度太小
if (len <= tcp_header_len) {
/* Bulk data transfer: sender */
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 0;
} else { /* Header too small */
TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);
goto discard;
}
} else {
int eaten = 0;
int copied_early = 0;
...
}
(2)确定运行现场
Fast Path路径运行在应用程序进程现场,查看当前应用进程是否是等待数据的进程,linux内核为当前进程的全局指针current总是指向当前正在运行的进程,当前运行的进程是:进程状态是TASK_RUNNING,放在运行进程链表中的第一个进程。查看当前套接字是否运行在当前用户进程现场,通过比较ucopy数据结构中的进程指针是否与当前运行进程相同。存放在struct ucopy数据结构中的当前进程是tcp_recvmsg,tcp_recvmsg是套接字层的接受函数,由应用层调用,最后调用tcp_copy_to_iovec将数据包直接复制到应用层,如果复制成功要清除prequeue队列中已经复制的数据,腾出空间。
...
//当前进程是否有锁定
//当前进程的全局指针current
//tp->ucopy.task指针是否等于当前进程
if (tp->ucopy.task == current &&
sock_owned_by_user(sk) && !copied_early) {
__set_current_state(TASK_RUNNING);
//将数据包复制到应用层空间
if (!tcp_copy_to_iovec(sk, skb, tcp_header_len))
eaten = 1;
}
//复制成功
if (eaten) {
/* 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);
tcp_rcv_rtt_measure_ts(sk, skb);
__skb_pull(skb, tcp_header_len);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITSTOUSER);
}
//清除prequeue队列中已经复制的数据包,并回复ack
if (copied_early)
tcp_cleanup_rbuf(sk, skb->len);
...
(3)、复制不成功
如果复制不能可能是应用层没有进程运行、向用户层复制数据失败,如是向用户层复制数据失败可能是校验和不正确,就需要重新计算校验和,如果套接字没有多余空间就走Slow Path路劲处理。
...
//复制不成功
if (!eaten) {
//从新计算校验和
if (tcp_checksum_complete_user(sk, skb))
goto csum_error;
/* 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);
tcp_rcv_rtt_measure_ts(sk, skb);
//套接字空间不足
if ((int)skb->truesize > sk->sk_forward_alloc)
goto step5;
...
(4)连续大量复制数据
LINUX_MIB_TCPHPHITS是大块数据传送,应将协议头从skb中移走,把数据段中的数据部分放入到sk_receive_queue常规接受缓冲区队列中。
...
//大块数据传送
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITS);
/* Bulk data transfer: receiver */
//去掉tcp头部
__skb_pull(skb, tcp_header_len);
//将数据包加入到sk_receive_queue队列中
__skb_queue_tail(&sk->sk_receive_queue, skb);
skb_set_owner_r(skb, sk);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
...
(5)数据接受后处理
到目前接受的是有效数据,tcp_event_data_recv更新延迟回答时钟超时间隔值,tcp_ack处理输入ack,如果不需要发送ack就调转到no_ack标签处,sk->sk_data_read函数指针指向data_ready,表明套接字已经准备号下一次应用读。
...
//更新延迟回答时钟超时间隔值
tcp_event_data_recv(sk, skb);
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
/* Well, only one small jumplet in fast path... */
//收到数据后回复ack确认
tcp_ack(sk, skb, FLAG_DATA);
tcp_data_snd_check(sk);
if (!inet_csk_ack_scheduled(sk))
goto no_ack;
}
if (!copied_early || tp->rcv_nxt != tp->rcv_wup)
__tcp_ack_snd_check(sk, 0);
no_ack:
#ifdef CONFIG_NET_DMA
if (copied_early)
__skb_queue_tail(&sk->sk_async_wait_queue, skb);
else
#endif
if (eaten)
__kfree_skb(skb);
else
//no_ack标签表明套接字已经准备好下一次应用读
sk->sk_data_ready(sk, 0);
return 0;
}
}
...
(6)、Slow Path处理
函数由内核内部bottom half调用或者是prequeue队列不能处理就会进入Slow Path处理,调用tcp_validate_incoming做检验。
...
slow_path:
//重新计算校验和
if (len < (th->doff << 2) || tcp_checksum_complete_user(sk, skb))
goto csum_error;
/*
* Standard slow path.
*/
res = tcp_validate_incoming(sk, skb, th, 1);
if (res <= 0)
return -res;
...
(7)、最后处理
处理URG标志,然后调用tcp_data_queue将数据复制到应用层或者放入提哦啊接字常规接受队列中。
...
step5:
if (th->ack && tcp_ack(sk, skb, FLAG_SLOWPATH) < 0)
goto discard;
tcp_rcv_rtt_measure_ts(sk, skb);
/* Process urgent data. */
//紧急数据段处理
tcp_urg(sk, skb, th);
/* step 7: process the segment text */
//根据情况将数据复制到应用层或者
//将数据加入sk_receive_queue常规队列中
tcp_data_queue(sk, skb);
tcp_data_snd_check(sk);
tcp_ack_snd_check(sk);
return 0;
...
tcp_rcv_established完整代码
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
{
struct tcp_sock *tp = tcp_sk(sk);
int res;
/*
* Header prediction.
* The code loosely follows the one in the famous
* "30 instruction TCP receive" Van Jacobson mail.
*
* Van's trick is to deposit buffers into socket queue
* on a device interrupt, to call tcp_recv function
* on the receive process context and checksum and copy
* the buffer to user space. smart...
*
* Our current scheme is not silly either but we take the
* extra cost of the net_bh soft interrupt processing...
* We do checksum and copy also but from device to kernel.
*/
tp->rx_opt.saw_tstamp = 0;
/* pred_flags is 0xS?10 << 16 + snd_wnd
* if header_prediction is to be made
* 'S' will always be tp->tcp_header_len >> 2
* '?' will be 0 for the fast path, otherwise pred_flags is 0 to
* turn it off (when there are holes in the receive
* space for instance)
* PSH flag is ignored.
*/
//预定向标志和输入数据段的标志比较
//数据段序列号是否正确
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;
/* Timestamp header prediction: tcp_header_len
* is automatically equal to th->doff*4 due to pred_flags
* match.
*/
/* Check timestamp */
//时间戳选项之外如果还有别的选项就送给Slow Path处理
if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
/* No? Slow path! */
if (!tcp_parse_aligned_timestamp(tp, th))
goto slow_path;
//对数据包做PAWS快速检查,如果检查走Slow Path处理
/* If PAWS failed, check it more carefully in slow path */
if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
goto slow_path;
/* DO NOT update ts_recent here, if checksum fails
* and timestamp was corrupted part, it will result
* in a hung connection since we will drop all
* future packets due to the PAWS test.
*/
}
//数据包长度太小
if (len <= tcp_header_len) {
/* Bulk data transfer: sender */
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 0;
} else { /* Header too small */
TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);
goto discard;
}
} else {
int eaten = 0;
int copied_early = 0;
//tp->copied_seq表示未读的数据包序列号
//tp->rcv_nxt表示下一个期望读取的数据包序列号
//len-tcp_header_len小于tp->ucpoy.len表示数据包还没有复制完
if (tp->copied_seq == tp->rcv_nxt &&
len - tcp_header_len <= tp->ucopy.len) {
#ifdef CONFIG_NET_DMA
if (tcp_dma_try_early_copy(sk, skb, tcp_header_len)) {
copied_early = 1;
eaten = 1;
}
#endif
//当前进程是否有锁定
//当前进程的全局指针current
//tp->ucopy.task指针是否等于当前进程
if (tp->ucopy.task == current &&
sock_owned_by_user(sk) && !copied_early) {
__set_current_state(TASK_RUNNING);
//将数据包复制到应用层空间
if (!tcp_copy_to_iovec(sk, skb, tcp_header_len))
eaten = 1;
}
//复制成功
if (eaten) {
/* 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);
tcp_rcv_rtt_measure_ts(sk, skb);
__skb_pull(skb, tcp_header_len);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITSTOUSER);
}
//清除prequeue队列中已经复制的数据包,并回复ack
if (copied_early)
tcp_cleanup_rbuf(sk, skb->len);
}
//复制不成功
if (!eaten) {
//从新计算校验和
if (tcp_checksum_complete_user(sk, skb))
goto csum_error;
/* 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);
tcp_rcv_rtt_measure_ts(sk, skb);
if ((int)skb->truesize > sk->sk_forward_alloc)
goto step5;
//大块数据传送
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITS);
/* Bulk data transfer: receiver */
//去掉tcp头部
__skb_pull(skb, tcp_header_len);
//将数据包加入到sk_receive_queue队列中
__skb_queue_tail(&sk->sk_receive_queue, skb);
skb_set_owner_r(skb, sk);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
}
//更新延迟回答时钟超时间隔值
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;
}
//收到数据后回复ack确认
if (!copied_early || tp->rcv_nxt != tp->rcv_wup)
__tcp_ack_snd_check(sk, 0);
no_ack:
#ifdef CONFIG_NET_DMA
if (copied_early)
__skb_queue_tail(&sk->sk_async_wait_queue, skb);
else
#endif
if (eaten)
__kfree_skb(skb);
else
//no_ack标签表明套接字已经准备好下一次应用读
sk->sk_data_ready(sk, 0);
return 0;
}
}
slow_path:
if (len < (th->doff << 2) || tcp_checksum_complete_user(sk, skb))
goto csum_error;
/*
* Standard slow path.
*/
res = tcp_validate_incoming(sk, skb, th, 1);
if (res <= 0)
return -res;
step5:
if (th->ack && tcp_ack(sk, skb, FLAG_SLOWPATH) < 0)
goto discard;
tcp_rcv_rtt_measure_ts(sk, skb);
/* Process urgent data. */
//紧急数据段处理
tcp_urg(sk, skb, th);
/* step 7: process the segment text */
//根据情况将数据复制到应用层或者
//将数据加入sk_receive_queue常规队列中
tcp_data_queue(sk, skb);
tcp_data_snd_check(sk);
tcp_ack_snd_check(sk);
return 0;
csum_error:
TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);
discard:
__kfree_skb(skb);
return 0;
}