增加定时器框架 统一管理 TIME_WAIT、保活、空闲超时

一、背景与需求

现存3类核心超时任务

  1. TIME_WAIT 2MSL 延迟关闭定时器
    TCP四次挥手完成后进入TIME_WAIT状态,需等待2MSL时长再彻底释放端口与Socket资源,避免延迟残留报文干扰新连接。不能直接close立刻回收fd。
  2. TCP保活定时器(KeepAlive)
    检测静默僵死长连接(防火墙断网、掉线、无报文),周期性发送保活探测包,超时无响应则主动断开连接,防止大量僵死连接占用fd/内存。
  3. 空闲超时定时器(idle timeout)
    长时间无读写数据的空闲连接,自动关闭回收资源,防止内存/句柄泄漏,降低服务器负载。

简易定时器的缺陷

  • 全局轮询遍历全部Socket:O(N)复杂度,海量连接时CPU开销巨大
  • 分散定时器/全局sleep:无法统一调度、精度差、易内存泄漏、无法精准取消
  • 需求:统一全局定时器管理器,支持O(logN)添加/删除/到期检查,以到期绝对时间排序 → 红黑树定时器是经典方案
    • 红黑树:有序排序,key = 到期绝对时间戳
    • 每次只检查最左侧节点(最早到期任务),避免全量遍历

前置前提

  • epoll Reactor事件循环 + 异步发送队列 + 引用计数/锁模型已就绪
  • 精确系统时间(毫秒级时间戳)
  • 支持:新增定时器、取消定时器、到期回调、绑定对应Socket、保证并发安全

二、红黑树定时器原理 & 整体结构

核心设计

  • Key:绝对到期时间戳(ms,如系统毫秒时钟),红黑树按到期时间升序排序,最左节点 = 最先到期任务
  • 每个定时器节点绑定:所属Socket、回调函数、类型标记、参数、是否有效标记
    • 类型:TIME_WAIT / KEEP_ALIVE / IDLE
    • 回调:到期后执行对应逻辑(关闭Socket、发送保活探测、断开僵死连接)
    • 有效标记:避免延迟回调访问已销毁Socket(配合refcount引用计数)
  • 主线程epoll循环每轮检查红黑树根/最左节点,判断是否到期并触发回调
  • 备选:最小堆定时器(实现简单但删除任意节点效率差);长期原型优先红黑树

C基础数据结构伪代码

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <pthread.h>
#include <sys/time.h>

// 定时器类型
typedef enum {
    TIMER_TYPE_TIME_WAIT,
    TIMER_TYPE_KEEP_ALIVE,
    TIMER_TYPE_IDLE
} TimerType;

// 前向声明
typedef struct SocketSk SocketSk;
typedef void (*TimerCallback)(void *arg, TimerType type);

// 红黑树颜色
typedef enum {
    RBT_RED = 0,
    RBT_BLACK = 1
} RbtColor;

// 定时器红黑树节点
typedef struct TimerNode {
    uint64_t expire_ms;         // key: 绝对到期毫秒时间戳
    RbtColor color;
    struct TimerNode *parent;
    struct TimerNode *left;
    struct TimerNode *right;

    TimerType type;
    TimerCallback cb;
    void *arg;                  // 绑定SocketSk指针
    int valid;                  // 有效标记:防止回调野指针
} TimerNode;

// 全局红黑树定时器管理器
typedef struct {
    TimerNode *root;
    TimerNode *nil;             // 红黑树哨兵节点
    pthread_mutex_t lock;      // 全局定时器锁
} TimerManager;

static TimerManager g_timer_mgr;
辅助工具函数
  1. 获取当前毫秒时间戳
uint64_t get_current_ms(void) {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return (uint64_t)tv.tv_sec * 1000 + (uint64_t)tv.tv_usec / 1000;
}
  1. 红黑树基础操作:插入、删除、左旋、右旋、修复着色、查找最小节点(最早到期)
    • 核心目标:保证红黑树平衡,维持到期时间有序,保证增删查 O(logN)
    • 重点:支持按绑定Socket批量取消定时器(Socket关闭时),防止残留无效定时器

