Linux 网络:TCP 三握四挥简析

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本文基于 linux-4.14.132 内核代码进行分析。

3. TCP连接的建立和断开

3.1 TCP协议状态机

在这里插入图片描述

3.2 TCP的三握四挥

在这里插入图片描述
在后面的分析中,我们将始终参考 3.1,3.2 两小节中的状态图。

3.2.1 TCP 连接建立的三次握手过程分析

3.2.1.1 服务端和客户端套接字的创建

通过 socket() 系统调用创建套接字后,套接字初始状态为 CLOSED 状态(即 TCP_CLOSE)。来看代码实现细节:

/* 应用层 通过系统调用 sys_socket() 创建套接字 */
server_fd = socket(AF_INET, SOCK_STREAM, 0); // 服务端 

remote_fd = socket(AF_INET, SOCK_STREAM, 0); // 客户端

/* 内核空间:初始创建时,套接字为 CLOSED 状态(即 TCP_CLOSE) */
sys_socket(AF_INET, SOCK_STREAM, 0) // net/socket.c
	sock_create(family, type, protocol, &sock)
		__sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
			sock = sock_alloc();
			sock->type = type; // sock->type = SOCK_STREAM;
			...
			pf = rcu_dereference(net_families[family]); // 获取协议簇接口
			...
			// 进入协议簇(family)的套接字创建过程
			pf->create(net, sock, protocol, kern) = inet_create() // net/ipv4/af_inet.c
				struct sock *sk;
				
				sock->state = SS_UNCONNECTED; /* socket 初始为[未连接状态 (SS_UNCONNECTED)] */
				...
				list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
					if (protocol == answer->protocol) { /* 显式指定了 protocol */
						...
					}  else { /* 非显式指定 protocol */
						/* Check for the two wild cases. */
						if (IPPROTO_IP == protocol) { /* protocol == 0 意味着创建各 @type 下缺省协议的套接字 */
							protocol = answer->protocol;
							break;
						}
   						...
					}
					...
				}
				...
				/* 设定套接字对应协议接口 */
				sock->ops = answer->ops; /* 设定套接字对应协议接口: &inet_stream_ops */
				answer_prot = answer->prot; /* &tcp_prot */
				...
				/* 创建套接字[网络层管理数据]对象 */
				sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
					struct sock *sk;
					
					sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
					if (sk) {
						sk->sk_family = family; // sk->sk_family = PF_INET;
						sk->sk_prot = sk->sk_prot_creator = prot; /* &tcp_prot */
						...
					}
					
					return sk;
				...
				/* 初始化套接字[网络层管理数据]: 如 type, 状态等 (TCP_CLOSE) */
				sock_init_data(sock, sk);
					...
					//sk->sk_rcvbuf  = sysctl_rmem_default;
					//sk->sk_sndbuf  = sysctl_wmem_default;
					//sk->sk_state  = TCP_CLOSE; /* 设定套接字初始状态为 CLOSE */
					sk_set_socket(sk, sock); /* 绑定网络层管理数据到 socket */
						sk_tx_queue_clear(sk);
						sk->sk_socket = sock;
						...
					if (sock) {
						sk->sk_type = sock->type; // sk->sk_type = SOCK_STREAM;
						sk->sk_wq = sock->wq;
  						sock->sk = sk; /* 设定套接字的网络层管理数据对象 */
  						...
					}
					...
					sk->sk_state_change = sock_def_wakeup;
					sk->sk_data_ready = sock_def_readable;
					//sk->sk_write_space = sock_def_write_space;
					...
				sk->sk_destruct    = inet_sock_destruct;
				sk->sk_protocol    = protocol; // IPPROTO_TCP
				sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
				...
				/*
				 * 前面完成的是 IPv4 协议簇套接字公共初始化。
				 * 这里是 IPv4 协议簇下的子类型 @type 和 子协议 @protocol 套接字的特定初始化。
				 * 套接字的初始化是一个个层层递进的过程,有点类似于 C++ 子类对象的构建过程:
				 * 先调用父类的构造函数,然后在逐级调用子类的构造函数。
				 *
				 * 这里是 TCP 类型(IPPROTO_TCP) 套接字 初始化。
				 */
				if (sk->sk_prot->init) {
					err = sk->sk_prot->init(sk); /* tcp_v4_init_sock() */
					...
				}
				...

/* TCP 类型套接字初始化 */
tcp_v4_init_sock() // net/ipv4/tcp_ipv4.c
	struct inet_connection_sock *icsk = inet_csk(sk);
	
	/* TCP 套接字初始化 */
	tcp_init_sock(sk);
		...
		sk->sk_state = TCP_CLOSE; /* TCP 套接字创建时初始状态为 TCP_CLOSE */

		sk->sk_write_space = sk_stream_write_space;
		...
		sk->sk_sndbuf = sysctl_tcp_wmem[1];
		sk->sk_rcvbuf = sysctl_tcp_rmem[1];
		...

	/* 设定 IPv4 TCP 套接字操作接口 */
	icsk->icsk_af_ops = &ipv4_specific;
3.2.1.2 服务端进入 LISTEN 状态

调用 listen() 后,服务端监听套接字 由 CLOSED 状态进入 LISTEN :CLOSED => LISTEN

// 用户空间

struct sockaddr_in server_addr;
int backlog = 8;

memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888); // 服务端 端口号
server_addr.sin_addr.s_addr = inet_addr("192.168.1.123"); // 服务端 IP 地址
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

listen(server_fd, backlog); // 服务端: CLOSED => LISTEN
// 内核空间

sys_listen(fd, backlog) // net/socket.c
	struct socket *sock;
	
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (sock) {
		...
		err = sock->ops->listen(sock, backlog); /* inet_listen() */
		...
	}

inet_listen() // net/ipv4/af_inet.c
	struct sock *sk = sock->sk;
	unsigned char old_state;

	err = -EINVAL;
	// 处于未连接状态的、 SOCK_STREAM 类型套接字 才能监听
	if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
		goto out;
	
	old_state = sk->sk_state;
	/* 只有对 TCP_CLOSE 或 TCP_LISTEN 态套接字 listen 才是合法的 */
	if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
		goto out;
	
	if (old_state != TCP_LISTEN) {
		...
		/*
		 * . accept 队列初始化
		 * . backlog 初始化
		 * . 套接字由 TCP_CLOSE 转为 TCP_LISTEN 态 (CLOSED => LISTEN)
		 */
		err = inet_csk_listen_start(sk, backlog);
		if (err)
			goto out;
	}
	sk->sk_max_ack_backlog = backlog;
	err = 0;
	
