linux内核协议栈 TCP数据发送之发送窗口

目录

1 发送窗口概述

2 snd_una 和 snd_wnd 的更新

2.1 发送窗口初始化

2.1.1 客户端初始化

2.1.2 服务器端初始化

2.2 本地接收窗口 rcv_wnd 通告

2.2.1 客户端发送

2.2.2 服务器发送

2.3 传输过程中更新发送窗口

2.3.1 发送窗口更新条件

3 发送窗口对发送过程的影响


TCP的发送过程由滑动窗口控制,而滑动窗口的大小受限于发送窗口和拥塞窗口,拥塞窗口由拥塞控制算法的代表,而发送窗口是流量控制算法的代表,这篇笔记记录了发送窗口相关的内容,包括发送窗口的初始化、更新、以及它是如何影响数据发送过程的。

1 发送窗口概述

TCP的发送窗口可以用下图表示:
在这里插入图片描述

如图所示,TCB中有三个成员和发送窗口强相关。

struct tcp_sock {
...
	//下一个要发送的序号,即序号等于snd_nxt的数据还没有发送
	u32	snd_nxt;	/* Next sequence we send		*/
	//已经发送,但是还没有被确认的最小序号,注意序号等于snd_una的数据已经发送,
	//最想收到的确认号要大于snd_una。但是有一个特殊情况,如果发送的所有数据都
	//已经被确认,那么snd_una将等于下一个要发送的数据,即snd_una代表的数据还
	//没有发送,见下面tcp_ack()更新snd_una就可以理解这一点了
	u32	snd_una;	/* First byte we want an ack for	*/
	//发送窗口大小,以字节为单位,来源于输入段首部的窗口字段,即对端接收缓冲区的剩余大小
	u32	snd_wnd;	/* The window we expect to receive	*/
	//记录到目前为止对端通告过的窗口的最大值,可以代表对端接收缓冲区的最大值
	u32	max_window;	/* Maximal window ever seen from peer	*/
	//写系统调用一旦成功返回,说明数据一被TCP协议接收,这时就要为每一个数据分配一个序号,
	//write_seq就是下一个要分配的序号,其初始值由secure_tcp_sequence_number()基于
	//算法生成。注意等于write_seq的序号还没有被分配
	u32	write_seq;	/* Tail(+1) of data held in tcp send buffer */
...
};

注意:发送窗口的大小描述的是对端接受缓冲区的大小,即对端的的接受窗口的大小

2 snd_una 和 snd_wnd 的更新

snd_una是发送窗口的左边界,如果该字段更新,即使发送窗口大小snd_wnd没有发生变化,整个发送窗口也会前移,这样从流量控制的角度,就可以发送更多的数据(是否真的可以发送,还要考虑拥塞窗口等其它因素)。

2.1 发送窗口初始化

可以想的到,snd_una的初始化一定发生在第一个数据段发送过程中,而snd_wnd的初始化应该是发生在第一个输入段处理过程中,所以需要客户端和服务器端分开来看。

2.1.1 客户端初始化

客户端对snd_una的初始化当然是发生在SYN段的发送过程中,相关代码如下:

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
...
	//选择初始发送序号
	if (!tp->write_seq)
		tp->write_seq = secure_tcp_sequence_number(inet->saddr,
							   inet->daddr,
							   inet->sport,
							   usin->sin_port);
...
}
static void tcp_connect_init(struct sock *sk)
{
...
	//发送窗口大小要从输入段首部的窗口字段获取,这时还没有任何输入段,先初始化为0
	tp->snd_wnd = 0;
	//初始化snd_una为第一个序号,该函数之后write_seq将会分配给SYN段
	tp->snd_una = tp->write_seq;
...
}

对snd_wnd的初始化发生在收到SYN+ACK段时,相关代码如下:

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
					 struct tcphdr *th, unsigned len)
{
...
	if (th->ack) {
...
		tp->snd_wnd = ntohs(th->window);
...
	}
}

2.1.2 服务器端初始化

正面理解的话,服务器端对snd_una的初始化应该是发生在发送SYN+ACK段时,但是实际上不是,而是发生在收到第三次握手的ACK段时。如笔记TCP之服务器端收到ACK包所述,三次握手完成后,创建了子套接字,然后在tcp_child_process()中会继续调用tcp_rcv_state_process()处理ACK报文,代码如下:

int tcp_child_process(struct sock *parent, struct sock *child,
		      struct sk_buff *skb)
{
	int ret = 0;
	int state = child->sk_state;

