1. 程式人生 > >TCP連線狀態管理

TCP連線狀態管理

TCP協議連線初始化後的狀態管理和切換由tcp_rcv_sate_preocess函式完成,tcp_v4_recevie函式收到資料包後檢視TCP協議頭,區分是隻含純傳送負載資料還是包含控制資訊SYN、FIN、RST、ACK等的資料包。各種狀態的資料包處理過程大部分在tcp_rcv_state_process函式中完成,除ESTABLISHE和TIME_WAIT這兩個狀態外。資料包到達後如果是CLOSED狀態就扔掉。

1、從LINSTEN到SYN_RECV

處於LINSTEN狀態表名套接字是一個伺服器,在等待一個連線請求,這時TCP協議收到的各種標誌資料包處理如下:

ACK:傳送連線復位。

RST:連線由客戶端復位,扔掉資料包。

SYN:客戶端傳送一個連線請求,呼叫icsk_af_ops->conne_rquest實際指向函式tcp_v4_conn_request,初始化序列號、傳送SYN             和ACK標誌給客戶端,將TCP狀態設定為TCP_SYN_RECV。

其他資料包:這時連線還沒建立,就扔掉。

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
			  struct tcphdr *th, unsigned len)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	int queued = 0;
	int res;

	tp->rx_opt.saw_tstamp = 0;

	switch (sk->sk_state) {
		//狀態是CLOSE直接扔掉資料包
	case TCP_CLOSE:
		goto discard;

	//表名套接字是一個伺服器
	case TCP_LISTEN:
		//收到ACK包返回1,傳送連線復位
		if (th->ack)
			return 1;
		//收到RST包直接扔掉
		if (th->rst)
			goto discard;

		if (th->syn) {
			//處理連線請求,實際呼叫tcp_v4_conn_request
			//回覆對端ACK SYN包
			if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
				return 1;
			kfree_skb(skb);
			return 0;
		}
		goto discard;

...

}

2、SYN_SENT到ESTABLISHED

套接字狀態是SYN_SENT,表名套接字是一個客戶單,它傳送了SYN包,在等待伺服器的SYN和ACK包,以確保狀態轉換到ESTABLISHED

...

	//表明是個客戶端
	case TCP_SYN_SENT:
		//處理成功從SYN_SET切換到ESTABLISHED
		queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
		if (queued >= 0)
			return queued;

		/* Do step6 onward by hand. */
		tcp_urg(sk, skb, th);
		__kfree_skb(skb);
		tcp_data_snd_check(sk);
		return 0;

...

(1)收到ACK

tcp_rcv_synsent_state_process函式會對資料包和TCP協議頭進行檢驗,如果資料包合法而且設定了正確的ACK標誌,就把套接字狀態切換到ESTABLISHED。

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
					 struct tcphdr *th, unsigned len)
{
...

        //收到ACK且資料包合法,切換到ESTABLISHED狀態
		tcp_set_state(sk, TCP_ESTABLISHED);

....

}

(2)收到連線復位

如果收到連線復位請求,則復位連線並扔掉資料包。

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
					 struct tcphdr *th, unsigned len)
{

...

        //收到連線復位,復位連線,並扔掉資料包
		if (th->rst) {
			tcp_reset(sk);
			goto discard;
		}

...

}

(3)沒有SYN標誌

如果資料包沒有SYN標誌,就扔掉。

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
					 struct tcphdr *th, unsigned len)
{

...

       //沒有SYN標誌扔掉資料包
		if (!th->syn)
			goto discard_and_undo;

...

}

(4)tcp_rcv_synsent_state_process返回值

tcp_rcv_synsent_state_process返回一個負值,表明資料段中還有資料等待處理,處理檢視URG標誌外不做任何處理。

...

//處理成功從SYN_SET切換到ESTABLISHED
		queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
		if (queued >= 0)
			return queued;

		/* Do step6 onward by hand. */
		tcp_urg(sk, skb, th);
		__kfree_skb(skb);
		tcp_data_snd_check(sk);

...

到目前位置處理了SYN_SENT、LINSTEN、CLOSE這三個狀態的套接字,其他狀態在接下來處理。

3、資料包有效性檢查

資料包的有效性檢查由函式tcp_validate_incoming完成,按照RFC793規範進行。