out:
	return err;

inet_csk_listen_start() // net/ipv4/inet_connection_sock.c
	struct inet_connection_sock *icsk = inet_csk(sk);

	reqsk_queue_alloc(&icsk->icsk_accept_queue); // 创建和初始化 accept 队列(全连接队列)
	
	sk->sk_max_ack_backlog = backlog;
	sk->sk_ack_backlog = 0;
	inet_csk_delack_init(sk);

	sk_state_store(sk, TCP_LISTEN); // 由 TCP_CLOSE 转为 TCP_LISTEN 态 (CLOSED => LISTEN)
		smp_store_release(&sk->sk_state, newstate);
	...
3.2.1.3 服务端 应用程序 用 accept() 取出 已建立好 的 客户端连接

服务端 应用程序accept() 取出 位于 服务端监听套接字 accept 队列(全连接队列) 中、已建立好(已完成三次握手) 的 客户端连接套接字信息。如果服务端监听套接字 accept 队列(全连接队列)当前没有已建立好的客户端连接,则陷入睡眠等待,直到有新建立好(已完成三次握手)的客户端连接到来后被唤醒。

client_fd = accept(server_fd, NULL, NULL);

sys_accept(server_fd, NULL, NULL) // net/socket.c
	sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
		struct socket *sock, *newsock;
		
		sock = sockfd_lookup_light(fd, &err, &fput_needed);
		
		/* 为可能连接的客户端准备一个 sock 对象 */
		newsock = sock_alloc(); /* (1) */
		...

		newsock->type = sock->type; // SOCK_STREAM
 		newsock->ops = sock->ops; // &inet_stream_ops

		...
		
		/*
		 * 为客户端 sock 分配一个 fd 。
		 * 
		 * !!!
		 * 注意:
		 * @newfd 和 服务端监听套接字 @server_fd 不是同一个,@newfd 是 用来和 新的
		 * 客户端连接 通信 的 服务端 套接字,也即前面代码中 accept() 返回的 @client_fd 。
		 */
		newfd = get_unused_fd_flags(flags);
		/* 为客户端 sock 分配一个文件对象 (struct file) */
		newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
		...

		// 睡眠等待 完成握手过程、添加到 服务端监听套接字 @sock 的 accept 队列(全连接队列) 的 客户端连接
		err = sock->ops->accept(sock, newsock, sock->file->f_flags, false); /* inet_accept() */

		/* 将指代 已连接客户端 的 套接字 @newsock 的 fd 放入进程的 fd 表 */
		fd_install(newfd, newfile);
 		err = newfd; /* 返回 服务端 和 客户端通信的套接字 句柄 到用户空间 */

		...
	out:
 		return err;

inet_accept(newsock, flags, sock->sk->sk_prot_creator->name) // net/ipv4/af_inet.c
	struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err, kern); /* inet_csk_accept() */
		inet_csk_accept(sk1, flags, &err, kern) // net/ipv4/inet_connection_sock.c
			struct inet_connection_sock *icsk = inet_csk(sk);
			struct request_sock_queue *queue = &icsk->icsk_accept_queue;
			struct request_sock *req;
 			struct sock *newsk;

			/* Find already established connection */
			if (reqsk_queue_empty(queue)) { /* accept 队列(全连接队列) 为空,即 还没有 建立好的 客户端 连接 */
				long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); 
				
				// 这里只讨论阻塞方式,不关心非阻塞方式的逻辑
				
				/* If this is a non blocking socket don't sleep */
				error = -EAGAIN;
				if (!timeo)
					goto out_err;
				
				// 等待 直到有 建立好的 客户端连接
				error = inet_csk_wait_for_connect(sk, timeo);
				if (error)
					goto out_err;
			}
			req = reqsk_queue_remove(queue, sk); // 从 accpet 队列(全连接队列) 中取出/移除一个建立好的连接
 			newsk = req->sk; // 返回 新建立的、 和 客户端通信的 套接字
 			...
 	...
 	newsock->state = SS_CONNECTED; // 等待到客户端连接后,套接字标记为已连接状态 SS_CONNECTED

// 睡眠等待 新建立好的客户端连接 (已完成三次握手)
inet_csk_wait_for_connect(sk, timeo) // net/ipv4/inet_connection_sock.c
	struct inet_connection_sock *icsk = inet_csk(sk);
	DEFINE_WAIT(wait);
	int err;

	for (;;) {
		prepare_to_wait_exclusive(sk_sleep(sk), &wait,
					TASK_INTERRUPTIBLE);
		...
		if (reqsk_queue_empty(&icsk->icsk_accept_queue)) // 还没有 建立好的 客户端连接,陷入睡眠等待
			timeo = schedule_timeout(timeo);
		...
		if (!reqsk_queue_empty(&icsk->icsk_accept_queue)) // 有 建立好的 客户端连接了,结束等待
			break;
		...
		if (signal_pending(current)) // 被信号中断
			break;
		err = -EAGAIN;
		if (!timeo)
			break;
	}
	finish_wait(sk_sleep(sk), &wait);
	return err;
3.2.1.4 客户端 向 服务端 发送 SYN 连接请求,进入 SYN-SENT 态

客户端 通过 connect()服务端 (监听套接字) 发起了 SYN 连接请求,然后自身进入 SYN-SENT 态睡眠等待服务端的 SYN + ACK。来看这一个过程的代码实现细节:

// 客户端
int remote_fd;
struct sockaddr_in server_addr;

remote_fd = socket(AF_INET, SOCK_STREAM, 0);

memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT); // 服务端端口号
server_addr.sin_addr.s_addr = inet_addr("192.168.1.188"); // 假设服务端 IP 为 192.168.1.188
connect(remote_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))); // 连接服务端
/*
 * c1. 客户端 通过 connect() 向 服务端监听套接字 发送 SYN 连接请求。
 */
