Linux tcp_keepalive 保活定时器 keepalive_timer 实现
2026/7/21 12:15:51 网站建设 项目流程

Linux tcp_keepalive 保活定时器 keepalive_timer 实现

tcp_keepalive_timer 是 TCP 保活探测的核心定时器回调函数,注册在 inet_connection_sock 的 icsk_delack_timer 定时器基座上,但使用 ICSK_TIME_KEEPALIVE 作为定时器类型区分。该定时器在连接建立后启动(tcp_init_transfer 中调用 tcp_keepalive_timer 仅为首次 reset),但真正的保活探测序列在无数据交换的静默期后触发。

```c
static void tcp_keepalive_timer(struct timer_list *t)
{
struct sock *sk = from_timer(sk, t, sk_timer);
struct tcp_sock *tp = tcp_sk(sk);
u32 elapsed;

bh_lock_sock(sk);
if (sock_owned_by_user(sk)) {
if (__timer_pending(sk->sk_timer))
goto out;
sk_reset_timer(sk, &sk->sk_timer, jiffies + HZ / 20);
goto out;
}

if (sk->sk_state == TCP_CLOSE || sk->sk_state == TCP_LISTEN)
goto out;

elapsed = keepalive_time_elapsed(tp);
if (elapsed >= keepalive_time + keepalive_intvl * tp->keepalive_probes) {
tcp_send_active_reset(sk, GFP_ATOMIC);
tcp_write_err(sk);
goto out;
}

if (elapsed < keepalive_time)
goto resched;

if (!tp->packets_out && !tcp_write_queue_empty(sk))
tcp_push_one(sk, mss_now, TCP_NAGLE_OFF);

tcp_send_probe0(sk);
resched:
sk_reset_timer(sk, &sk->sk_timer, keepalive_time_when(tp));
out:
bh_unlock_sock(sk);
sock_put(sk);
}
```

函数入口使用 bh_lock_sock 而非 lock_sock,因为定时器运行在 TIMER_SOFTIRQ 上下文。若用户进程已持有 lock_sock(sock_owned_by_user 为 true),定时器不阻塞等待而是短延时(HZ/20 = 50ms)后重新调度。这种 defer 策略避免了优先级反转——用户进程可能在用户态长时间阻塞(如 fgets 等待输入),而保活探测仍需进行。

保活探测的阈值链:keepalive_time(默认 7200 秒)定义了空闲时长;超过该值后发送第一个保活探测报文(空 ACK 段,序列号为 snd_una-1 以触发对端 ACK)。每个探测的间隔由 keepalive_intvl(默认 75 秒)控制。在连续发送 keepalive_probes(默认 9 次)失败后,定时器调用 tcp_send_active_reset 发送 RST 断开连接,并调用 tcp_write_err 通知上层 socket 错误(sk->sk_err = ETIMEDOUT)。

```c
void tcp_send_probe0(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb = tcp_write_queue_tail(sk);
int err;

if (skb && !tcp_skb_is_write_queue(sk, skb))
skb = NULL;

if (skb) {
if (TCP_SKB_CB(skb)->end_seq != tp->snd_nxt)
tp->snd_nxt = TCP_SKB_CB(skb)->end_seq;
err = tcp_write_xmit(sk, tcp_current_mss(sk), 1, 0, GFP_ATOMIC);
} else {
tcp_v4_send_ack(sk, tp->snd_una - 1, tp->rcv_nxt,
tp->rcv_wnd >> tp->rcv_wscale,
tcp_time_stamp_raw(), tp->rx_opt.ts_recent,
0, 0, FLAG_ACK_NO_DELAY);
}
}
```

tcp_send_probe0 负责将保活探测报文实际发送到 IP 层。有两种情况:如果 write_queue 非空(即有未确认数据),则调用 tcp_write_xmit 发送尾部 skb 的剩余数据;如果 write_queue 为空,直接调用 tcp_v4_send_ack 构造一个 seq = snd_una-1 的 ACK 包。对端收到 seq = snd_una-1 的 ACK 后,若连接尚存则回复 ACK(序列号为 rcv_nxt),sender 收到该 ACK 后重置保活定时器。

tcp_keepalive_timer 由 sk_reset_timer 在多个路径被重置:
- tcp_rcv_established 收到数据包时重置;
- tcp_ack 累积确认前进时重置;
- 用户进程调用 send/recv 导致数据交互时在 __tcp_push_pending_frames 中重置。

核心的竞争条件出现在连接关闭路径。当用户进程调用 close() 触发 tcp_close,该函数调用 tcp_set_state(sk, TCP_CLOSE) 后,tcp_keepalive_timer 可能已经在 TIMER_SOFTIRQ 队列中 pending。tcp_close 中调用 sk_stop_timer 移除定时器,但如果定时器已经触发并在 bh_lock_sock 上自旋,则退出时 sk_state 已被改为 TCP_CLOSE,定时器检查到 TCP_CLOSE 后直接 goto out,不会发送探测报文。另一种情况是 tcp_write_err(在保活探测超限后)调用 tcp_done 关闭 socket,tcp_done 内部将 sk_state 置为 TCP_CLOSE 并调用 sk_stop_timer 停止所有定时器。

保活探测对 TCP_NODELAY 的影响:若设置了 TCP_NODELAY,保活探测的 tcp_push_one 不受 Nagle 约束立刻发送。默认情况下(TCP_CORK 或 Nagle 算法),tcp_push_one 可能延迟探测报文发送,但 keepalive 定时器的逻辑直接调用 tcp_v4_send_ack 会绕过 Nagle 检查。

```c
static u32 keepalive_time_elapsed(struct tcp_sock *tp)
{
const struct tcp_sock *tp = tcp_sk(sk);
s32 delta;

delta = tp->rcv_tstamp - tp->lsndtime;
if (delta < 0)
delta = 0;
return delta;
}
```

keepalive_time_elapsed 使用 tp->rcv_tstamp(最后收到数据的时间戳)和 tp->lsndtime(最后发送数据的时间戳)中的较新者减去当前时间计算空闲时长。rcv_tstamp 在 tcp_replace_ts_recent 中被更新,而 lsndtime 在 tcp_transmit_skb 结束时被 tp->lsndtime = tcp_jiffies32 更新。这里的一个角落问题:若 tcp_v4_rcv 中调用 tcp_rcv_established 时没有数据(如 pure ACK),rcv_tstamp 不会被更新,可能导致保活定时器未按预期重置。但 tcp_rcv_established 在收到 ACK 不带数据时会通过 tcp_data_queue 的 __kfree_skb 路径,而在 tcp_replace_ts_recent 中只要时间戳选项非空,rcv_tstamp 依然会更新。

keepalive_intvl 的 sysctl 修改不影响已经在运行的保活探测序列:定时器一旦启动,其间隔由 tp->keepalive_intvl 快照决定,该值在 tcp_init_transfer 时从 net->ipv4.sysctl_tcp_keepalive_intvl 复制。若需要立即生效,必须重启连接或通过 setsockopt TCP_KEEPINTVL 覆盖 socket 级别的设置。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询