	//如果用户进程没有锁住child,则让child重新处理该ACK报文,这可以让child
	//套接字由TCP_SYN_RECV迁移到TCP_ESTABLISH状态
	if (!sock_owned_by_user(child)) {
		//见下文
		ret = tcp_rcv_state_process(child, skb, tcp_hdr(skb),
					    skb->len);
		/* Wakeup parent, send SIGIO */
		//child套接字状态发生了迁移,唤醒监听套接字上的进程,可能由于调用accept()而block
		if (state == TCP_SYN_RECV && child->sk_state != state)
			parent->sk_data_ready(parent, 0);
	} else {
		/* Alas, it is possible again, because we do lookup
		 * in main socket hash table and lock on listening
		 * socket does not protect us more.
		 */
		 //缓存该skb后续处理
		sk_add_backlog(child, skb);
	}

	bh_unlock_sock(child);
	sock_put(child);
	return ret;
}

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
			  struct tcphdr *th, unsigned len)
{
...
	/* step 5: check the ACK field */
	if (th->ack) {
		int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH);

		switch (sk->sk_state) {
		case TCP_SYN_RECV:
			if (acceptable) {
...
				tcp_set_state(sk, TCP_ESTABLISHED);
				//用ACK段中的确认号初始化本端的snd_una
				tp->snd_una = TCP_SKB_CB(skb)->ack_seq;
				//用输入报文的窗口字段初始化发送窗口大小
				tp->snd_wnd = ntohs(th->window) <<
					      tp->rx_opt.snd_wscale;
...
			}
			break;
...
		}//end of switch()
	} else
		goto discard;
...
	return 0;
}

2.2 本地接收窗口 rcv_wnd 通告

上述的初始化过程已经表述客户端和服务器的本地发送窗口snd_wnd 均是在就收到对端的报文解析tcp的滑动窗口字段:ntohs(th->window)而赋值的。那么window是如何发送的了?

2.2.1 客户端发送

tcp_connect
	--tcp_connect_init
		--tcp_select_initial_window //根据本地的接受缓冲区,mtu计算得出本地的接受窗口
	--tcp_transmit_skb //发送window
	

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
			    gfp_t gfp_mask)
{
	...
	/* Build TCP header and checksum it. */
	th = tcp_hdr(skb);
	th->source		= inet->sport;
	th->dest		= inet->dport;
	th->seq			= htonl(tcb->seq);
	th->ack_seq		= htonl(tp->rcv_nxt);
	*(((__be16 *)th) + 6)	= htons(((tcp_header_size >> 2) << 12) |
					tcb->flags);

	if (unlikely(tcb->flags & TCPCB_FLAG_SYN)) {
		/* RFC1323: The window in SYN & SYN/ACK segments
		 * is never scaled.
		 */
		th->window	= htons(min(tp->rcv_wnd, 65535U));
	} else {
		th->window	= htons(tcp_select_window(sk));
	}
	...
}

2.2.2 服务器发送

tcp_v4_send_synack
	--__tcp_v4_send_synack
		--tcp_make_synack
			--tcp_select_initial_window
		--ip_build_and_send_pkt	

struct sk_buff *tcp_make_synack(struct sock *sk, struct dst_entry *dst,
				struct request_sock *req)
{
	struct inet_request_sock *ireq = inet_rsk(req);
	struct tcp_sock *tp = tcp_sk(sk);
	struct tcphdr *th;
	int tcp_header_size;
	struct tcp_out_options opts;
	struct sk_buff *skb;
	struct tcp_md5sig_key *md5;
	__u8 *md5_hash_location;
	int mss;

	skb = sock_wmalloc(sk, MAX_TCP_HEADER + 15, 1, GFP_ATOMIC);
	...

	if (req->rcv_wnd == 0) { /* ignored for retransmitted syns */
		__u8 rcv_wscale;
		/* Set this up on the first call only */
		req->window_clamp = tp->window_clamp ? : dst_metric(dst, RTAX_WINDOW);
		/* tcp_full_space because it is guaranteed to be the first packet */
		tcp_select_initial_window(tcp_full_space(sk),
			mss - (ireq->tstamp_ok ? TCPOLEN_TSTAMP_ALIGNED : 0),
			&req->rcv_wnd,
			&req->window_clamp,
			ireq->wscale_ok,
			&rcv_wscale);
		ireq->rcv_wscale = rcv_wscale;
	}
	...
	th = tcp_hdr(skb);
	memset(th, 0, sizeof(struct tcphdr));
	th->syn = 1;
	th->ack = 1;
	...

	/* RFC1323: The window in SYN & SYN/ACK segments is never scaled. */
	th->window = htons(min(req->rcv_wnd, 65535U));
	...
	return skb;
}

2.3 传输过程中更新发送窗口