sys_connect(remote_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))) // net/socket.c
	struct socket *sock;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	...
	/* @uservaddr: 客户端 想连接的 目标地址 */
	err = move_addr_to_kernel(uservaddr, addrlen, &address);
	...
	err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags); // inet_stream_connect()
		inet_stream_connect() // net/ipv4/af_inet.c
			__inet_stream_connect(sock, uaddr, addr_len, flags, 0);
				switch (sock->state) {
				...
				// 发送 SYN 请求包到 服务端
				case SS_UNCONNECTED:
					...
					err = sk->sk_prot->connect(sk, uaddr, addr_len); /* tcp_v4_connect() */
					sock->state = SS_CONNECTING; /* 套接字标记为 正在连接状态 */
					...
					break;
				}

tcp_v4_connect(sk, uaddr, addr_len) // net/ipv4/tcp_ipv4.c
	// 一些路由等相关的其它处理
	...
	tcp_set_state(sk, TCP_SYN_SENT); /* 客户端套接字状态由 CLOSED 转为 SYN-SENT: CLOSED => SYN-SENT */
	...
	err = tcp_connect(sk); /* 发送 SYN 包 */
		struct tcp_sock *tp = tcp_sk(sk);
		struct sk_buff *buff;

		...
		/* 为 SYN 包分配空间 */
		buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
		...
		tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); /* 构建 SYN 包 */
		...
		/* 发送 SYN 包 */
		err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
				tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
	...
/*
 * c2. 客户端 套接字 向 服务端监听套接字 发送 SYN 请求,在 SYN-SENT 态睡眠等待 服务端回复 SYN+ACK 。
 */

// 接前面的 inet_stream_connect() 调用 tcp_v4_connect() 之后的流程
inet_stream_connect() // net/ipv4/af_inet.c
	...
	switch (sock->state) {
	// 发送 SYN 请求包到服务端
	case SS_UNCONNECTED:
		...
		err = sk->sk_prot->connect(sk, uaddr, addr_len); /* tcp_v4_connect() */
		sock->state = SS_CONNECTING; /* 套接字标记为 已连接状态 */
		...
		break;
	}

	...
	if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
		...
		/*
		 * 已向服务端发送 SYN 包,陷入睡眠等待服务端回复 SYN + ACK. 
		 * 在收到服务端的 SYN + ACK 后, 内核再回复 ACK 给服务端,
		 * 然后唤醒等待在此处的进程.
		 */
		if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
			goto out;
		...
	}
	...

	sock->state = SS_CONNECTED; /* 到此, socket 进入已连接状态 (SS_CONNECTED) */

// 睡眠等待服务端回送 SYN + ACK
inet_wait_for_connect() // net/ipv4/af_inet.c
	DEFINE_WAIT_FUNC(wait, woken_wake_function);

	// sk_sleep() 
	//		return &rcu_dereference_raw(sk->sk_wq)->wait;
	add_wait_queue(sk_sleep(sk), &wait);
	...
	while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
		...
		timeo = wait_woken(&wait, TASK_INTERRUPTIBLE, timeo); // 等待服务端回复: SYN + ACK
		...
	}
	...
3.2.1.5 服务端 收取 客户端 SYN,然后回复 客户端 SYN + ACK,进入 SYN-RECEIVED 态

上一小节说到 客户端套接字服务端监听套接字 发送 SYN 连接请求后,在 SYN-SENT 态睡眠等待 服务端监听套接字 回复 SYN + ACK 。接下来 服务端监听套接字 收取 客户端套接字 SYN 连接请求,为 SYN 连接请求 新建一个 记录客户端连接信息、初始为 SYN-RECEIVED 态 的 轻量套接字 struct request_sock(见后面代码注释 /* (2) */ 处),然后将其插入到 服务端监听套接字SYN 队列(半连接队列),最后回复 客户端套接字 SYN + ACK 的细节。注意,服务端监听套接字 仍维持在 LISTEN 态,进入 SYN-RECEIVED 态的是为新连接建立的轻量套接字 struct request_sock

/*
 * s1. 服务端监听套接字 收取 客户端 SYN,为 SYN 连接请求 建立轻量套接字,
 *     并将 轻量套接字 插入 服务端监听套接字 的 SYN 队列(半连接队列),
 *     最后 服务端监听套接字 回复 客户端 SYN + ACK.
 */

xxx_nic_interrput() // 从网卡数据接收中断入口开始
	napi_gro_receive()
		napi_skb_finish()
			netif_receive_skb_internal()
				__netif_receive_skb()
					__netif_receive_skb_core()
						pt_prev->func() = ip_rcv()
							ip_rcv_finish()
								dst_input()
									ip_local_deliver()
										ip_local_deliver_finish()
											// 见后续分析
											ipprot->handler() = tcp_v4_rcv()
	
// 接上面的分析
tcp_v4_rcv()
	...
	/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字: 服务端监听套接字 */
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
				th->dest, sdif, &refcounted);
	...
	if (sk->sk_state == TCP_LISTEN) { /* 服务端监听套接字 处于 LISTEN 态 */
		ret = tcp_v4_do_rcv(sk, skb); // 见后续分析
		goto put_and_return;
	}
	...

// 接上面的分析
tcp_v4_do_rcv(sk, skb)
	...
	if (tcp_rcv_state_process(sk, skb)) { // 见后续分析
		// 出错处理
		...
	}
	return 0;
	...

// 接上面的分析
tcp_rcv_state_process(sk, skb)
	...
	switch (sk->sk_state) {
	...
	case TCP_LISTEN:
		if (th->syn) { /* @skb 为 SYN 报文 */
			...
			// 见后续分析
			acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0; // tcp_v4_conn_request()
			...
			if (!acceptable) /* @sk 不接收 @skb, 回发给源头 RESET */
				return 1;
			/* @sk 正常接收 SYN @skb */
			consume_skb(skb);
			return 0;
		}
	...
	}
	
// 接上面的分析
acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0; // tcp_v4_conn_request()
	tcp_v4_conn_request(sk, skb) // net/ipv4/tcp_ipv4.c
		// 见后续分析
		return tcp_conn_request(&tcp_request_sock_ops,
					&tcp_request_sock_ipv4_ops, sk, skb);

