1. 程式人生 > >TCP協議接受IP層資料包過程

TCP協議接受IP層資料包過程

IP層函式ip_local_deliver函式處理對資料包處理接受後根據iphdr->protocol資料域中協議號在inet_protocol全域性變數中查詢傳輸層的接受函式,TCP協議的接受函式是tcp_v4_rcv,tcp_v4_rcv函式的功能主要包含兩個方面:

(1)、資料包合法性檢查

(2)、確定資料包是快速路徑處理還是慢速路徑處理

下面分析tcp_v4_rcv函式的處理分析。

1、資料包正確性檢查

(1)、skb->pkt_type判斷目的地是否是本機,如果不是本機就扔掉資料包。

(2)、呼叫pskb_may_pull檢視TCP協議頭的正確性。

(3)、儲存TCP協議頭部到區域性變數th中,然後檢查協議頭長度th->doff是否正確,TCP協議頭的每個單元佔32位也就是4位元組,               所以sizeof(struct tcphdr)/4為TCP協議頭佔用的單元數。

(4)、檢查TCP資料段校驗和的正確資訊,不正確就扔掉資料包。

int tcp_v4_rcv(struct sk_buff *skb)
{
	const struct iphdr *iph;
	struct tcphdr *th;
	struct sock *sk;
	int ret;
	struct net *net = dev_net(skb->dev);

	//資料包目的不是本地就扔掉
	if (skb->pkt_type != PACKET_HOST)
		goto discard_it;

	/* Count it even if it's bad */
	TCP_INC_STATS_BH(net, TCP_MIB_INSEGS);

	//檢查TCP協議頭的正確性
	if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
		goto discard_it;

	th = tcp_hdr(skb);

	//頭部長度檢查
	if (th->doff < sizeof(struct tcphdr) / 4)
		goto bad_packet;
	if (!pskb_may_pull(skb, th->doff * 4))
		goto discard_it;

	/* An explanation is required here, I think.
	 * Packet length and doff are validated by header prediction,
	 * provided case of th->doff==0 is eliminated.
	 * So, we defer the checks. */
	 //檢查校驗和
	if (!skb_csum_unnecessary(skb) && tcp_v4_checksum_init(skb))
		goto bad_packet;
...
}

2、儲存協議頭控制資訊

將TCP協議的控制資訊儲存到skb的控制緩衝區,TCP控制緩衝區結構體tcp_skb_cb通過巨集TCP_SKB_CB(skb)來訪問,主要儲存有:

(1)、TCP資料段起始序列號seq。

(2)、TCP回答序列號ack_seq。

(3)、when用於資料包重傳計算時間

...

th = tcp_hdr(skb);
	iph = ip_hdr(skb);
	//起始序列號
	TCP_SKB_CB(skb)->seq = ntohl(th->seq);
	TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
				    skb->len - th->doff * 4);
	//回答序列號
	TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
	//重傳時間
	TCP_SKB_CB(skb)->when	 = 0;
	TCP_SKB_CB(skb)->flags	 = iph->tos;
	//選擇回答
	TCP_SKB_CB(skb)->sacked	 = 0;

...

3、查詢套接字

呼叫函式__inet_lookup_skb查詢資料段所屬的套接字,根據資料包的網路埠、源埠號、目的埠號在雜湊表中查詢

...

//根據埠在雜湊表中查詢套接字
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
	if (!sk)
		goto no_tcp_socket;

...

如果沒有找到開啟的套接字就調轉到no_tcp_socket標籤處理,沒有開啟的套接字也要對資料包完成校驗和檢查,如果資料包不正確就更新接受的錯誤資訊,如果資料包完好無損表名對端方資料段給一個已經關閉或者沒有開啟的套接字,這時要回復一個復位請求來告訴對端關閉連線,然後扔掉資料包,釋放記憶體。

...

no_tcp_socket:
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
		goto discard_it;
	//校驗和檢查
	if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
bad_packet:
		TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
	} else {
		//沒有開啟的套接字就傳送一個復位請求來告訴對端關閉連線
		tcp_v4_send_reset(NULL, skb);
	}

...

4、資料段處理

數段處理主要有三個方面:

(1)、看套接字的狀態是否是TIME_WAIT,如果是TIME_WAIT就調轉到do_time_wait標籤處理。