三、三类定时器详细实现

3.1 TIME_WAIT 2MSL 延迟定时器

原理
  • MSL:报文最大生存时间,2MSL = 两倍最大生存时长(通常设置60s/30s)
  • 触发时机:TCP四次挥手完成、进入TIME_WAIT状态时创建定时器
  • 到期回调:彻底释放Socket、回收fd、销毁内存
  • 提前取消:正常复用端口/主动重置时取消定时器
// 创建TIME_WAIT定时器
int timer_add_time_wait(SocketSk *sk, uint64_t msl_ms) {
    uint64_t now = get_current_ms();
    uint64_t expire = now + 2 * msl_ms;

    TimerNode *node = timer_node_create(expire, TIMER_TYPE_TIME_WAIT, time_wait_callback, sk);
    rbt_insert(&g_timer_mgr, node);
    return 0;
}

// TIME_WAIT到期回调
void time_wait_callback(void *arg, TimerType type) {
    SocketSk *sk = (SocketSk *)arg;
    if (!sk || sk->refcount <= 0) return;
    // 彻底销毁Socket、回收fd,完成延迟关闭
    final_destroy_socket(sk);
}
  • 核心:在定时器到期前不真正释放fd和端口,仅标记关闭状态,保证延迟报文被正确处理,不污染新连接
  • 可配合 SO_REUSEADDR,优化快速重启场景

3.2 TCP KeepAlive 保活定时器

原理

三层保活逻辑:

  1. 空闲时长无数据 → 发送TCP保活探测报文
  2. 多次探测无ACK响应 → 判断连接僵死,主动关闭
  3. 正常收到数据/ACK → 重置保活定时器
// 创建保活定时器
int timer_add_keepalive(SocketSk *sk, uint64_t idle_ms) {
    uint64_t now = get_current_ms();
    uint64_t expire = now + idle_ms;
    TimerNode *node = timer_node_create(expire, TIMER_TYPE_KEEP_ALIVE, keepalive_callback, sk);
    rbt_insert(&g_timer_mgr, node);
    return 0;
}

// 保活回调:发送探测包 / 判断超时断开
void keepalive_callback(void *arg, TimerType type) {
    SocketSk *sk = (SocketSk *)arg;
    if (!sk || sk->write_closed) return;

    // 发送TCP保活探测报文
    send_tcp_keepalive_probe(sk);
    // 重新设置下一次保活定时器
    timer_add_keepalive(sk, KEEP_ALIVE_INTERVAL_MS);
}

// 每次正常读写数据时,重置保活定时器(取消旧节点,新建)
void reset_keepalive_timer(SocketSk *sk) {
    timer_cancel_by_arg(&g_timer_mgr, sk, TIMER_TYPE_KEEP_ALIVE);
    timer_add_keepalive(sk, KEEP_ALIVE_IDLE_MS);
}
  • 关键:每次正常收发数据都重置保活定时器,避免有效连接被误断
  • 配合TCP底层保活选项,兼顾内核保活与应用层保活,处理防火墙静默断连场景

3.3 空闲超时定时器

原理
  • 长时间无读写交互的空闲连接,自动关闭释放资源,防止内存/句柄泄漏
  • 每次读写数据都重置空闲超时计时器
// 空闲超时回调
void idle_timeout_callback(void *arg, TimerType type) {
    SocketSk *sk = (SocketSk *)arg;
    if (!sk) return;
    printf("idle timeout, close fd %d\n", sk->fd);
    graceful_close_socket(sk); // 优雅关闭
}

void reset_idle_timer(SocketSk *sk) {
    timer_cancel_by_arg(&g_timer_mgr, sk, TIMER_TYPE_IDLE);
    timer_add_idle(sk, IDLE_TIMEOUT_MS);
}