// 接上面的分析
tcp_conn_request(&tcp_request_sock_ops, // net/ipv4/tcp_input.c
		&tcp_request_sock_ipv4_ops, sk, skb);
	...
	struct request_sock *req;
	...

	if (sk_acceptq_is_full(sk)) { // 套接字的 @sk accept 队列(全连接队列) 已满
		NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}
	
	...
	
	/*
	 * 为连接请求, 分配 轻量套接字 struct request_sock (指代 server_fd 套接字), 
	 * 将其先放到 服务端监听套机字 @sk 的 SYN 队列(半连接队列),然后在收到 客户端 
	 * 对 服务端 SYN (SYN+ACK) 的 ACK 回复后,再从 服务端监听套机字 @sk 的 SYN 队列(半连接队列)
	 * 移动到 accept 队列(全连接队列)。
	 */
	req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie); /* (2) */
		struct request_sock *req = reqsk_alloc(ops, sk_listener, attach_listener);
			req = kmem_cache_alloc(ops->slab, GFP_ATOMIC | __GFP_NOWARN);
				if (attach_listener) {
					...
					/* 
					 * 设定 记录 客户端连接信息 的 轻量套接字 所属的 服务端监听套接字, 
					 * 即 客户端 的 SYN 请求是向 服务端监听套接字 @sk_listener 发起的。
					 */
					req->rsk_listener = sk_listener;
				}
				req->rsk_ops = ops; /* req->rsk_ops = &tcp_request_sock_ops */
				req_to_sk(req)->sk_prot = sk_listener->sk_prot;
				...
		if (req) {
			struct inet_request_sock *ireq = inet_rsk(req);
			...
			// 标记 服务端 记录客户端连接信息的、轻量级套接字(struct request_sock) 为 SYN-RECEIVED 态:
			// 即 已经收到 客户端的 SYN 连接请求包。
   			// 注:TCP_NEW_SYN_RECV 是 高内核版本新引入的套接字状态,TCP_SYN_RECV 状态被 TFO 特性使用。
   			ireq->ireq_state = TCP_NEW_SYN_RECV;
   			...
   			ireq->ireq_family = sk_listener->sk_family;
		}
	tcp_rsk(req)->af_specific = af_ops; /* tcp_rsk(req)->af_specific = &tcp_request_sock_ipv4_ops */
	
	...

	af_ops->init_req(req, sk, skb); /* 设置轻量级套接字的 源、目的 IP: 同 @sk 的 源、目的 IP */
		tcp_v4_init_req()
	
	...

	if (fastopen_sk) { /* TCP Fast Open(TFO) 特性 */
		...
	}  else {
		tcp_rsk(req)->tfo_listener = false;
		if (!want_cookie)
			/* 添加 记录客户端连接信息的 轻量套接字 @req 到 服务端监听套接字 @sk 的 SYN 队列(半连接队列) */
			inet_csk_reqsk_queue_hash_add(sk, req, tcp_timeout_init((struct sock *)req));
				reqsk_queue_hash_req(req, timeout); // 将 轻量套接字 插入到 SYN 队列(半连接队列)
				inet_csk_reqsk_queue_added(sk);
					reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue);
						atomic_inc(&queue->young);
						atomic_inc(&queue->qlen); // SYN 队列(半连接队列) 长度 加 1
		/* 服务端监听套接字 收到 客户端 SYN 后,回送 SYN + ACK */
		af_ops->send_synack(sk, dst, &fl, req, &foc, // tcp_v4_send_synack()
           			!want_cookie ? TCP_SYNACK_NORMAL : TCP_SYNACK_COOKIE);
			...
			struct sk_buff *skb;

			skb = tcp_make_synack(sk, dst, req, foc, synack_type);
			if (skb) {
				...
				err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
							ireq->ir_rmt_addr,
							rcu_dereference(ireq->ireq_opt));
				...
			}
	}
	reqsk_put(req);
	return 0;
3.2.1.6 客户端 收取 服务端 SYN + ACK,回复 服务端 ACK,进入 ESTABLISHED 态

上一小节中,服务端监听套接字 完成了收取 客户端 SYN ,建立插入连接请求轻量套接字,并回复 客户端套接字SYN + ACK 的过程。接下来 客户端套接字 将收取 服务端SYN + ACK,并回复 服务端 的 SYNACK,进入 ESTABLISHED 态,于是完成了 客户端 => 服务端 单向连接 的建立。来看这一过程代码实现细节:

// 从网卡数据接收中断入口开始
xxx_nic_interrput()
	napi_gro_receive()
		napi_skb_finish()
			netif_receive_skb_internal()
				__netif_receive_skb()
					__netif_receive_skb_core()
						pt_prev->func() = ip_rcv()
							ip_rcv_finish()
								dst_input()
									ip_local_deliver()
										ip_local_deliver_finish()
											ipprot->handler() = tcp_v4_rcv()

tcp_v4_rcv() // net/ipv4/tcp_ipv4.c
	struct sock *sk;

	...
lookup:
	/* @sk: 客户端套接字 */
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
			       th->dest, sdif, &refcounted); 
	...
	ret = 0;
	if (!sock_owned_by_user(sk)) {
		ret = tcp_v4_do_rcv(sk, skb);
	}  else if (tcp_add_backlog(sk, skb)) {
		...
	}
	...

tcp_v4_do_rcv(sk, skb)
	...
	if (tcp_rcv_state_process(sk, skb)) {
		...
	}

tcp_rcv_state_process(sk, skb)
	...
	switch (sk->sk_state) {
	...
	case TCP_SYN_SENT: /* 已往服务端发送了 SYN 的客户端处于 TCP_SYN_SENT (connect()  调用) */
		...
		queued = tcp_rcv_synsent_state_process(sk, skb, th); /* 收取服务端发送的 SYN + ACK */
		...
		return 0;
	...
	}

