时钟与定时器

本页目录

周期设为 100 ms,实际每轮却约 130 ms;取消返回后,回调又出现。前者是时间基准,后者是寿命协议。

查阅目标阅读位置
计时与事件硬件第 1~3 节
取消竞态与周期漂移第 4~5 节
tick 与 timerfd 实验第 6~7 节

时钟告诉内核时间走到哪里,定时器记录未来应该发生什么,调度器决定已唤醒任务何时实际运行。这三个阶段的延迟必须分开,不能用 clock_getres 返回的纳秒分辨率保证应用一定按纳秒误差醒来。

读时间、安排中断、执行任务是三个阶段 Clocksource 计数器 → 当前时间 定时器队列 记录到期点与回调 Clockevent 安排下一次硬件通知 到期后处理回调或使任务可运行 实际执行仍受中断、锁、调度与系统负载影响 纳秒表示单位不等于纳秒唤醒误差;HZ 也不等于硬件计数器频率。

1. Clocksource 与 clockevent

clocksource 提供可读取的计数器。内核依据计数差、mask 和换算参数更新时间;计数器频率不一定等于 CPU 当前执行频率。

clockevent 负责在未来产生通知。set_next_event() 接收设备相应计数单位的 delta,而非所有设备统一接收纳秒;set_next_ktime() 等接口可使用直接时间值。核心负责换算与设备最小/最大范围等约束。

同一硬件设备可能支持两种职责,也可能分别由不同硬件承担。例如可读取的 TSC 不直接等于传统 APIC timer;TSC deadline 等机制又提供基于相应计数的事件能力。CPU 深度空闲时,本地事件设备可能需要 broadcast 机制协助。见 timekeeping 硬件抽象。

2. Jiffies、HZ 与回绕

jiffies 是内核的粗粒度时间计数,HZ 指定相关 tick 尺度。tickless 系统仍维护逻辑时间,不要求每个 CPU 每秒恰好收到 HZ 次定时中断。

getconf CLK_TCK 返回用户态接口的计时单位,不能用它直接读出 CONFIG_HZ。HZ=250 时一个 jiffy 为 4 ms,也不意味着所有 hrtimer 都只能以 4 ms 编程。

在 32 位 unsigned long 下,2^32 个计数以 HZ=100 约 497 天回绕,以 HZ=1000 约 49.7 天。到期比较应使用 time_after/time_before 等宏,并遵守相应距离范围,不能把 jiffies > expires 用作可移植超时判断。很远的未来区间也不能无限依赖半范围差值比较。

毫秒转换用 msecs_to_jiffies() 等 API,避免自行整数除法把短延迟截断成零,或忽略溢出和上限。

3. Timer wheel 与 hrtimer 为何共存

机制组织方式与用途时间语义
timer_list / timer wheel分级分桶,适合大量超时且很多会提前取消的场景jiffies 尺度,较远到期可采用更粗粒度
hrtimer按到期值组织高精度时间事件ktime/ns 表示,实际精度依事件硬件和配置

定时器不是“必须精确到期执行”的预约。IRQ 关闭、CPU 忙、线程优先级、虚拟机暂停等都会使处理滞后;高精度主要改善到期安排能力,不直接提供硬实时上限。

普通 timer callback 通常在 TIMER_SOFTIRQ 语义下执行,不可任意睡眠。hrtimer 的 hard/soft 模式和 PREEMPT_RT 配置影响实际回调上下文,也不能统一说“hrtimer 总在 HRTIMER_SOFTIRQ”或“总在 hardirq”。

若需执行阻塞工作,可由回调安排合适的普通 workqueue 或唤醒任务。schedule_hrtimeout() 是调用者等待的接口,不会把一个已经在原子上下文的 callback 转换成可睡眠函数。见 hrtimer 设计。

4. 初始化、取消与重装的寿命问题

新代码使用 timer_setup() 初始化 timer_list;当前内核提供 hrtimer_setup() 初始化 hrtimer,较旧目标可能仍需旧式初始化接口,须对照目标头文件。

动作能解决什么尚未解决什么
删除 pending timer撤销排队中的到期事件callback 可能已开始
同步删除等待当前活动结束其他路径仍可能重装
阻止重装关闭未来触发来源对象仍需活到最后访问者退出

因此退出协议必须同时协调排队状态、当前回调和未来访问者。

现代 timer_list 的 timer_shutdown_sync() 用于需要同步停止并阻止重装的退出场景,特别是 timer 与 work 互相重新调度时。旧代码中的 del_timer/del_timer_sync 名字及当前 timer_delete 系列应按目标版本核对,不能把普通 del_timer 注释成“同步删除”。

即使用 shutdown,也必须阻止未来路径访问已释放的 timer 对象;“对存活但关闭的 timer 重装被忽略”不等于“可以对悬空 timer 指针调用 API”。hrtimer_cancel 同样需要和发布/重装者协调。