(2)、檢查是否配置了IPsec安全策略,如果配置就要對IPsec策略檢查,如果檢查為通過就扔到資料包。

(3)、獲取鎖防止併發訪問套接字,如果獲取鎖失敗就呼叫sk_add_backlog函式把資料段加入到backlog_queue佇列中,如果獲取鎖成功就呼叫tcp_prequeue函式把資料段放入prequeue佇列中,prequeue佇列存放在使用者程序管理複製資料的struct ucopy資料結構中(struct ucopy是tcp_sock資料結構的一部分),資料段加入到prequeue佇列後就交給應用程式處理。如果tcp_prequeue返回0,說明當前沒有使用者程序處理這個套接字,這時呼叫tcp_v4_do_rcv函式繼續資料段的Slow Path接受處理,這個函式主要是把資料加入到sk_receive_queue佇列中。

...

process:
	//套接字狀態是TIME_WAIT就調轉到超時處理
	if (sk->sk_state == TCP_TIME_WAIT)
		goto do_time_wait;

	if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) {
		NET_INC_STATS_BH(net, LINUX_MIB_TCPMINTTLDROP);
		goto discard_and_relse;
	}

	//IPsec策略檢查處理
	if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
		goto discard_and_relse;
	nf_reset(skb);

	if (sk_filter(sk, skb))
		goto discard_and_relse;

	skb->dev = NULL;

	//加鎖
	bh_lock_sock_nested(sk);
	ret = 0;
	if (!sock_owned_by_user(sk)) {
#ifdef CONFIG_NET_DMA
		//DMA方式處理
		struct tcp_sock *tp = tcp_sk(sk);
		if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)
			tp->ucopy.dma_chan = dma_find_channel(DMA_MEMCPY);
		if (tp->ucopy.dma_chan)
			ret = tcp_v4_do_rcv(sk, skb);
		else
#endif
		{	//將資料包加入prequeue佇列按Past Path路徑處理
			if (!tcp_prequeue(sk, skb))
				//不成功呼叫tcp_v4_do_rcv按照Slow Path處理
				ret = tcp_v4_do_rcv(sk, skb);
		}
	//加鎖不成功將資料段加入backlog_queue佇列
	} else if (unlikely(sk_add_backlog(sk, skb))) {
		bh_unlock_sock(sk);
		NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
		goto discard_and_relse;
	}
	bh_unlock_sock(sk);
	//應用計數減1,並是否套接字
	sock_put(sk);

	return ret;

...

5、TIME_WAIT處理

首先呼叫tcp_timewait_state_process函式獲取資料包的型別,是SYN資料包、FIN資料包、資料段資料包,然後分別處理,各種狀態處理如下:

TCP_TW_SUCCESS:延遲資料包或者源ACK的複製,處理方式是扔掉資料包。

TCP_TW_RST:結束連線的FIN,傳送一個復位給對端關閉連線。

TCP_TW_ACK:收到最後一個ACK,回覆對端一個ACK並關閉連線。

TCP_TW_SYN:收到建立連線請求,呼叫tcp_v4_lookup_listener函式來建立監聽連線程序並建立新的連線。

...

do_time_wait:
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
		inet_twsk_put(inet_twsk(sk));
		goto discard_it;
	}

	if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
		TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
		inet_twsk_put(inet_twsk(sk));
		goto discard_it;
	}
	//獲取資料包型別
	switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
	case TCP_TW_SYN: {
		//如果是SYN請求,就開啟套接字連線
		struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
							&tcp_hashinfo,
							iph->daddr, th->dest,
							inet_iif(skb));
		if (sk2) {
			inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
			inet_twsk_put(inet_twsk(sk));
			sk = sk2;
			goto process;
		}
		/* Fall through to ACK */
	}
	//收到最後一個ACK,也回覆對端一個ACK並關閉連線
	case TCP_TW_ACK:
		tcp_v4_timewait_ack(sk, skb);
		break;
		//接受連線FIN就回復一個復位給對端
	case TCP_TW_RST:
		goto no_tcp_socket;
		//延遲資料段直接扔掉資料包
	case TCP_TW_SUCCESS:;
	}

...

6、tcp_v4_rcv函式流程圖

tcp_v4_do_rcv函式處理流程