Switch-Router

理解内核TCP定时器实现

Published at 2021-09-15 | Last Update 2021-09-15

为什么内核TCP需要定时器

TCP协议中的一些过程是需要在一段特定时间间隔后被处罚, 包括基本的报文 retransmit、delay ACK、timewait 等等.

另外, 定时器触发后的响应函数都是在 bottom half 上下文运行, 整个过程无需用户程序参与.

定时器分类

站在一个tcp连接的视角,内核为其准备了以下几个定时器: reqsk、retransmit、delack、keepalive、timewait、pacing.

接下来分别进行解释

reqsk timer

这是一个仅用于服务端的定时器, 它在收到 SYN 报文后被创建和启动,如果收不到对端 SYN ACK 的确认,

超时响应函数reqsk_timer_handler会被调用, 重传 SYN ACK, 并重新 rearm 定时器, 如果超过一定次数, 半连接将被释放.

创建定时器
tcp_conn_request
 |- inet_csk_reqsk_queue_hash_add
    |- reqsk_queue_hash_req
       |-- timer_setup(&req->rsk_timer, reqsk_timer_handler, TIMER_PINNED);

retransmit timer

retransmit timer 用于报文重传. 然后, 稍微有点复杂的是,内核tcp的多个特性都可以去设置其超时时间.

  • ICSK_TIME_RETRANS: 最基本的超过 RTO 后重传报文
  • ICSK_TIME_PROBE0: 超时后发送零窗口探测报文, 参考 zero-window-size 死锁
  • ICSK_TIME_EARLY_RETRANS: 超时后进行 early 重传 — 参考 early retrans 特性
  • ICSK_TIME_LOSS_PROBE: 超时后发送 TLP 探测报文 — 参考 TLP 特性
  • ICSK_TIME_REO_TIMEOUT: 超时后标记 lost — 参考 RACK 特性

其中, RETRANS 表示通常意义上的超时重传, PROBE0 表示进行零窗口探测, 余下三个则是一些针对快速重传和快速恢复的优化

retransmit timer 在客户端和服务端的创建时机稍有不同, 前者在tcp socket创建时, 后者在三次握手成功后.

客户端创建定时器
tcp_init_sock
  |- tcp_init_xmit_timers 
      |- inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer);
        |- timer_setup(&icsk->icsk_retransmit_timer, retransmit_handler, 0);

服务端创建定时器
tcp_v4_syn_recv_sock
 |- tcp_create_openreq_child
   |-tcp_init_xmit_timers 
      |- inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer);
        |- timer_setup(&icsk->icsk_retransmit_timer, retransmit_handler, 0);

启动定时器时, 需要额外将来源记录在 icsk->icsk_pending 中, 之后在超时响应函数tcp_write_timer()中, 根据不同的来源进行不同处理.

static inline void inet_csk_reset_xmit_timer(struct sock *sk, const int what,
					     unsigned long when,
					     const unsigned long max_when)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    ...
    if (what == ICSK_TIME_RETRANS || what == ICSK_TIME_PROBE0 ||
        what == ICSK_TIME_EARLY_RETRANS || what == ICSK_TIME_LOSS_PROBE ||
        what == ICSK_TIME_REO_TIMEOUT) {
        icsk->icsk_pending = what;              // 记录来源
        icsk->icsk_timeout = jiffies + when;
        sk_reset_timer(sk, &icsk->icsk_retransmit_timer, icsk->icsk_timeout);
    } 
    ...
}

delack timer

delack timer 用于控制 delay ack 的发送, 其创建时机与 retransmit timer 一致

创建定时器
tcp_init_xmit_timers 
 |- inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer);
    |- timer_setup(&icsk->icsk_delack_timer, delack_handler, 0);

其启动时机有多个, 比如收到报文、发送ack后.

启动定时器
inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK, TCP_DELACK_MAX, TCP_RTO_MAX);
  |- inet_csk_reset_xmit_timer
    |- icsk->icsk_ack.pending |= ICSK_ACK_TIMER;
       icsk->icsk_ack.timeout = jiffies + when;
       sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);

keepalive timer

keepalive timer 用于防止 TCP 连接长时间闲置, 其创建时机与 retransmit timer 一致.

每当tcp连接双方有交互, inet_csk_reset_keepalive_timer()会将定时器重置.

创建定时器
tcp_init_xmit_timers 
 |- inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer);
    |- timer_setup(&sk->sk_timer, keepalive_handler, 0);

timewait timer

timewait timer 用于 TIME-WAIT 或 FIN-WAIT-2 状态的计时, TIME-WAIT 超时后连接被彻底释放.

创建定时器
tcp_time_wait
 |- inet_twsk_alloc
   |- timer_setup(&tw->tw_timer, tw_timer_handler, TIMER_PINNED);

在创建后, 该定时器便会被启动

tcp_time_wait
 |- inet_twsk_schedule
   |- __inet_twsk_schedule
     |- mod_timer(&tw->tw_timer, jiffies + timeo)

pacing timer

pacing timer 用于在 BBR 之类的拥塞控制算法中控制报文的发送. 其创建时机与 retransmit timer 一致.

创建定时器
tcp_init_xmit_timers
 |- hrtimer_init(&tcp_sk(sk)->pacing_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS_PINNED_SOFT);

而在报文发送时, 启动该定时器

启动定时器
tcp_write_xmit
 |- tcp_pacing_check
   |- hrtimer_start(&tp->pacing_timer,ns_to_ktime(tp->tcp_wstamp_ns), HRTIMER_MODE_ABS_PINNED_SOFT);