tcp_rcv_synsent_state_process(sk, skb, th)
	...
	if (th->ack) { // ACK 包标记
		...
		if (!th->syn) // 不是 (SYN + ACK)
			goto discard_and_undo;
		...
		tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
		tcp_ack(sk, skb, FLAG_SLOWPATH); // 处理服务端回复包中的 ACK
		// 包序列号的一些处理
		tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
		tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;
		...
		// MTU, MSS 处理
		tcp_mtup_init(sk);
		tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
		tcp_initialize_rcv_mss(sk);
		...
		tcp_finish_connect(sk, skb); // 连接建立完成:套接字 @sk 由 SYN-SENT 转为 ESTABLISHED
			tcp_set_state(sk, TCP_ESTABLISHED);
				...
				sk_state_store(sk, state); // sk->state = TCP_ESTABLISHED
			...
		...
		if (!sock_flag(sk, SOCK_DEAD)) {
			/* 唤醒等待 server 端 SYN + ACK 的 connect() */
			sk->sk_state_change(sk) = sock_def_wakeup(sk)
				struct socket_wq *wq;
				...
				wq = rcu_dereference(sk->sk_wq);
				if (skwq_has_sleeper(wq))
					wake_up_interruptible_all(&wq->wait); // 唤醒 connect() 调用链中在 inet_wait_for_connect() 中等待连接建立完成的进程
				...
			sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
		}
		...
		if (sk->sk_write_pending ||
		    icsk->icsk_accept_queue.rskq_defer_accept ||
		    icsk->icsk_ack.pingpong) {
		    ...
		} else {
			tcp_send_ack(sk); /* 客户端 构建 + 发送 ACK 包给服务端 */
		}
	}
	...
3.2.1.7 服务端 收取 客户端 ACK,进入 ESTABLISHED 态,完成 三次握手

上一小节中,客户端 => 服务端 的单向连接 已经建立完成。接下来 服务端3.2.1.6 小节中代码注释 /* (2) */ 处、为 客户端 SYN 连接请求 建立的 轻量套接字 struct request_sock,将收取 客户端套接字服务端 SYN 连接请求 回复的 ACK,然后将 位于 服务端监听套接字 SYN 队列(半连接队列)连接请求轻量套接字 struct request_sock 取出,用 轻量套接字 的数据信息、在 服务端 为该 轻量套接字 所指代的 客户端连接 建立新的、初始状态为 SYN-RECEIVED 的、完整的 TCP 套接字对象(struct sock),然后添加该 TCP 套接字对象服务端监听套接字 的 accept 队列(全连接队列),同时将 TCP 套接字对象(struct sock) 的 状态更新为 ESTABLISHED,此时如果发现有在 accept() 中 睡眠等待 新连接就绪的 进程,则唤醒它们获取代表新连接的 TCP 套接字对象。进程调用 accept() 进入睡眠的过程参考 3.2.1.3 小节的分析。同时,来看这一过程代码实现细节:

/*
 * s2. 服务端 收取 客户端 ACK,将位于 服务端监听套接字 SYN 队列(半连接队列)、代表 客户端连接 的
 *     轻量套接字,用 轻量套接字 的数据信息,为 客户端连接 建立完成 TCP 套接字 struct sock,并
 *     添加到 服务端监听套接字 的 accept 队列(全连接队列)。此时如果发现有在 accept() 中 睡眠等待 
 *     新连接就绪的 进程,则唤醒它们获取 代表新连接 TCP 套接字对象。
 */

xxx_nic_interrput() // 从网卡数据接收中断入口开始
	...
	tcp_v4_rcv()
		...
	lookup:
		// 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字。
		// @sk: 3.2.1.6 小节中代码注释 /* (2) */ 处 建立的 轻量套接字 struct request_sock
		sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
						th->dest, sdif, &refcounted);
		...
	process:
		...
		/*
		 * 已经收到了客户端 SYN 并回应了 SYN + ACK 的 服务端 客户端连接轻量级套接字: 
	 	 * . 服务端 客户端连接 轻量套接字 @sk / @req 处于 TCP_NEW_SYN_RECV 状态
	 	 * . 服务端 客户端连接 轻量套接字 @sk / @req 关联 的 服务端监听套接字 
	 	 *   @req->rsk_listener 处于 TCP_LISTEN 状态,即 @req 指代的客户端连接
	 	 *   是 向 服务端监听套接字 @req->rsk_listener 发起。
	 	 */
		if (sk->sk_state == TCP_NEW_SYN_RECV) {
			struct request_sock *req = inet_reqsk(sk); // 服务端轻量级"代理"套接字
			struct sock *nsk;

			sk = req->rsk_listener;
			...
			if (unlikely(sk->sk_state != TCP_LISTEN)) {
				inet_csk_reqsk_queue_drop_and_put(sk, req);
				goto lookup;
			}
			...
			if (!tcp_filter(sk, skb)) { /* 如果数据包没有被 eBPF 过滤掉 */
				...
				/*
				 * 通过 轻量套接字 @req 的信息,为 客户端新连接 建立 服务端的完整套接字: 
				 * 建立 TCP 的套接字数据 (struct sock *nsk),并将  添加 到 服务端监听套接字 
				 * @req->rsk_listener 的 accept 队列 (全连接队列)。
				 */
				nsk = tcp_check_req(sk, skb, req, false); // 见后续 s2.1 分析
			}
			...
			if (nsk == sk) {
				...
			}
			// 新连接初始化(MTU、缓冲等),然后唤醒在 accept() 调用中阻塞等待新连接的进程
			// 见后续 s2.2 分析
			else if (tcp_child_process(sk, nsk, skb)) {
				...
			}  else {
				sock_put(sk);
				return 0;
			}
			...
		}

// s2.1 
// 为 客户端新连接 建立完整的 TCP 套接字(struct sock),并将其添加到 accept 队列(全连接队列)。
nsk = tcp_check_req(sk, skb, req, false); // net/ipv4/tcp_minisocks.c
	child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req);
		tcp_v4_syn_recv_sock() // net/ipv4/tcp_ipv4.c
			struct sock *newsk;
			...
			newsk = tcp_create_openreq_child(sk, req, skb);
				struct sock *newsk = inet_csk_clone_lock(sk, req, GFP_ATOMIC);
					struct sock *newsk = sk_clone_lock(sk, priority);
					if (newsk) {
						...
						newsk->sk_state = TCP_SYN_RECV;
						...
					}
				...
			...
	...
	/* 
	 * . 将 客户端新连接 轻量套接字 从 服务端监听套接字 的 SYN 队列(半连接队列) 移除,
	 * . 将 新建的 客户端新连接 TCP 套接字(struct sock) 添加到 服务端监听套接字 的 accept 队列(全连接队列) 
	 */
	return inet_csk_complete_hashdance(sk, child, req, own_req);
		if (own_req) {
			// 将 客户端新连接 轻量套接字 从 服务端监听套接字 的 SYN 队列(半连接队列) 移除
			inet_csk_reqsk_queue_drop(sk, req); // 从 ehash 表删除
			reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);
				if (req->num_timeout == 0)
					atomic_dec(&queue->young);
				atomic_dec(&queue->qlen); // SYN 队列(半连接队列) 长度 减 1
			// 将 新建的 客户端新连接 TCP 套接字(struct sock),
			// 添加到 服务端监听套接字 @sk 的 accept 队列(全连接队列).
			if (inet_csk_reqsk_queue_add(sk, req, child))
				return child;
		}
		...