3.4 通用定时器取消接口

// 按Socket+类型批量删除定时器
void timer_cancel_by_arg(TimerManager *mgr, void *arg, TimerType type) {
    pthread_mutex_lock(&mgr->lock);
    rbt_delete_by_arg(mgr, arg, type);
    pthread_mutex_unlock(&mgr->lock);
}
  • 核心:Socket关闭时必须取消全部关联定时器,防止无效节点堆积、野指针回调

四、epoll主循环整合定时器检查

每轮epoll_wait后检查到期定时器,统一触发回调:

void process_expired_timers() {
    pthread_mutex_lock(&g_timer_mgr.lock);
    uint64_t now = get_current_ms();

    // 循环取出最小到期节点
    while (1) {
        TimerNode *min_node = rbt_get_min(&g_timer_mgr);
        if (min_node == g_timer_mgr.nil || min_node->expire_ms > now) {
            break;
        }
        // 标记失效、执行回调
        min_node->valid = 0;
        if (min_node->cb) {
            min_node->cb(min_node->arg, min_node->type);
        }
        // 删除节点
        rbt_delete(&g_timer_mgr, min_node);
    }
    pthread_mutex_unlock(&g_timer_mgr.lock);
}

void epoll_event_loop() {
    struct epoll_event events[MAX_EVENTS];
    while (1) {
        int n = epoll_wait(g_epoll_fd, events, MAX_EVENTS, 10); // 短超时防止卡死
        if (n > 0) {
            // 处理epoll可读/可写事件
            handle_epoll_events(events, n);
        }
        // 统一处理到期定时器
        process_expired_timers();
    }
}
  • epoll_wait设置短暂超时(如10ms),保证定时器可按时被扫描,避免epoll长期阻塞导致定时器延迟触发
  • 回调执行前校验valid标记 + refcount引用计数,保证Socket对象有效

五、并发安全 & 内存管理要点

  1. 全局红黑树锁
    插入/删除/遍历定时器节点全程加锁,防止epoll主线程/回调线程并发修改红黑树结构,造成树结构损坏、崩溃
  2. 引用计数联动
    • 定时器回调前检查SocketSk refcount和valid标记,禁止访问已销毁Socket
    • Socket关闭时先调用timer_cancel_by_arg,清理全部绑定定时器,再做最终内存释放
  3. 哨兵节点设计
    红黑树使用nil哨兵节点简化边界判断,减少空指针崩溃
  4. 内存池优化
    定时器节点复用内存池,减少频繁malloc/free带来的性能开销和碎片

六、避坑指南

  1. ❗不要用相对时间做红黑树key
    • 相对时间会破坏有序性,必须用绝对系统时间戳作为key
  2. ❗不要遗留无效定时器节点
    • Socket关闭但定时器未取消 → 内存泄漏、野指针访问、反复无效回调
    • ✅ 增加valid标记做双重防护
  3. ❗长阻塞回调
    • 定时器回调尽量做轻量操作(标记关闭、发探测包),不要做阻塞IO,卡住主线程epoll循环
  4. ❗精度问题
    • 毫秒级足够业务原型;高精度场景可改用纳秒时间戳;不要依赖定时器做硬实时
  5. ❗TIME_WAIT复用冲突
    • 正确设置2MSL时长 + SO_REUSEADDR配合,兼顾延迟报文安全与端口复用需求

七、整体数据流

  1. TCP完成四次挥手 → 创建TIME_WAIT红黑树定时器,延迟2MSL释放
  2. 长连接建立 → 创建保活+空闲定时器;每次正常读写重置这两类定时器
  3. epoll主循环每轮检查红黑树到期节点,统一执行回调
  4. Socket优雅关闭 → 提前取消全部绑定定时器,保证无残留节点
  5. 底层协议栈、Socket层、epoll事件循环、定时器框架完全解耦,可独立迭代维护
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