目录
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()就可以明白发送窗口是如何影响发送过程的。