(1)、序列號檢查

tcp_sequence函式檢查序列號是否在視窗範圍內,如果超出了當前視窗就扔掉資料包。

static int tcp_validate_incoming(struct sock *sk, struct sk_buff *skb,
			      struct tcphdr *th, int syn_inerr)
{

...

/* Step 1: check sequence number */
	//第一步序列號檢查
	if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {
		/* RFC793, page 37: "In all states except SYN-SENT, all reset
		 * (RST) segments are validated by checking their SEQ-fields."
		 * And page 69: "If an incoming segment is not acceptable,
		 * an acknowledgment should be sent in reply (unless the RST
		 * bit is set, if so drop the segment and return)".
		 */
		 //函式復位標誌RST直接扔掉資料包並返回
		if (!th->rst)
			tcp_send_dupack(sk, skb);
		goto discard;
	}

...

}

(2)復位連線標誌

如果資料包有復位連線標誌RST,就復位連線,並扔掉資料包。

static int tcp_validate_incoming(struct sock *sk, struct sk_buff *skb,
			      struct tcphdr *th, int syn_inerr)
{

...

    //如果有復位標誌RST則復位連線,扔掉資料包
	/* Step 2: check RST bit */
	if (th->rst) {
		tcp_reset(sk);
		goto discard;
	}


...

}

(3)檢查SYN是否在視窗範圍內

static int tcp_validate_incoming(struct sock *sk, struct sk_buff *skb,
			      struct tcphdr *th, int syn_inerr)
{

...

    //如果是SYN,檢視序列號是否在當前視窗範圍內
	/* step 4: Check for a SYN in window. */
	if (th->syn && !before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
		if (syn_inerr)
			TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONSYN);
		tcp_reset(sk);
		return -1;
	}

...

}

4、資料包有效:有ACK標誌

(1)TCP_SYN_RECV

如果收到ACK標誌資料包,而且套接字狀態處於SYN_RECV,這時最大可能是處於被迫開啟狀態,應切換到ESTABLISH狀態,計算RTT,如果接受到的ACK資料包中有時間戳選項,RTT基於時間戳計算,RTT的值儲存在struct tcp_sock資料結構的srtt資料域中,重新構建協議頭

...

/* step 5: check the ACK field */
	if (th->ack) {
		int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH) > 0;

		switch (sk->sk_state) {
		//收到ACK,連線狀態處於SYN_RECV,這時處於被迫開啟的狀態
		//狀態切換到ESTABLISHE
		case TCP_SYN_RECV:
			if (acceptable) {
				tp->copied_seq = tp->rcv_nxt;
				smp_mb();
				tcp_set_state(sk, TCP_ESTABLISHED);
				sk->sk_state_change(sk);

				/* Note, that this wakeup is only for marginal
				 * crossed SYN case. Passively open sockets
				 * are not waked up, because sk->sk_sleep ==
				 * NULL and sk->sk_socket == NULL.
				 */
				 //喚醒套接字
				if (sk->sk_socket)
					sk_wake_async(sk,
						      SOCK_WAKE_IO, POLL_OUT);

				tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
				tp->snd_wnd = ntohs(th->window) <<
					      tp->rx_opt.snd_wscale;
				tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);

				/* tcp_ack considers this ACK as duplicate
				 * and does not calculate rtt.
				 * Force it here.
				 */
				 //計算RTT
				tcp_ack_update_rtt(sk, 0, 0);

				if (tp->rx_opt.tstamp_ok)
					tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;

				/* Make sure socket is routed, for
				 * correct metrics.
				 */
				 //重新構建TCP頭部
				icsk->icsk_af_ops->rebuild_header(sk);

				//初始化套接字某些欄位
				tcp_init_metrics(sk);

				tcp_init_congestion_control(sk);

				/* Prevent spurious tcp_cwnd_restart() on
				 * first data packet.
				 */
				tp->lsndtime = tcp_time_stamp;

				tcp_mtup_init(sk);
				//套接字上的預留緩衝區基於收到的MSS值來確定
				//初始化視窗大小一個猜測值
				tcp_initialize_rcv_mss(sk);
               //為套接字預留緩衝區空間
				tcp_init_buffer_space(sk);
                //計算struc tcp_sock資料結構上的pred_flags資料域
                //該資料域決定是否應交給Fast Path處理
				tcp_fast_path_on(tp);
			} else {
				return 1;
			}
			break;