// 接 s2.1 分析:
// 将 新建的 客户端新连接 TCP 套接字(struct sock), 
// 添加到 服务端监听套接字 @sk 的 accept 队列(全连接队列).
inet_csk_reqsk_queue_add(sk, req, child)
	struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;

	...
	if (unlikely(sk->sk_state != TCP_LISTEN)) {
		...
	} else {
		req->sk = child;
		req->dl_next = NULL;
		if (queue->rskq_accept_head == NULL)
			queue->rskq_accept_head = req;
		else
			queue->rskq_accept_tail->dl_next = req;
		queue->rskq_accept_tail = req;
		sk_acceptq_added(sk);
			sk->sk_ack_backlog++; /* accept 队列(全连接队列) 长度 加 1 */
	}
	...

// s2.2 新连接初始化(MTU、缓冲等),然后唤醒在 accept() 调用中阻塞等待新连接的进程
tcp_child_process(sk, nsk, skb)
	int state = child->sk_state;

	...
	if (!sock_owned_by_user(child)) {
		ret = tcp_rcv_state_process(child, skb);
			...
			switch (sk->sk_state) {
			case TCP_SYN_RECV:
				...
				tcp_set_state(sk, TCP_ESTABLISHED); /* 与客户端的通信的套接字装换为已连接状态 ESTABLISHED */
				...
				tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
				...
				tcp_initialize_rcv_mss(sk);
				...
				break;
			}
			...
		/* Wakeup parent, send SIGIO */
		if (state == TCP_SYN_RECV && child->sk_state != state)
			/* 唤醒在 accept() 中等待连接的进程 */
			parent->sk_data_ready(parent) = sock_def_readable()
				...
				wq = rcu_dereference(sk->sk_wq);
				if (skwq_has_sleeper(wq))
					wake_up_interruptible_sync_poll(&wq->wait, POLLIN | POLLPRI |
													POLLRDNORM | POLLRDBAND);
					sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
				...
	} else {
		...
	}
	...
	return 0;

到此,3.2 小节中图示的 TCP 连接建立的 三次握手 过程 已经全部分析完成。当然,这里分析的只是 TCP 连接建立的几种可能序列当中的一个,对其它的建立过程序列感兴趣的读者可以自行分析源码。另外,对于 SYN Cookies ,TFO(TCP Fast Open),端口重用 等特性,本文也未加讨论。

3.2.1.8 TCP 三次握手 过程小结

调用 listen() 后,服务端 监听套接字(即前面代码中的 server_fd 指代的套接字) 处于 LISTEN 状态,客户端套接字 处于 CLOSED 状态。客户端通过 connect() 调用,向 服务端 监听套接字 发送 SYN 请求,然后自身进入到 SYN-SENT 状态等待服务端对 SYN 请求SYN + ACK 回复,客户端在收到服务端的 SYN + ACK 回复后,也对服务端的 SYN 回复一个 ACK,之后自身进入到 ESTABLISHED 状态,并从 connect() 调用返回;而处于 LISTEN 状态的 服务端 监听套接字 收到客户端的 SYN 请求后,先是新建一个记录客户端连接信息的、初始为 SYN-RECEIVED 状态 的 轻量连接请求套接字(struct request_sock) ,接着将该轻量套接字添加到 服务端 监听套接字SYN 队列(半连接队列),回复 SYN + ACK 给客户端,然后服务端等待客户端回复 ACK,当客户端的 ACK 到来后,将前面新建的、记录客户端连接信息的 轻量套接字(struct request_sock),从服务端 监听套接字SYN 队列(半连接队列) 移出,并以该 轻量套接字 为模板,为客户端连接建立一个完成 TCP 套接字(struct sock),并将其添加到 服务端监听套接字 的 accept 队列(全连接队列),并唤醒可能阻塞在 accept() 等待连接到来的进程。细心的读者注意到了吧,服务端 监听套接字 server_fd 的状态不会发生变化,仍然处于 LISTEN 状态,这从 3.1,3.2 小节中的状态转换图是看不出来的,而且还会误解是监听套接字 server_fd 的状态发生了变化。服务端 真正变为 ESTABLISHED 状态的套接字是 accept() 调用中注释 /* (1) */ 处创建的 newsock,这个后面再做分析。
TCP 连接的建立是一个双向的过程,不管是服务端还是客户端,都会向对端通过 SYN 发起连接请求,而对方也会在收到 SYN 后回应一个 ACK ,这样一个双向连接就建立好了。以这样的方式建立连接的基本理由,是因为 TCP 是全双工通信。从后面的章节也可以看到,拆除连接(四次挥手) 也是一个双向拆除的过程。
另外,连接建立的过程,是由内核协议栈完成的。细心的童鞋会发现,在调用 listen() 后,即使服务端不调用 accept() ,客户端照样可以通过 connect() 建立连接,只是这样的连接,无法正常和服务端进行数据通信。这是因为 listen() 已经建立好了 accept 队列当客户端发起连接时,内核协议栈会把建立好的连接信息放入 accept 队列;而 accept() 只是从该队列中取出建立好的连接信息,它本身并不参与连接的建立过程。用 netstat 观察,可以发现这些 TCP 连接没有进程名信息,取而代之的是 -- 。这样的现象,可能会让初接触 TCP 编程的童鞋大吃一惊。

3.2.2 TCP 连接断开的四次挥手过程分析

当我们的通信完毕,可以通过调用 close() 关闭连接。和连接的建立一样,连接的关闭过程也存在多种可能的序列,本文以 3.2 小节中的连接关闭过程为例来进行分析。

3.2.2.1 客户端通过 close() 向服务端发送 FIN,进入 FIN-WAIT-1 状态
// 客户端发起本端的连接断开请求
close(remote_fd);

