爲什麼內核中TCP連接在第一次握手後沒有進入SYN_RECV狀態?

注:本文分析基於3.10.0-693.el7內核版本,即CentOS 7.4

背景

之前梳理TCP建鏈的過程時,發現一個問題,百思不得其解。各種文章和書籍裏說的都是服務端在接收到客戶端的SYN報文後就進入SYN_RECV狀態,然而當我看內核源碼時,發現卻不是這樣的。連接在第一次握手後還是保持LISTEN狀態,只是請求被放入了半連接狀態。等到第三次握手後,服務端才根據半連接隊列裏的請求重新構造一個socket,此時新構造的,用於後續通信的socket的狀態才被置爲SYN_RECV,同時放入全連接隊列和established哈希表時,狀態再切爲established。疑問就此產生,爲什麼netstat命令查詢第一次握手後的連接時顯示的是SYN_RECV,這和內核狀態不一致啊。。。。雖然內核狀態和我們平時理解的也不一樣。所以就要看看netstat是怎麼查詢狀態的。

前提準備

1、由於netstat命令由net-tools組件提供,因此需要下載net-tools的源碼包。關於源碼包的下載,可以參考——《使用yum下載rpm源碼包》

2、源碼包下載後使用sourceinsight創建工程,便於閱讀。

查找netstat打印狀態實現源碼

以往都是正向梳理代碼流程,這次我就說說怎麼在net-tools源碼包中找到netstat命令實現的部分。
1、netstat中會輸出連接狀態,因此隨機選取一個狀態作爲關鍵詞進行搜索,這裏使用ESTABLISHED。搜索後如下,
在這裏插入圖片描述

只有五處,簡直太美麗了。

2、在netstat.c文件中可以看到對狀態的幾個枚舉值,

static const char *tcp_state[] =
{
    "",
    N_("ESTABLISHED"),
    N_("SYN_SENT"),
    N_("SYN_RECV"),
    N_("FIN_WAIT1"),
    N_("FIN_WAIT2"),
    N_("TIME_WAIT"),
    N_("CLOSE"),
    N_("CLOSE_WAIT"),
    N_("LAST_ACK"),
    N_("LISTEN"),
    N_("CLOSING")
};

這個枚舉值和內核中是一樣的,包括順序,爲了後面打印狀態。

3、既然tcp_state是狀態數組,打印的時候肯定也是會用到的,因此再用tcp_state作爲關鍵詞搜索,
在這裏插入圖片描述
只有三個結果,又是美麗的結果。一眼看過去就知道是在第三個結果裏打印的連接狀態。

4、那我們就來看看tcp_do_one這個函數了。

static void tcp_do_one(int lnr, const char *line, const char *prot)
{
	...
	//從緩衝區讀取連接信息
    num = sscanf(line,
    "%d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %X %lX:%lX %X:%lX %lX %d %d %lu %*s\n",
		 &d, local_addr, &local_port, rem_addr, &rem_port, &state,
		 &txq, &rxq, &timer_run, &time_len, &retr, &uid, &timeout, &inode);
	...
	//打印連接信息
	printf("%-4s  %6ld %6ld %-*s %-*s %-11s",
	       prot, rxq, txq, (int)netmax(23,strlen(local_addr)), local_addr, (int)netmax(23,strlen(rem_addr)), rem_addr, _(tcp_state[state]));

	finish_this_one(uid,inode,timers);
}

這下我們的關注點就在line這個入參了。

5、通過sourceinsight的調用關係圖,
在這裏插入圖片描述
可以知道只有tcp_info裏有調用。

6、那就看看tcp_info怎麼搞的唄。

#define _PATH_PROCNET_TCP		"/proc/net/tcp"
#define _PATH_PROCNET_TCP6		"/proc/net/tcp6"

static int tcp_info(void)
{
	INFO_GUTS6(_PATH_PROCNET_TCP, _PATH_PROCNET_TCP6, "AF INET (tcp)",
	           tcp_do_one , "tcp", "tcp6");
}