显然,数据传输过程中,应该在收到ACK后更新snd_una和snd_wnd。如果输入段中携带了ACK,最终都会有tcp_ack()处理确认相关的内容。

快速路径处理情况,因为此时在接收数据,所以输入段的windows字段一定是没有发生变化的,所以无需更新snd_wnd的值,直接更新snd_una即可。

慢速路径处理情况,情况复杂,需要做更多的判断,调用 tcp_ack_update_window() 完成发送窗口的更新。

static int tcp_ack(struct sock *sk, struct sk_buff *skb, int flag)
{
...
	u32 prior_snd_una = tp->snd_una;
	u32 ack = TCP_SKB_CB(skb)->ack_seq;
...
	if (!(flag & FLAG_SLOWPATH) && after(ack, prior_snd_una)) {
...
		//快速路径情况,用ack更新snd_una,由于快速路径,所以通告的窗口大小一定
		//没有发生变化,所以不需要更新snd_wnd
		tp->snd_una = ack;
		flag |= FLAG_WIN_UPDATE;
...
	} else {
...
		//慢速路径下,调用函数更新窗口
		flag |= tcp_ack_update_window(sk, skb, ack, ack_seq);
...
	}
...
}

/* Update our send window.
 *
 * Window update algorithm, described in RFC793/RFC1122 (used in linux-2.2
 * and in FreeBSD. NetBSD's one is even worse.) is wrong.
 */
static int tcp_ack_update_window(struct sock *sk, struct sk_buff *skb, u32 ack,
				 u32 ack_seq)
{
	struct tcp_sock *tp = tcp_sk(sk);
	int flag = 0;
	//ACK段中携带的通告窗口
	u32 nwin = ntohs(tcp_hdr(skb)->window);

	//协议规定,SYN和SYN+ACK段中是不可以携带窗口扩大因子的,所以这里
	//判断不带SYN标记位时是否需要根据窗口扩大因子调整通告的新窗口大小
	if (likely(!tcp_hdr(skb)->syn))
		nwin <<= tp->rx_opt.snd_wscale;

	if (tcp_may_update_window(tp, ack, ack_seq, nwin)) {
		//需要更新窗口
		flag |= FLAG_WIN_UPDATE;
		//更新snd_wl
		tcp_update_wl(tp, ack, ack_seq);

		if (tp->snd_wnd != nwin) {
			//更新发送窗口
			tp->snd_wnd = nwin;

			/* Note, it is the only place, where
			 * fast path is recovered for sending TCP.
			 */
			//更新了发送窗口大小,需要重新判断是否设置首部预测标记
			tp->pred_flags = 0;
			tcp_fast_path_check(sk);
			//更新已知最大通告窗口
			if (nwin > tp->max_window) {
				tp->max_window = nwin;
				//因为MSS和max_window相关,所以max_window发生了变化,需要重新计算MSS
				tcp_sync_mss(sk, inet_csk(sk)->icsk_pmtu_cookie);
			}
		}
	}
	//更新发送窗口左边界
	tp->snd_una = ack;
	return flag;
}

2.3.1 发送窗口更新条件

慢速路径下的核心是判断什么时候应该更新发送窗口,这是由tcp_may_update_window()实现的。

/* Check that window update is acceptable.
 * The function assumes that snd_una<=ack<=snd_next.
 */
static inline int tcp_may_update_window(const struct tcp_sock *tp,
					const u32 ack, const u32 ack_seq,
					const u32 nwin)
{
	//cond1: 确认号大于snd_una,说明确认了新数据,可以更新发送窗口左边界;
	//cond2: ACK段的序号大于snd_wl1,说明对方有发送新数据,所以需要更新snd_wl1;
	//cond3: 通告的接收窗口有变化.
	//上面只有有一个条件成立,那么就可以更新发送窗口了(条件2着实没理解...)。
	return (after(ack, tp->snd_una) ||
		after(ack_seq, tp->snd_wl1) ||
		(ack_seq == tp->snd_wl1 && nwin > tp->snd_wnd));
}

3 发送窗口对发送过程的影响

这里要明白的是,发送窗口是实现流量控制的关键,它影响的只有新数据的发送过程,与重传无关,因为重传的数据一定是在对端接收能力之内。

《linux内核协议栈 TCP层数据发送之发送新数》中有看到新数据发送的两个关键函数tcp_write_xmit()和tcp_push_one(),而且二者非常相似,参考之前的笔记中分析的tcp_snd_wnd_test()和tcp_mss_split_point()就可以明白发送窗口是如何影响发送过程的。

猜你喜欢

转载自blog.csdn.net/wangquan1992/article/details/109030547