sys_close() // fs/open.c
	__close_fd(current->files, fd)
		filp_close(file, files)
			// 中间过程有点小复杂,不是这里关注的重点
			...
			sock_close()

sock_close() // net/socket.c
	__sock_release(SOCKET_I(inode), inode)
		if (sock->ops) {
			...
			sock->ops->release(sock) = inet_release() // net/ipv4/af_inet.c
				struct sock *sk = sock->sk;
				if (sk) {
					...
					sk->sk_prot->close(sk, timeout) = tcp_close()
				}
				return 0;
			...
		}

tcp_close(sk, timeout) // net/ipv4/tcp.c
	...
	sk->sk_shutdown = SHUTDOWN_MASK;
	...
	
	/*
	 * 关闭连接时,需处理接收缓冲里还没有被应用层读取的数据,
	 * 我们假定关闭连接时,客户端应用已经拿走了接收缓冲里的
	 * 所有数据。
	 * 对于客户端套接字缓冲还有未读取数据的情形,读者可自行分析。
	 */
	...
	
	sk_mem_reclaim(sk);

	if (unlikely(tcp_sk(sk)->repair)) {
		...
	}  else if (data_was_unread) {
		...
	}  else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
		...
	} else if (tcp_close_state(sk)) { // 套接字由 ESTABLISHED 态转为 FIN-WAIT-1
		tcp_send_fin(sk); // 向服务端发送 FIN 包
	}
	
	...
3.2.2.2 服务端收取客户端 FIN,回以 ACK,进入 CLOSE-WAIT 状态

客户端发送 FIN 包给服务端,服务端收到客户端的 FIN 包后,回应以一个 ACK

xxx_nic_interrput()
	...
	tcp_v4_rcv()
		...
	lookup:
		/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */
		sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
			       th->dest, sdif, &refcounted);
	...
	process:
		...
		if (!sock_owned_by_user(sk)) {
			ret = tcp_v4_do_rcv(sk, skb);
				...
				if (sk->sk_state == TCP_ESTABLISHED) {
					...
					tcp_rcv_established(sk, skb, tcp_hdr(skb)); // net/ipv4/tcp_input.c
						...
						tcp_send_ack(sk); // 回应 FIN 一个 ACK
						...
						tcp_data_queue(sk, skb);
							if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
								...
								// 服务端收到客户端的 FIN ,除了回一个 ACK 外,
								// 还要针对 FIN 包做一些和客户端连接的套接字的特定处理:
								// 状态由 ESTABLISHED 转为 CLOSE-WAIT 等等...
								if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
									tcp_fin(sk);
										sk->sk_shutdown |= RCV_SHUTDOWN;
										sock_set_flag(sk, SOCK_DONE);
										switch (sk->sk_state) {
										case TCP_SYN_RECV:
										case TCP_ESTABLISHED:
											// ESTABLISHED => CLOSE-WAIT
											tcp_set_state(sk, TCP_CLOSE_WAIT);
											inet_csk(sk)->icsk_ack.pingpong = 1;
											break;
										...
										}
										...
								...
							}
						...
					return 0;
				}
		} else if (...) {
			...
		}
3.2.2.3 客户端收取服务端对 FIN 的回应 ACK,进入 FIN-WAIT-2 状态

客户端在调用 close() 向服务端发送 FIN 包后,当前处于 FIN-WAIT-1 状态(如果没有设置SOCK_LINGERclose() 调用也已经返回);服务端对客户端的 FIN 包回应了一个 ACK,自身进入 CLOSE-WAIT 状态;客户端收到这个 ACK 后,进入 FIN-WAIT-2 状态,这时从服务端向客户端发送数据的通道就已经关闭了。看客户端处理 FINACK 包后,进入 FIN-WAIT-2 状态这一过程的代码实现细节:

xxx_nic_interrput()
	...
	tcp_v4_rcv()
		...
	lookup:
		/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */
		sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
			       th->dest, sdif, &refcounted);
		...
	process:
		...
		if (!sock_owned_by_user(sk)) {
			ret = tcp_v4_do_rcv(sk, skb);
				...
				if (tcp_rcv_state_process(sk, skb)) {
					rsk = sk;
					goto reset;
				}
				return 0;
				...
		} else if (tcp_add_backlog(sk, skb)) {
			...
		}
	...

tcp_rcv_state_process(sk, skb)
	...
	switch (sk->sk_state) {
	...
	case TCP_FIN_WAIT1: {
		...
		tcp_set_state(sk, TCP_FIN_WAIT2); /* 客户端: FIN-WAIT-1 => FIN-WAIT-2 */
		sk->sk_shutdown |= SEND_SHUTDOWN;
		...
	}
	...
	}
	...
3.2.2.4 服务端通过 close() 向客户端发 FIN,进入 LAST-ACK 状态

此时,TCP 连接已经处于半关闭状态(客户端接收数据通道已经关闭)。同样的,在服务端不再想接收客户端的数据时,调用 close() 向客户端发送 FIN 包,然后服务端套接字由 CLOSE-WAIT 进入 LAST-ACK 状态:

// 关闭服务端和客户端通信的套接字,注意,这里关闭的不是 server_fd
close(client_fd);

sys_close() // fs/open.c
	...
	sock_close() // net/socket.c
		__sock_release(SOCKET_I(inode), inode)
		if (sock->ops) {
			...
			sock->ops->release(sock) = inet_release() // net/ipv4/af_inet.c
				struct sock *sk = sock->sk;
				if (sk) {
					...
					sk->sk_prot->close(sk, timeout) = tcp_close()
				}
				return 0;
			...
		}

tcp_close(sk, timeout) // net/ipv4/tcp.c
	...
	sk->sk_shutdown = SHUTDOWN_MASK;
	...
	
	sk_mem_reclaim(sk);
	
	...
	if (unlikely(tcp_sk(sk)->repair)) {
		...
	}  else if (data_was_unread) {
		...
	}  else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
		...
	} else if (tcp_close_state(sk)) { // 套接字由 CLOSE-WAIT 态转为 LAST-ACK
		tcp_send_fin(sk); // 向客户端发送 FIN 包
	}
	...