...

(2)FIN_WAIT_1

如果套接字狀態處於FIN_WAIT_1收到一個ACK包,套接字狀態就會切換到FIN_WAIT_2,同時設定套接字的shutdown資料域的值為SEND_SHUTDOWN,指明隨後套接字切換成CLOSED狀態時應向站點發送包含RST的資料包shutdown。

處理TCP選項:

TCP_LINGER2選項:決定套接字進入CLOSED狀態之前,需要在FIN_WAIT_2狀態上等待多長時間,他的值儲存在struct                                              tcp_sock tp->linger2資料域中,如果linger2為負值,則套接字立即切換到CLOSED狀態,不經過                                                    FIN_WAIT_2和TIMEI_WAIT狀態。

keepalive選項:keepalive時鐘超時的情況要被複位。

如果收到ACK資料包是最後一個回答FIN,或套接字被其他程序鎖定,則復位keepalive時鐘,如果不這麼做就會丟失FIN。

....

case TCP_FIN_WAIT1:
			//FIN_WAIT1狀態收到ACK切換到FIN_WAIT2
			if (tp->snd_una == tp->write_seq) {
				tcp_set_state(sk, TCP_FIN_WAIT2);
				//設定shutdown資料域為SEND_SHUTDOWN
				//指明套接字切換成CLOSED要傳送RST資料包
				sk->sk_shutdown |= SEND_SHUTDOWN;
				dst_confirm(__sk_dst_get(sk));

				if (!sock_flag(sk, SOCK_DEAD))
					/* Wake up lingering close() */
					sk->sk_state_change(sk);
				else {
                    //不是一個死套接字
					int tmo;

					//決定套接字進入CLOSED狀態前要在FIN_WAIT2狀態等待多長時間
					//這個時間儲存在tp->linger2,如果linger2小於0,則套接字立即從
					//FIN_WAIT1切換到CLISED,不經過FIN_WAIT2
					if (tp->linger2 < 0 ||
					    (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
					     after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt))) {
						tcp_done(sk);
						NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
						return 1;
					}

					tmo = tcp_fin_time(sk);
					
					if (tmo > TCP_TIMEWAIT_LEN) {
						//keepalive時鐘超時情況下被複位
						inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN);
					} else if (th->fin || sock_owned_by_user(sk)) {
						/* Bad case. We could lose such FIN otherwise.
						 * It is not a big problem, but it looks confusing
						 * and not so rare event. We still can lose it now,
						 * if it spins in bh_lock_sock(), but it is really
						 * marginal case.
						 */
						 //收到的ACK是最後一個回答FIN
						 //或者套接字被其他程序鎖定就要復位時鐘
						inet_csk_reset_keepalive_timer(sk, tmo);
					} else {
						//複製進入FIN_WAIT2
						tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
						goto discard;
					}
				}
			}
			break;

...

(3)CLOSING

收到ACK後套接字直接進入TIME_WAIT狀態,說明對端沒有資料向外傳送了。

...

case TCP_CLOSING:
			//TCP_CLOSING收到ACK直接進入TIME_WAIT,
			//表名對端沒有向外傳送資料了
			if (tp->snd_una == tp->write_seq) {
				tcp_time_wait(sk, TCP_TIME_WAIT, 0);
				goto discard;
			}
			break;

...

(4)LAST_ACK

套接字被迫關閉,響應應用程式close,收到ACK就關閉套接字,所以呼叫tcp_done。

...

case TCP_LAST_ACK:
			//套接字被迫關閉,這個狀態收到ACK就可以關閉套接字
			//所以呼叫tcp_done關閉套接字
			if (tp->snd_una == tp->write_seq) {
				tcp_update_metrics(sk);
				tcp_done(sk);
				goto discard;
			}
			break;

...

5、處理段中的資料內容

以下5中狀態多要將資料段放入佇列中:case TCP_CLOSE_WAIT、TCP_CLOSING、TCP_LAST_ACK、TCP_FIN_WAIT1、TCP_FIN_WAIT2併發送一個復位。

...

