TCP数据发送之发送窗口

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

1. 概述

TCP的发送窗口可以用下图表示:
TCP数据发送之发送窗口_第1张图片

如图所示,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 传输过程中更新

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

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;
	u32 nwin = ntohs(tcp_hdr(skb)->window);

	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;
		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);
			//如果通告的最大接收窗口发生变化,更新max_window
			if (nwin > tp->max_window) {
				tp->max_window = nwin;
				tcp_sync_mss(sk, inet_csk(sk)->icsk_pmtu_cookie);
			}
		}
	}
	//用ack更新snd_una
	tp->snd_una = ack;

	return flag;
}

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

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

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

你可能感兴趣的:(linux网络)