在TCP连接空闲一段时间之后,发送端再次开启发送时,可能导致大量的数据发送到网络中。由于一段空闲时间后,TCP发送端不能再使用ACK时钟发送新报文到网络中,所以,可能线速发送一整拥塞窗口的数据,容易造成网络拥塞,而且,网络状况可能已经改变。因此,内核中在空闲时间超过RTO之后,将使用慢启动恢复发送。
空闲检查
在空闲检查函数中,如果内核未打开在空闲之后启用慢启动的功能,即tcp_slow_start_after_idle值为零,或者网络中存在发送的报文;或者拥塞控制算法定义了相关处理,不启用慢启动。否则,如果当前时间与最后一次的发送时间差值大于RTO时长,开启慢启动。
static inline void tcp_slow_start_after_idle_check(struct sock *sk)
{
const struct tcp_congestion_ops *ca_ops = inet_csk(sk)->icsk_ca_ops;
struct tcp_sock *tp = tcp_sk(sk);
s32 delta;
if (!sock_net(sk)->ipv4.sysctl_tcp_slow_start_after_idle || tp->packets_out ||
ca_ops->cong_control)
return;
delta = tcp_jiffies32 - tp->lsndtime;
if (delta > inet_csk(sk)->icsk_rto)
tcp_cwnd_restart(sk, delta);
}
如下tcp_cwnd_restart函数,重启动窗口值首先赋值为tcp_init_cwnd函数的计算值,其一般情况下等于初始窗口值TCP_INIT_CWND(10),但是,如果在路由项中缓存了初始窗口值,等于缓存值。慢启动阈值ssthresh等于其当前值与拥塞窗口*3/4两者之间的最大值。其次,重启动窗口值选择其与当前窗口值两者之间的较小值,如果当前拥塞窗口大于重启动窗口,空闲时长每经过RTO时段,将当前拥塞窗口减半。最后,最终的拥塞窗口等于拥塞窗口与重启动窗口之间的最大值。
/* RFC2861. Reset CWND after idle period longer RTO to "restart window".
* This is the first part of cwnd validation mechanism.
*/
void tcp_cwnd_restart(struct sock *sk, s32 delta)
{
struct tcp_sock *tp = tcp_sk(sk);
u32 restart_cwnd = tcp_init_cwnd(tp, __sk_dst_get(sk));
u32 cwnd = tp->snd_cwnd;
tcp_ca_event(sk, CA_EVENT_CWND_RESTART);
tp->snd_ssthresh = tcp_current_ssthresh(sk);
restart_cwnd = min(restart_cwnd, cwnd);
while ((delta -= inet_csk(sk)->icsk_rto) > 0 && cwnd > restart_cwnd)
cwnd >>= 1;
tp->snd_cwnd = max(cwnd, restart_cwnd);
tp->snd_cwnd_stamp = tcp_jiffies32;
tp->snd_cwnd_used = 0;
}
如下在传输函数__tcp_transmit_skb中,调用tcp_event_data_sent更新最后的发送时间到变量lsndtime中,这里注意只有当发送数据报文时,才更新发送时间。
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
...
if (skb->len != tcp_header_size) {
tcp_event_data_sent(tp, sk);
static void tcp_event_data_sent(struct tcp_sock *tp, struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
const u32 now = tcp_jiffies32;
if (tcp_packets_in_flight(tp) == 0)
tcp_ca_event(sk, CA_EVENT_TX_START);
tp->lsndtime = now;
空闲检查路径
在TCP报文发送流程中,函数skb_entail将skb添加到套接口发送队列sk_write_queue末尾,函数最后,调用tcp_slow_start_after_idle_check检查函数,即在发送报文前检查空闲时间是否超时。
static void skb_entail(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct tcp_skb_cb *tcb = TCP_SKB_CB(skb);
skb->csum = 0;
tcb->seq = tcb->end_seq = tp->write_seq;
tcb->tcp_flags = TCPHDR_ACK;
tcb->sacked = 0;
__skb_header_release(skb);
tcp_add_write_queue_tail(sk, skb);
...
tcp_slow_start_after_idle_check(sk);
}
另外,在接收到对端ACK报文时,如果此报文更新了发送窗口,并且发送队列不为空,调用空闲检查函数。如果发送端是因为发送窗口为空,而停止发送,当发送窗口再次打开时,继续发送之前,需要进行此空闲检查。
static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack, u32 ack_seq)
{
struct tcp_sock *tp = tcp_sk(sk);
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_seq);
if (tp->snd_wnd != nwin) {
tp->snd_wnd = nwin;
...
if (!tcp_write_queue_empty(sk))
tcp_slow_start_after_idle_check(sk);
内核版本 5.0