【LINUX】Linux从闹钟到高精度定时器的实现

在Linux服务器开发中,定时器无处不在------心跳检测、超时重传、定时任务调度。很多同学只会用sleep()或简单的alarm(),却对底层机制一知半解。本文将带你从应用层到内核层,搞懂Linux定时器的实现。

1 从一个面试题说起

面试官:Linux下有哪些实现定时器的方法?它们各有什么优缺点?

菜鸟:额,我知道sleep(3)可以延时...

高手:主要有5种方式------sleep/alarm(精度低)、settimer(周期触发)、timerfd(融入epoll)、时间堆(高性能)、时间轮(大量定时器)。选择哪种取决于精度要求和应用场景。

今天我们就沿着这个问题,简单看一下这些实现。

2 应用层定时器

2.1 sleep与alarm

c 复制代码
#include <unistd.h>
#include <signal.h>

// sleep:进程休眠指定秒数
sleep(5);  // 精度秒级,挂起进程

// alarm:在指定秒数后发送SIGALRM信号
void sig_handler(int signo) {
    printf("定时器触发!\n");
}
signal(SIGALRM, sig_handler);
alarm(3);  // 3秒后触发,只触发一次

精度只有秒级,且alarm通过信号处理,存在信号丢失和异步安全问题。

2.2 setitimer

c 复制代码
#include <sys/time.h>

struct itimerval tick;
tick.it_value.tv_sec = 0;    // 首次触发时间
tick.it_value.tv_usec = 100000; // 100ms后首次触发
tick.it_interval.tv_sec = 0;    // 周期触发间隔
tick.it_interval.tv_usec = 100000; // 每100ms触发一次

setitimer(ITIMER_REAL, &tick, NULL);
// 同样通过SIGALRM信号处理

支持微秒级精度和周期性触发,但仍受限于信号处理机制。

3 融入事件循环的timerfd

3.1 为什么需要timerfd?

信号最大的问题是难以与epoll/select等I/O多路复用机制配合。信号中断后,你无法确定是哪个fd就绪,还得处理EINTR错误。timerfd应运而生------它把定时器变成了一个文件描述符!

3.2 timerfd实现

c 复制代码
#include <sys/timerfd.h>
#include <sys/epoll.h>

int timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);

struct itimerspec new_value;
new_value.it_value.tv_sec = 1;     // 首次1秒后到期
new_value.it_value.tv_nsec = 0;
new_value.it_interval.tv_sec = 0;  // 之后每500ms到期
new_value.it_interval.tv_nsec = 500000000;

timerfd_settime(timerfd, 0, &new_value, NULL);

// 加入epoll统一管理
epoll_ctl(epfd, EPOLL_CTL_ADD, timerfd, &ev);

在epoll循环中:

c 复制代码
uint64_t exp;
read(timerfd, &exp, sizeof(uint64_t)); // 必须读,否则会重复触发
printf("定时器到期 %llu 次\n", exp);

纳秒级精度,完美融入epoll事件循环,不会丢失定时事件(read会返回到期次数)。

4 定时器的底层实现

讲完应用层,我们从内核源码角度看看定时器是如何实现的。

4.1 时间车轮(Time Wheel)

Linux 2.4内核使用时间轮算法管理定时器。内核把未来时间分成多个槽(slot),每个槽挂接一个定时器链表。

bash 复制代码
   [0] -> timer1 -> timer5
   [1] -> timer3
   [2] -> NULL
   ...
   [511] -> timer2 -> timer4
   
   指针每tick移动一次,时间复杂度O(1)

这种结构在定时器数量很大时效率极高,但精度受tick频率限制(通常1ms到10ms)。

4.2 红黑树(高精度定时器hrtimer)

现代Linux内核(2.6.16+)引入了高精度定时器hrtimer,基于红黑树实现。

  • 红黑树节点按到期时间排序

  • 每次取最左边的节点(最近到期)

  • 到期后执行回调,并从树中删除

  • 插入和删除复杂度O(log n)

c 复制代码
// 内核中定义一个hrtimer
struct hrtimer timer;
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
timer.function = my_callback;

hrtimer_start(&timer, ktime_set(0, 500000000), HRTIMER_MODE_REL);
// 500ms后回调

4.3 时钟中断与Tickless

传统内核依赖固定的时钟中断(如1000Hz),每次中断检查定时器。但在空闲时频繁中断会耗电。

Tickless内核动态编程硬件定时器,只在最近一个定时器到期时才产生中断。这样CPU可以长时间睡眠,极大节省功耗(手机、笔记本受益明显)。

5 高性能服务器中的时间轮实现

在高并发场景(如百万长连接),每个连接都可能有定时器。此时红黑树的O(log n)开销也显得昂贵,工业界常用多层级时间轮:

cpp 复制代码
class TimerWheel {
    // 5个轮子,分别对应1ms, 20ms, 400ms, 8s, 160s
    const int WHEEL_SIZE = 256;
    vector<list<Timer*>> wheels[5];
    
    void add_timer(Timer* t) {
        // 根据超时时间选择层级
        int level = get_level(t->expire);
        int slot = (t->expire >> (level*8)) & 0xFF;
        wheels[level][slot].push_back(t);
    }
    
    void tick() {
        // 当前时间推进一个单位
        // 如果是第0层的本轮刻度走完,就级联下降上层
    }
};

Nginx、Linux内核网络协议栈都使用了多级时间轮来管理大量连接的超时。

6 总结与选型建议

定时器方案 精度 性能 适用场景
alarm/sleep 秒级 简单脚本
setitimer 微秒 单进程周期任务
timerfd 纳秒 需集成事件循环的服务
时间堆(红黑树) 纳秒 O(log n) 定时器数量适中
时间轮 取决于tick O(1) 海量定时器(10万+)

选择建议:

  • 写小工具用setitimer或sleep
  • 网络服务首选timerfd,配合epoll完美工作
  • 实现Reactor/Proactor框架,内部用时间轮管理连接超时
  • 做游戏服务器,混合使用:红黑树处理技能冷却,时间轮处理心跳
相关推荐
深爱水瓶 血影S狂风4 小时前
函数调用过程探究
linux·运维·服务器
酷可达拉斯4 小时前
Linux操作系统-磁盘空间使用率100%如何处理?
linux·运维·服务器·云计算·bash
AIgorithmGEEK5 小时前
Linux 权限管理:从「Permission Denied」到「畅行无阻」
linux·权限·chmod·粘滞位·umask·文件权限的构成
BullSmall5 小时前
scanoss-engine 本地最简部署 & 扫描命令(Linux/Anolis OS)
linux·运维·服务器
bellus-5 小时前
Ubuntu 26.04 Miniforge3 安装与配置完整指南
linux·运维·ubuntu
今天AI了吗7 小时前
OpenCode AI 编程:Ubuntu 24.04 环境安装与使用指南
linux·人工智能·ubuntu
用什么都重名7 小时前
Ubuntu 24.04 安装 NVIDIA 驱动:解决 “NVIDIA-SMI has failed“ 报错全记录
linux·运维·ubuntu
Industio_触觉智能7 小时前
瑞芯微ISP内核驱动深度解析|RKISP39从架构到Probe初始化全流程(上篇)
linux·嵌入式硬件·硬件架构·摄像头·isp·瑞芯微·rk3576
有续技术7 小时前
三菱 (Mitsubishi) 机床 设置查看 IP 地址
linux·服务器·网络