#define INFO_GUTS6(file,file6,name,proc,prot4,prot6)	\
 char buffer[8192];					\
 int rc = 0;						\
 int lnr = 0;						\
 if (!flag_arg || flag_inet) {				\
    INFO_GUTS1(file,name,proc,prot4)			\
 }							\
 if (!flag_arg || flag_inet6) {				\
    INFO_GUTS2(file6,proc,prot6)			\
 }							\
 INFO_GUTS3

#define INFO_GUTS1(file,name,proc,prot)			\
  procinfo = proc_fopen((file));			\
  if (procinfo == NULL) {				\
    if (errno != ENOENT) {				\
      perror((file));					\
      return -1;					\
    }							\
    if (!flag_noprot && (flag_arg || flag_ver))		\
      ESYSNOT("netstat", (name));			\
    if (!flag_noprot && flag_arg)			\
      rc = 1;						\
  } else {						\
    do {						\
      if (fgets(buffer, sizeof(buffer), procinfo))	\
        (proc)(lnr++, buffer,prot);			\
    } while (!feof(procinfo));				\
    fclose(procinfo);					\
  }
  ...

所以最終發現,其實netstat也只是從**/proc/net/tcp**這個文件中讀取的。

好吧,壓力又來到了內核這邊。

內核如何生成/proc/net/tcp的內容

1、根據/proc/net/tcp的內容中有local_address字樣,因此以此爲關鍵字在內核裏搜索,
在這裏插入圖片描述

2、很好,這下我們看看tcp4_seq_show這個函數。

static int tcp4_seq_show(struct seq_file *seq, void *v)
{
	struct tcp_iter_state *st;
	struct sock *sk = v;
	int len;

	if (v == SEQ_START_TOKEN) {
		seq_printf(seq, "%-*s\n", TMPSZ - 1,
		           "  sl  local_address rem_address   st tx_queue "
		           "rx_queue tr tm->when retrnsmt   uid  timeout "
		           "inode");
		goto out;
	}
	st = seq->private;

	switch (st->state) {
	case TCP_SEQ_STATE_LISTENING:
	case TCP_SEQ_STATE_ESTABLISHED:
		if (sk->sk_state == TCP_TIME_WAIT)
			get_timewait4_sock(v, seq, st->num, &len);
		else
			get_tcp4_sock(v, seq, st->num, &len);
		break;
	case TCP_SEQ_STATE_OPENREQ:
		get_openreq4(st->syn_wait_sk, v, seq, st->num, st->uid, &len);
		break;
	}
	seq_printf(seq, "%*s\n", TMPSZ - 1 - len, "");
out:
	return 0;
}

static void get_openreq4(const struct sock *sk, const struct request_sock *req,
			 struct seq_file *f, int i, kuid_t uid, int *len)
{
	const struct inet_request_sock *ireq = inet_rsk(req);
	long delta = req->expires - jiffies;

	seq_printf(f, "%4d: %08X:%04X %08X:%04X"
		" %02X %08X:%08X %02X:%08lX %08X %5u %8d %u %d %pK%n",
		i,
		ireq->ir_loc_addr,
		ntohs(inet_sk(sk)->inet_sport),
		ireq->ir_rmt_addr,
		ntohs(ireq->ir_rmt_port),
		TCP_SYN_RECV,
		0, 0, /* could print option size, but that is af dependent. */
		1,    /* timers active (only the expire timer) */
		jiffies_delta_to_clock_t(delta),
		req->num_timeout,
		from_kuid_munged(seq_user_ns(f), uid),
		0,  /* non standard timer */
		0, /* open_requests have no inode */
		atomic_read(&sk->sk_refcnt),
		req,
		len);
}

由此可知,當st->state的狀態爲TCP_SEQ_STATE_OPENREQ時,通過get_openreq4直接打印SYN_RECV狀態的連接。因此,我們需要知道是在哪裏設置的這個值。

3、以TCP_SEQ_STATE_OPENREQ爲關鍵詞搜索,
在這裏插入圖片描述
天公作美,只有一處是賦值的。

