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 */
...
};
snd_una是发送窗口的左边界,如果该字段更新,即使发送窗口大小snd_wnd没有发生变化,整个发送窗口也会前移,这样从流量控制的角度,就可以发送更多的数据(是否真的可以发送,还要考虑拥塞窗口等其它因素)。
可以想的到,snd_una的初始化一定发生在第一个数据段发送过程中,而snd_wnd的初始化应该是发生在第一个输入段处理过程中,所以需要客户端和服务器端分开来看。
客户端对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);
...
}
}
正面理解的话,服务器端对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;
}
显然,数据传输过程中,应该在收到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;
}
这里要明白的是,发送窗口是实现流量控制的关键,它影响的只有新数据的发送过程,与重传无关,因为重传的数据一定是在对端接收能力之内。
从TCP之数据发送(二)中有看到新数据发送的两个关键函数tcp_write_xmit()和tcp_push_one(),而且二者非常相似,参考之前的笔记中分析的tcp_snd_wnd_test()和tcp_mss_split_point()就可以明白发送窗口是如何影响发送过程的。