TCP協議ESTABLISHED狀態處理
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;
}