赞
踩
TCP的发送过程由滑动窗口控制,而滑动窗口的大小受限于发送窗口和拥塞窗口,拥塞窗口由拥塞控制算法的代表,而发送窗口是流量控制算法的代表,这篇笔记记录了发送窗口相关的内容,包括发送窗口的初始化、更新、以及它是如何影响数据发送过程的。
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()就可以明白发送窗口是如何影响发送过程的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。