//以下幾種狀態是可以接受資料的
	//處理資料包
	/* step 7: process the segment text */
	switch (sk->sk_state) {
	case TCP_CLOSE_WAIT:
	case TCP_CLOSING:
	case TCP_LAST_ACK:
		if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt))
			break;
	case TCP_FIN_WAIT1:
	case TCP_FIN_WAIT2:
		/* RFC 793 says to queue data in these states,
		 * RFC 1122 says we MUST send a reset.
		 * BSD 4.4 also does reset.
		 */
		if (sk->sk_shutdown & RCV_SHUTDOWN) {
			if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
			    after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) {
				NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
				tcp_reset(sk);
				return 1;
			}
		}
		/* Fall through */

...

6、ESTABLISHED狀態處理

套接字狀態為ESTABLISHED狀態是可以收到常規資料段的,它呼叫tcp_data_queue函式將資料段放入套接字輸入緩衝區佇列中。

tcp_rcv_state_process完整程式碼:

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
			  struct tcphdr *th, unsigned len)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	int queued = 0;
	int res;

	tp->rx_opt.saw_tstamp = 0;

	switch (sk->sk_state) {
		//狀態是CLOSE直接扔掉資料包
	case TCP_CLOSE:
		goto discard;

	//表名套接字是一個伺服器
	case TCP_LISTEN:
		//收到ACK包返回1,傳送連線復位
		if (th->ack)
			return 1;
		//收到RST包直接扔掉
		if (th->rst)
			goto discard;

		if (th->syn) {
			//處理連線請求,實際呼叫tcp_v4_conn_request
			//回覆對端ACK SYN包
			if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
				return 1;

			/* Now we have several options: In theory there is
			 * nothing else in the frame. KA9Q has an option to
			 * send data with the syn, BSD accepts data with the
			 * syn up to the [to be] advertised window and
			 * Solaris 2.1 gives you a protocol error. For now
			 * we just ignore it, that fits the spec precisely
			 * and avoids incompatibilities. It would be nice in
			 * future to drop through and process the data.
			 *
			 * Now that TTCP is starting to be used we ought to
			 * queue this data.
			 * But, this leaves one open to an easy denial of
			 * service attack, and SYN cookies can't defend
			 * against this problem. So, we drop the data
			 * in the interest of security over speed unless
			 * it's still in use.
			 */
			kfree_skb(skb);
			return 0;
		}
		goto discard;
	//表明是個客戶端
	case TCP_SYN_SENT:
		//處理成功從SYN_SET切換到ESTABLISHED
		queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
		if (queued >= 0)
			return queued;

		/* Do step6 onward by hand. */
		tcp_urg(sk, skb, th);
		__kfree_skb(skb);
		tcp_data_snd_check(sk);
		return 0;
	}

	//對資料包檢查
	res = tcp_validate_incoming(sk, skb, th, 0);
	if (res <= 0)
		return -res;

	/* step 5: check the ACK field */
	if (th->ack) {
		int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH) > 0;

		switch (sk->sk_state) {
		//收到ACK,連線狀態處於SYN_RECV,這時處於被迫開啟的狀態
		//狀態切換到ESTABLISHE
		case TCP_SYN_RECV:
			if (acceptable) {
				tp->copied_seq = tp->rcv_nxt;
				smp_mb();
				tcp_set_state(sk, TCP_ESTABLISHED);
				sk->sk_state_change(sk);

				/* Note, that this wakeup is only for marginal
				 * crossed SYN case. Passively open sockets
				 * are not waked up, because sk->sk_sleep ==
				 * NULL and sk->sk_socket == NULL.
				 */
				 //喚醒套接字
				if (sk->sk_socket)
					sk_wake_async(sk,
						      SOCK_WAKE_IO, POLL_OUT);

				tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
				tp->snd_wnd = ntohs(th->window) <<
					      tp->rx_opt.snd_wscale;
				tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);

				/* tcp_ack considers this ACK as duplicate
				 * and does not calculate rtt.
				 * Force it here.
				 */
				 //計算RTT
				tcp_ack_update_rtt(sk, 0, 0);

				if (tp->rx_opt.tstamp_ok)
					tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;

				/* Make sure socket is routed, for
				 * correct metrics.
				 */
				 //重新構建TCP頭部
				icsk->icsk_af_ops->rebuild_header(sk);

				//初始化套接字某些欄位
				tcp_init_metrics(sk);

				tcp_init_congestion_control(sk);

				/* Prevent spurious tcp_cwnd_restart() on
				 * first data packet.
				 */
				tp->lsndtime = tcp_time_stamp;

				tcp_mtup_init(sk);
				//套接字上的預留緩衝區基於收到的MSS值來確定
				//初始化視窗大小一個猜測值
				tcp_initialize_rcv_mss(sk);
				tcp_init_buffer_space(sk);
				tcp_fast_path_on(tp);
			} else {
				return 1;
			}
			break;

		case TCP_FIN_WAIT1:
			//FIN_WAIT1狀態收到ACK切換到FIN_WAIT2
			if (tp->snd_una == tp->write_seq) {
				tcp_set_state(sk, TCP_FIN_WAIT2);
				//設定shutdown資料域為SEND_SHUTDOWN
				//指明套接字切換成CLOSED要傳送RST資料包
				sk->sk_shutdown |= SEND_SHUTDOWN;
				dst_confirm(__sk_dst_get(sk));

				if (!sock_flag(sk, SOCK_DEAD))
					/* Wake up lingering close() */
					sk->sk_state_change(sk);
				else {
					int tmo;

					//決定套接字進入CLOSED狀態前要在FIN_WAIT2狀態等待多長時間
					//這個時間儲存在tp->linger2,如果linger2小於0,則套接字立即從
					//FIN_WAIT1切換到CLISED,不經過FIN_WAIT2
					if (tp->linger2 < 0 ||
					    (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
					     after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt))) {
						tcp_done(sk);
						NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
						return 1;
					}

					tmo = tcp_fin_time(sk);
					
					if (tmo > TCP_TIMEWAIT_LEN) {
						//keepalive時鐘超時情況下被複位
						inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN);
					} else if (th->fin || sock_owned_by_user(sk)) {
						/* Bad case. We could lose such FIN otherwise.
						 * It is not a big problem, but it looks confusing
						 * and not so rare event. We still can lose it now,
						 * if it spins in bh_lock_sock(), but it is really
						 * marginal case.
						 */
						 //收到的ACK是最後一個回答FIN
						 //或者套接字被其他程序鎖定就要復位時鐘
						inet_csk_reset_keepalive_timer(sk, tmo);
					} else {
						//複製進入FIN_WAIT2
						tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
						goto discard;
					}
				}
			}
			break;

		case TCP_CLOSING:
			//TCP_CLOSING收到ACK直接進入TIME_WAIT,
			//表名對端沒有向外傳送資料了
			if (tp->snd_una == tp->write_seq) {
				tcp_time_wait(sk, TCP_TIME_WAIT, 0);
				goto discard;
			}
			break;

		case TCP_LAST_ACK:
			//套接字被迫關閉,這個狀態收到ACK就可以關閉套接字
			//所以呼叫tcp_done關閉套接字
			if (tp->snd_una == tp->write_seq) {
				tcp_update_metrics(sk);
				tcp_done(sk);
				goto discard;
			}
			break;
		}
	} else
		goto discard;

	/* step 6: check the URG bit */
	tcp_urg(sk, skb, th);

	//以下幾種狀態是可以接受資料的
	//處理資料包
	/* step 7: process the segment text */
	switch (sk->sk_state) {
	case TCP_CLOSE_WAIT:
	case TCP_CLOSING:
	case TCP_LAST_ACK:
		if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt))
			break;
	case TCP_FIN_WAIT1:
	case TCP_FIN_WAIT2:
		/* RFC 793 says to queue data in these states,
		 * RFC 1122 says we MUST send a reset.
		 * BSD 4.4 also does reset.
		 */
		if (sk->sk_shutdown & RCV_SHUTDOWN) {
			if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
			    after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) {
				NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
				tcp_reset(sk);
				return 1;
			}
		}
		/* Fall through */
	case TCP_ESTABLISHED:
		tcp_data_queue(sk, skb);
		queued = 1;
		break;
	}

	/* tcp_data could move socket to TIME-WAIT */
	if (sk->sk_state != TCP_CLOSE) {
		tcp_data_snd_check(sk);
		tcp_ack_snd_check(sk);
	}

	if (!queued) {
discard:
		__kfree_skb(skb);
	}
	return 0;
}