3.2.2.5 客户端收取服务端 FIN,回以 ACK,进入 TIME-WAIT,超时后进入 CLOSED 终态

客户端当前处于 FIN-WAIT-2 状态,收到服务端的 FIN 包后,回复服务端一个 ACK,然后自身进入 TIME-WAIT 状态:

xxx_nic_interrput()
	...
	tcp_v4_rcv()
		...
	lookup:
		/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */
		sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
			       th->dest, sdif, &refcounted);
		...
	process:
		...
		if (!sock_owned_by_user(sk)) {
			ret = tcp_v4_do_rcv(sk, skb);
				...
				if (tcp_rcv_state_process(sk, skb)) {
					rsk = sk;
					goto reset;
				}
				return 0;
				...
		} else if (tcp_add_backlog(sk, skb)) {
			...
		}
	...

tcp_rcv_state_process(sk, skb)
	...
	/* step 7: process the segment text */
	switch (sk->sk_state) {
	...
	case TCP_FIN_WAIT1:
	case TCP_FIN_WAIT2: // 客户端当前处于 FIN-WAIT-2 状态
		...
		/* Fall through */
	case TCP_ESTABLISHED:
		tcp_data_queue(sk, skb); // 处理服务端发送的 FIN , 并进入 TIME-WAIT 状态
		queued = 1;
		break;
	}
	...

// 处理服务端发送的 FIN , 并进入 TIME-WAIT 状态
tcp_data_queue(sk, skb)
	...
	if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
		...
		if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
			tcp_fin(sk);
				switch (sk->sk_state) {
				...
				case TCP_FIN_WAIT2:
					/* Received a FIN -- send ACK and enter TIME_WAIT. */
					tcp_send_ack(sk); // 回复给服务端 ACK
					tcp_time_wait(sk, TCP_TIME_WAIT, 0); // 进入 TIME-WAIT 态: FIN-WAIT-2 => TIME-WAIT
					break;
				...
				}
		...
	}
	...

// 进入 TIME-WAIT 态: FIN-WAIT-2 => TIME-WAIT
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
	...
	struct inet_timewait_sock *tw;
	...

	/* 分配 TIME-WAIT 套接字数据,初始化包括 TIME-WAIT 超时定时器 等 */
	tw = inet_twsk_alloc(sk, tcp_death_row, state);
		...
		tw = kmem_cache_alloc(sk->sk_prot_creator->twsk_prot->twsk_slab, GFP_ATOMIC);
		...
		if (tw) {
			...
			tw->tw_state	    = TCP_TIME_WAIT;
			tw->tw_substate	    = state; // tw->tw_substate = TCP_TIME_WAIT
			...
			// 初始化 TIME-WAIT 超时定时器
			setup_pinned_timer(&tw->tw_timer, tw_timer_handler, (unsigned long)tw);
			...
		}
	if (tw) {
		...
		// TIME-WATI 超时时间设置
		tw->tw_timeout = TCP_TIMEWAIT_LEN;
		if (state == TCP_TIME_WAIT)
			timeo = TCP_TIMEWAIT_LEN;
		...
		inet_twsk_schedule(tw, timeo); /* 启动 TIME-WAIT 超时定时器 */
			__inet_twsk_schedule(tw, timeo, false);
				tw->tw_kill = timeo <= 4*HZ;
				if (!rearm) {
					BUG_ON(mod_timer(&tw->tw_timer, jiffies + timeo)); // 启动 TIME-WAIT 超时定时器
					atomic_inc(&tw->tw_dr->tw_count);
				} else {
					...
				}
		...
	}

	...
	tcp_done(sk);
		...
		tcp_set_state(sk, TCP_CLOSE);
		...

// TIME-WAIT 定时器超时,触发 tw_timer_handler(),
// 回收 TIME-WAIT (struct inet_timewait_sock) 套接字资源
tw_timer_handler()
	struct inet_timewait_sock *tw = (struct inet_timewait_sock *)data;

	...
	inet_twsk_kill(tw); // 回收 TIME-WAIT (struct inet_timewait_sock) 套接字资源
3.2.2.6 服务端收取客户端对 FIN 的回应 ACK,进入 CLOSED 终态

上面分析中,客户端收到服务端的 FIN 后,回复服务端一个 ACK,然后自身进入由 FIN-WAIT-2 进入到 TIME-WAIT ,并在超时时间到达后,进入终态 CLOSED 。服务端收到客户端对 FINACK 后,也由 LAST-ACK 转入终态 CLOSED ,到此,整个连接的双向关闭过程终结了。下面看服务端收到客户端对 FINACK,由 LAST-ACK 转入终态 CLOSED 的代码细节:

xxx_nic_interrput()
	...
	tcp_v4_rcv()
		...
	lookup:
		/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */
		sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
			       th->dest, sdif, &refcounted);
		...
	process:
		...
		if (!sock_owned_by_user(sk)) {
			ret = tcp_v4_do_rcv(sk, skb);
				...
				if (tcp_rcv_state_process(sk, skb)) {
					rsk = sk;
					goto reset;
				}
				return 0;
				...
		} else if (tcp_add_backlog(sk, skb)) {
			...
		}
	...

// 服务端: LAST-ACK => CLOSED
tcp_rcv_state_process(sk, skb)
	...
	switch (sk->sk_state) {
	...
	case TCP_LAST_ACK:
		if (tp->snd_una == tp->write_seq) {
			tcp_update_metrics(sk);
			tcp_done(sk);
				tcp_set_state(sk, TCP_CLOSE); // 服务端: LAST-ACK => CLOSED
				tcp_clear_xmit_timers(sk);
				...
			goto discard;
		}
		break;
	...
	}
	...

到此,TCP 连接断开的四次挥手过程已经分析完毕。细心的读者可能会发现,示例代码中的 server_fd 还没有关闭。是的,但本文不打算对此进行分析,相信有了前面的基础,读者自行分析有不会是多困难的事情。

4. 抓包三握四挥过程示例

可通过工具 tcpdump 观察 TCP 三握四挥 的过程,请参考博文 Linux: tcpdump抓包示例
当然,Wireshark 可能是更好的选择。

5. 参考资料

rfc793: https://www.rfc-editor.org/rfc/rfc793.html
https://mp.weixin.qq.com/s/tCXH8BTrgYaVmwVx_Ek1qA

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值