取消时不要持有回调需要的锁

若线程持锁 L 调用同步取消,callback 已经开始并等待 L,则双方会互等。退出应先让新提交路径停止,在不持相互依赖锁的情况下同步等待,然后释放 timer、work 和其共同上下文。

5. 周期任务用哪个基准

假设周期 100 ms、每次处理耗时 30 ms,对比前几轮开始时间:

安排方式第 1 轮第 2 轮第 3 轮
每轮结束后再等 100 ms0130 ms260 ms
按 t0 + n × 100 ms 安排0100 ms200 ms

这里忽略其他调度延迟。绝对基准避免把工作时间反复加进周期,但实际超时后仍需决定补跑、跳过还是合并。

hrtimer_forward 等接口帮助推进到期点并报告跨越周期,返回 HRTIMER_RESTART 才按相应规则重启;它们不自动为每个漏掉周期执行一次完整业务回调。

用户态也可用 clock_nanosleep(..., TIMER_ABSTIME, ...) 或 timerfd 绝对时间模式减少相对睡眠漂移。选择 MONOTONIC、BOOTTIME 或 REALTIME 又决定校时和 suspend 如何影响截止时间。

固定周期 P=100 ms,把回调工作时间与派发迟延分别加入时间轴。相对重装会把上次工作的结束时间当成新起点,绝对期限则保留原相位。

n 从 0 开始,P 是周期,C 是每轮工作时间,J 是本图固定的派发迟延。这里的参数范围没有造成绝对周期过载;真实抖动也不会每次完全相同。

正在呈现知识画面
每次多出来的时间,怎样变成累计漂移?

结束后再等一个周期,会把工作和延迟反复计入下次基准;固定绝对相位则不会累积这两项,但仍可能每轮迟到。

6. NO_HZ 减少 tick,不消灭所有中断

NO_HZ_IDLE 在合适的空闲条件下停止周期调度 tick,按下一需要处理的事件唤醒。NO_HZ_FULL 在选定 CPU 和满足条件的负载下进一步减少运行任务时的 tick,但仍有维护工作、RCU、定时器和设备中断。

“只有一个 runnable task”是相关条件之一,不保证任何配置下都永远没有 tick。housekeeping CPU 与任务/IRQ 亲和性也要协同;nohz_full 不是完整 CPU 隔离配置。

cpuidle 的 state*/disable 表示是否允许某些空闲状态,不能直接判断当前 tick 模式。应结合内核配置、启动参数和实际追踪。见 NO_HZ 文档。

7. POSIX timer、itimer 与 timerfd

CLOCK_MONOTONIC/REALTIME 等墙钟类 POSIX timer 与 CPU-time timer 的计时来源不同,不能声称所有 timer_create 都直接对应同一种 hrtimer。SIGEV_SIGNAL、SIGEV_NONE、库实现参与的 SIGEV_THREAD 等通知机制也不同。

setitimer 的 REAL 按经过时间计时,VIRTUAL 按用户 CPU 时间,PROF 按用户与内核 CPU 时间计时;CPU 时间定时器不应当作普通睡眠超时。信号合并、overrun 及处理延迟还会影响应用观察。

timerfd 以 fd 可读性通知,read 得到一个 64 位累计到期次数,不是时间戳,也不保证每次 poll 可读只对应一次到期。下面的程序只创建本进程的 MONOTONIC 定时器,等约 75 ms 后读取 20 ms 周期的累计次数。

#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>

int main(void)
{
    int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
    if (fd < 0) { perror("timerfd_create"); return 1; }
    struct itimerspec spec = {
        .it_interval = { .tv_nsec = 20000000 },
        .it_value = { .tv_nsec = 20000000 }
    };
    if (timerfd_settime(fd, 0, &spec, NULL) < 0) {
        perror("timerfd_settime"); close(fd); return 1;
    }
    struct timespec delay = { .tv_nsec = 75000000 };
    while (nanosleep(&delay, &delay) < 0) {
        if (errno != EINTR) { perror("nanosleep"); close(fd); return 1; }
    }
    uint64_t count;
    ssize_t n;
    do { n = read(fd, &count, sizeof(count)); } while (n < 0 && errno == EINTR);
    if (n != sizeof(count)) { fprintf(stderr, "timerfd read failed\n"); close(fd); return 1; }
    printf("expirations=%" PRIu64 "\n", count);
    close(fd);
    return count >= 3 ? 0 : 1;
}
cc -std=c11 -O2 -Wall -Wextra -Werror timer-count.c -o timer-count
./timer-count

典型结果为 3,也可能因调度延迟更多,不能断言始终恰好 3。关闭 fd 回收该定时器;这个例子不修改系统时钟。详见 timerfd。