4、千呼萬喚始出來,就在listening_get_next函數中。

static void *listening_get_next(struct seq_file *seq, void *cur)
{
	struct inet_connection_sock *icsk;
	struct hlist_nulls_node *node;
	struct sock *sk = cur;
	struct inet_listen_hashbucket *ilb;
	struct tcp_iter_state *st = seq->private;
	struct net *net = seq_file_net(seq);

	if (!sk) {
		//直接獲取listen哈希表裏的連接
		ilb = &tcp_hashinfo.listening_hash[st->bucket];
		spin_lock_bh(&ilb->lock);
		sk = sk_nulls_head(&ilb->head);
		st->offset = 0;
		goto get_sk;
	}
	ilb = &tcp_hashinfo.listening_hash[st->bucket];
	++st->num;
	++st->offset;

	if (st->state == TCP_SEQ_STATE_OPENREQ) {
		struct request_sock *req = cur;

		icsk = inet_csk(st->syn_wait_sk);
		req = req->dl_next;
		while (1) {
			while (req) {
				//半連接隊列裏不爲空,且是我們需要查找的協議族(TCPV4|TCPV6等)
				if (req->rsk_ops->family == st->family) {
					cur = req;//找到了,返回
					goto out;
				}
				req = req->dl_next;
			}
			if (++st->sbucket >= icsk->icsk_accept_queue.listen_opt->nr_table_entries)
				break;
get_req:
			//獲取半連接隊列裏的請求
			req = icsk->icsk_accept_queue.listen_opt->syn_table[st->sbucket];
		}
		sk	  = sk_nulls_next(st->syn_wait_sk);
		st->state = TCP_SEQ_STATE_LISTENING;
		read_unlock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
	} else {
		icsk = inet_csk(sk);
		read_lock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
		//如果半連接隊列不爲空
		if (reqsk_queue_len(&icsk->icsk_accept_queue))
			goto start_req;
		read_unlock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
		sk = sk_nulls_next(sk);
	}
get_sk:
	sk_nulls_for_each_from(sk, node) {
		if (!net_eq(sock_net(sk), net))
			continue;
		if (sk->sk_family == st->family) {
			cur = sk;
			goto out;
		}
		icsk = inet_csk(sk);
		read_lock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
		if (reqsk_queue_len(&icsk->icsk_accept_queue)) {
start_req:
			st->uid		= sock_i_uid(sk);
			st->syn_wait_sk = sk;
			//設置TCP_SEQ_STATE_OPENREQ狀態,標識爲SYN_RECV狀態
			st->state	= TCP_SEQ_STATE_OPENREQ;
			st->sbucket	= 0;
			//獲取請求
			goto get_req;
		}
		read_unlock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
	}
	spin_unlock_bh(&ilb->lock);
	st->offset = 0;
	if (++st->bucket < INET_LHTABLE_SIZE) {
		ilb = &tcp_hashinfo.listening_hash[st->bucket];
		spin_lock_bh(&ilb->lock);
		sk = sk_nulls_head(&ilb->head);
		goto get_sk;
	}
	cur = NULL;
out:
	return cur;
}

所以說對於SYN_RECV狀態,並不是按照內核連接狀態打印的,而是和我們理解的一樣,在第一次握手後連接狀態就是SYN_RECV。

總結

我們知道TCP連接會存在於以下三個表或隊列中,
1、listen哈希表
2、半連接隊列
3、established哈希表

因此連接狀態的顯示邏輯爲,在listen哈希表中的連接就是LISTEN狀態,使用連接本身的狀態。在半連接隊列裏的連接也是LISTEN狀態,但是不使用連接本身狀態,而定義爲SYN_RECV狀態。而對於established哈希表裏的連接,其狀態和我們認爲的是一致的,因此直接使用內核中連接的狀態,這就是爲什麼開始時net-tools裏定義的tcp_state和內核中的一致的原因了。

終於把這個疑問解決了,開心。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章