为什么内核中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和内核中的一致的原因了。

终于把这个疑问解决了,开心。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值