1. 程式人生 > >TCP協議ESTABLISHED狀態處理

TCP協議ESTABLISHED狀態處理

TCP協議是可靠的、快速傳遞資料的協議,當套接字狀態是ESTABLISHED狀態表明兩端已經建立連線,可以互相傳送資料了,tcp_v4_do_rcv接受到資料後首先檢查套接字狀態,如果是ESTABLISHED就交給tcp_rcv_established函式處理具體資料接受過程。如果是LISTEN就由tcp_v4_hnd_req處理,如果是其他狀態就由tcp_rcv_state+process處理,關係圖如下。

tcp建立連線和資料接受關係圖

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;
}