Linux 时间子系统是构建现代分时多任务操作系统的基石。它精确地管理与时间相关的核心任务,为任务调度、资源管理、驱动开发乃至上层应用程序提供了不可或缺的支撑。这篇文章将从硬件到软件,为你全景式解析 Linux 时间子系统的运作机制。

整个子系统从功能上可分为“定时”与“计时”两大块。定时部分负责在特定时刻触发中断事件;而计时部分则专注于记录和跟踪现实世界的时间线。
定时部分:系统有一个全局计数器(counter),每个 CPU 核心则配备一个硬件定时器(timer)。定时器内置比较器,当预设值与全局计数器值匹配时便触发中断。在软件层面,每个 CPU 的定时器被抽象为时钟事件设备。在高精度模式下,每个时钟事件设备会关联一个本地高精度定时器(hrtimer)管理结构。hrtimer 基于事件触发,使用红黑树来高效管理该 CPU 上所有类型的软件定时任务。每次执行完到期任务后,它都会选取下一个最早到期的任务来设定下次中断时刻。
基于 hrtimer,内核进一步封装了多种类型和精度的软件定时器。例如,为了方便内核调度,定义了节拍定时器作为系统“心跳”,驱动任务调度、负载计算等。为了方便用户空间使用,则提供了 posix-timer、alarm、timer_fd、nanosleep、itimer 等接口。
计时部分:全局计数器在软件上被抽象为时钟源设备。其特点是计数频率高、精度高,且在系统休眠时仍能持续工作,可以通过寄存器高效读取当前计数值。timekeeping 模块作为 Linux 计时维护的核心,它利用时钟源提供持续不断的高精度计时,并依靠 RTC 或网络 NTP 提供真实世界的时间基准,从而维护各种系统时间的准确与可靠。
timekeeping 不仅为内核模块提供了丰富的时间获取接口,也封装了大量系统调用供用户空间使用。特别值得一提的是,它通过 VDSO 技术,允许用户空间程序绕过系统调用,以极高的效率获取系统时间。此外,任务调度、printk/ftrace 时间戳等功能也依赖于时钟源提供的高精度计时。
定时器与时钟源初始化
ARMv8 架构文档描述了其通用定时器(Generic Timer)的典型结构,如下图所示。其中 System counter 是位于“始终供电域”的全局计数器,确保系统休眠时也能正确计数。Timer_x 是 CPU 本地的定时器,每个处理单元(PE)至少有一个专属定时器。所有本地定时器都以 System counter 作为时钟源,共享其计数值以保证时间同步。本地定时器通过中断控制器,向 CPU 发起 PPI(私有外设中断)。

解析设备树
以高通 8 核 ARMv8 SoC 为例,CPU 本地定时器在设备树中称为 arch_timer,配置如下:
arch_timer: timer {
compatible = "arm,armv8-timer";
interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(8) | IRQ_TYPE_LEVEL_LOW)>,
<GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(8) | IRQ_TYPE_LEVEL_LOW)>,
<GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(8) | IRQ_TYPE_LEVEL_LOW)>,
<GIC_PPI 12 (GIC_CPU_MASK_SIMPLE(8) | IRQ_TYPE_LEVEL_LOW)>;
clock-frequency = <19200000>;
always-on;
};
- 匹配字符串:
"arm,armv8-timer"
- interrupts:4组 PPI 中断,对应4个软件中断号,实际根据运行模式选择其一。8个 CPU 共用同一中断号,但会各自产生中断。
- clock-frequency:时钟源计数频率为 19.2 MHz。
- always-on:计数器常开,不休眠。
出于虚拟化及安全执行等级考虑,ARMv8 为每个 CPU 核心提供了至少4种定时器,用于不同的执行环境:

这4种定时器与设备树中断号的对应关系如下:
- 中断号13:安全世界物理定时器
- 中断号14:非安全世界物理定时器
- 中断号11:虚拟定时器
- 中断号12:Hypervisor 定时器
虚拟定时器与物理定时器的区别在于,其 System counter 输入会在实际计数值上添加一个由 Host 控制的偏移量,从而实现不同 Guest 系统的无缝切换。
本地定时器初始化
ARM 平台的初始化代码位于 drivers/clocksource/arm_arch_timer.c。通过 TIMER_OF_DECLARE 宏,将设备树匹配字符串与初始化函数静态绑定。
初始化流程在内核启动路径中如下:
start_kernel
|--> time_init()
|--> timer_probe() // 根据定时器列表 __timer_of_table[] 依次初始化
|--> arch_timer_of_init() // 对应 “arm,armv8-timer” 的定时器初始化
|--> arch_timer_populate_kvm_info(); // 判断内核是否为 hyp 模式
|--> rate = arch_timer_get_cntfrq(); // 通过寄存器读取计数频率
|--> arch_timer_of_configure_rate(rate, np); // 通过设备树解析时钟源频率
|--> arch_timer_select_pp(); // 选择 PPI 中断号
|--> arch_timer_register(); // 注册 arch_timer 定时器中断
|--> alloc_percpu(struct clock_event_device); // 分配 per-cpu 定时器结构体
|--> request_percpu_irq(ppi, arch_timer_handler_virt, “arch_timer”, arch_timer_evt); // 为每 CPU 注册 PPI 中断
|--> arch_timer_cpu_pm_init(); // 注册电源管理通知器
|--> cpuhp_setup_state(arch_timer_starting_cpu, arch_timer_dying_cpu); // 注册 CPU 热插拔回调
总结来说,内核根据设备树配置和运行模式,选择注册对应的中断和处理函数,初始化 arch_timer 的功能函数指针,并最终向系统注册 ClockEvent device。
从开机日志中可以看到 arch_timer 的初始化情况:
[ 0.000000] arch_timer: cp15 and mmio timer(s) running at 19.20MHz (virt/virt).
系统启动早期只初始化 CPU0 的 arch_timer,其他 CPU 的定时器会在其启动时通过热插拔回调进行初始化。
运行时查看系统的 ClockEvent device:
adb shell head /sys/devices/system/clockevents/*/current_device
==> /sys/devices/system/clockevents/broadcast/current_device <==
arch_mem_timer
==> /sys/devices/system/clockevents/clockevent0/current_device <==
arch_sys_timer
...
通过 /proc/interrupts 查看,arch_timer 中断触发频率非常高,仅次于高频的 IPI 中断,可见其重要性。
时钟源初始化
初始化完定时器后,接下来会初始化时钟源。在 ARM 上称为 arch_counter,通过 clocksource 结构体描述:
static struct clocksource clocksource_counter = {
.name = “arch_sys_counter”,
.rating = 400,
.read = arch_counter_read,
.mask = CLOCKSOURCE_MASK(56),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
.rating = 400 表示其精度很高,是理想的时钟源。
.flags = CLOCK_SOURCE_IS_CONTINUOUS 表示连续计数。
软件初始化过程如下:
arch_timer_of_init();
|--> arch_timer_common_init()
|--> arch_counter_register() // 注册 arch_counter
|--> arch_timer_read_counter // 确定通过哪个寄存器读计数值
|--> clocksource_register_hz() // 向 clocksource 模块注册名为 “arch_sys_counter” 的时钟源
|--> timecounter_init() // 初始化 timecounter,较少用
|--> sched_clock_register() // 给调度程序注册读 cycle 函数
arch_counter 的核心是一个读寄存器函数 arch_timer_read_counter,最终会通过内联汇编读取 cntvct_el0 寄存器。该寄存器在特定条件下用户态也可访问,结合 VDSO 技术可实现用户空间高效读取系统时间。
从开机日志看,系统最终选择了 arch_sys_counter 作为时钟源:
[ 0.000000] clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x46d987e47, max_idle_ns: 440795202767 ns
[ 0.000000] sched_clock: 56 bits at 19MHz, resolution 52ns, wraps every 4398046511078ns
[ 0.075704] clocksource: Switched to clocksource arch_sys_counter
运行时查看当前时钟源:
adb shell cat /sys/devices/system/clocksource/clocksource0/current_clocksource
arch_sys_counter
时间维护
时间类型及接口
timekeeping 模块负责维护以下几种核心时间类型:
| 时间类型 |
宏定义 |
时间基准 |
特性 |
使用场景 |
| 真实时间/墙上时间 |
CLOCK_REALTIME |
1970年1月1日0时 |
世界标准时间(UTC) |
用户时间、安卓日志时间戳、网络传输 |
| 单调时间 |
CLOCK_MONOTONIC |
系统启动 |
系统运行时间,单调增加,休眠不增加,可能受 NTP 影响 |
定时器、内核日志时间戳 |
| 原始单调时间 |
CLOCK_MONOTONIC_RAW |
系统启动 |
系统运行时间,单调增加,休眠不增加,不受 NTP 影响 |
很少用 |
| 启动时间 |
CLOCK_BOOTTIME |
系统启动 |
系统上电时间,单调增加,休眠也会增加 |
ftrace 时间戳 |
| 原子时间 |
CLOCK_TAI |
1970年1月1日0时 |
国际原子时(TAI) |
很少用 |
| 节拍计数 |
jiffies |
系统启动 |
系统节拍累计计数,休眠不增加 |
低精度时间计时 |
timekeeping 维护一个主时间基数 base,每个节拍(tick)都会读取全局计数器的值来更新 base。各种类型的时间相对于 base 有一个 offset 值。
正常情况下只需更新 base,保持 offset 不变。当需要修改特定时间(如通过 RTC 或 settimeofday 更新实时时间)或系统从休眠唤醒时,才需要计算并更新相应时间的 offset。
由于 tick 触发频率相对较低(如 CONFIG_HZ=250 时为 4ms),直接使用 base + offset 精度不够。因此在真正获取系统时间时,会重新读取时钟源的 counter 值,计算出与 base 的纳秒级差值进行补偿,从而获得高精度时间。

timekeeping 提供了多种内核接口来获取不同精度和格式的时间,主要分为三类:
- 高精度版本:读取硬件计数器,纳秒级精度,速度较慢。
- 低精度版本:直接返回上次 tick 更新的
base 值,精度低,速度快。
- 高精度快速版本:精度与高精度版本相当,速度介于两者之间。

RTC 时间管理
RTC(实时时钟)在系统下电后依靠电池保持计时。系统上电后,RTC 驱动会通过 timekeeping 提供的 do_settimeofday64 接口来设置系统时间。
不同设备对 RTC 的使用策略不同。有的系统会直接将新时间回写 RTC;而有的系统则将 RTC 视为只读的计时起点,在用户文件中维护一个时间偏移量。下图展示了高通手机 ATS 时间管理机制,RTC 作为只读基准,时间偏移量由用户文件维护。

内核时间戳
内核中依赖时间戳的模块根据自身需求选择不同的时间获取方式:
printk:使用调度模块的 sched_clock() 接口,直接读取时钟源寄存器值。因此其时间戳在系统休眠时也会累积。
ftrace:默认使用启动时间(boot)作为时间戳,可通过 trace_clock 文件节点修改。
VDSO 系统调用加速
用户态程序频繁获取系统时间(如日志系统)如每次都陷入内核,开销很大。VDSO 机制允许用户空间程序在不进行系统调用的前提下,直接访问一些内核数据和函数。
内核将 timekeeping 维护的部分时间数据在 vdso.so 中创建一个 vdso_data 副本,并在每个 tick 更新时同步此副本。进程加载时会将 vdso.so 映射到自己的地址空间。进程通过 libc 接口读取 vdso_data 中的数据,并结合用户态也可访问的时钟源寄存器值,从而高效地获得高精度时间。

NTP 对时
NTP 对时并非简单地从服务器获取一个时间。其核心原理考虑了报文传输延迟。基本通信和计算模型如下:

客户端在 t1 时刻发送请求,服务器在 t2 时刻接收,在 t3 时刻发出回应,客户端在 t4 时刻收到。假设往返传输时间相同,记为 $t_s$。则客户端可以根据公式计算出准确的服务器时间 $t_5$:
$t_s = \frac{(t_4 - t_1) - (t_3 - t_2)}{2}$
$t_5 = t_3 + t_s$
高精度定时器
在现代 Linux 系统中,如果硬件支持高精度计数器,则会启用高精度定时器。每个 CPU 的 hrtimer_cpu_base 结构利用对应的 ClockEvent 设备来操控硬件定时器。
hrtimer 通过红黑树管理该 CPU 上所有的定时任务。每次定时器中断触发后,它从树中选择最早到期的任务执行,然后为下一个最早到期的任务设置新的硬件超时值。这意味着 hrtimer 本质上是“单次触发”的,周期性任务需要在每次触发后重新设定超时时间。
hrtimer 初始化与结构
hrtimer 初始化在系统启动时进行,首先初始化 CPU0 的 hrtimer_cpu_base 结构。每个 hrtimer_cpu_base 根据4种时间类型和2种中断执行环境(硬中断/软中断),分为8种 clock_base 来分别管理定时器。每个 clock_base 对应一棵红黑树。

定时器中断到来时,如果最近到期的任务是“硬” timer,则在当前中断上下文中处理;如果是“软” timer,则触发 HRTIMER_SOFTIRQ 软中断来处理。其他 CPU 会在启动时通过热插拔回调初始化自己的 hrtimer 结构。
使用示例
以下是一个在内核模块中使用 hrtimer 的示例,实现周期为 8.3ms 的定时打印:
static struct hrtimer timer; // 创建 hrtimer 定时器
// 定时器到期处理函数
static enum hrtimer_restart hrtimer_handler(struct hrtimer *hrt)
{
printk("hrtimer_handler");
hrtimer_forward_now(hrt, 8300000); // 将超时时间向后移8300000ns=8.3ms
return HRTIMER_RESTART; // 返回重新启动标志
}
static int __init hrtimer_test_init(void)
{
// 初始化 hrtimer,使用单调时间,硬中断模式
hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);
timer.function = hrtimer_handler; // 设置超时处理函数
// 启动定时器,8.3ms后首次触发
hrtimer_start(&timer, 8300000, HRTIMER_MODE_REL_HARD);
return 0;
}
static void __exit hrtimer_test_exit(void)
{
hrtimer_cancel(&timer); // 取消定时器
}
节拍定时器
在高精度时钟模式下,内核仍需周期性的 tick 中断来驱动调度等核心任务。为此,内核通过 hrtimer 模拟出一个周期性的 节拍定时器。
其初始化在系统切换到高精度模式时进行,创建并使用一个 hrtimer,将其超时时间设置为一个 tick 的时长(如 4ms)。每次超时后,在处理完调度、时间更新、低精度定时器检查等任务后,再次设定下一个 tick 的超时时间,从而模拟出周期性的心跳。
低精度定时器
低精度定时器基于传统的 timer_list 和 时间轮 算法管理,精度为 tick 级别。
时间轮算法
为了在 tick 到来时能快速查找大量到期的定时器,Linux 设计了时间轮管理方式。每个 CPU 的 timer_base 通过一个包含 576 个桶(bucket)的数组来组织定时器。
这些桶被分为 9 个等级。等级越高,相邻两个桶所代表的时间间隔(粒度)就越大。例如,Level 0 的粒度是 1 tick,Level 1 是 8 ticks,Level 2 是 64 ticks,以此类推。
插入定时器时,根据其超时时间与当前时间的差值,决定其应放入的等级。差值越大,放入的等级越高,精度损失也可能越大(因为多个接近的超时时间会被“折叠”到同一个桶中)。这种设计以牺牲长定时任务的精度为代价,换取了 O(1) 时间复杂度的插入和查找效率。

下表展示了 HZ=250 时各等级的粒度与定时范围:

看门狗定时器实例
看门狗定时器是低精度定时器的一个典型应用。软件 watchdog 线程以最高优先级运行,并通过低精度定时器设置周期性的“喂狗”任务(如每15.36秒)。
每次喂狗时间到,watchdog 线程会通过 IPI 中断 ping 所有其他 CPU。如果所有 CPU 都正常响应,则通过安全监控调用清除硬件看门狗计数器。如果某个 CPU 因长时间关中断而无法响应,watchdog 线程被阻塞,最终导致硬件看门狗触发系统恢复流程。
用户态定时器 API
nanosleep
nanosleep 基于 hrtimer 实现纳秒级延时。其核心是将调用线程设置为可中断的睡眠状态,然后调度出去。定时器到期后,通过中断唤醒该任务。
itimer
itimer 基于 hrtimer 和信号实现,提供三种定时方式:
| 定时类型 |
含义 |
到期信号 |
ITIMER_REAL |
真实时间定时 |
SIGALRM |
ITIMER_VIRTUAL |
进程在用户态的实际执行时间 |
SIGVTALRM |
ITIMER_PROF |
进程在用户态和内核态的实际执行时间 |
SIGPROF |
ITIMER_REAL 是常用的定时器。进程在 fork 时会初始化对应的 hrtimer,超时函数中会向进程发送 SIGALRM 信号。因此,一个进程内同时只能有一个线程使用 ITIMER_REAL。
另外两种 CPU 时间定时器的检查在每次 tick 事件处理时进行,通过 run_posix_cpu_timers 函数检查并发送对应信号。
alarm
alarm 是基于 itimer 的简化接口,以秒为单位进行定时,到期发送 SIGALRM 信号。
Posix Timer
Posix Timer 功能更强大,一个进程可创建多个定时器,并可指定到期信号和时钟类型。它提供了 timer_create、timer_settime、timer_gettime、timer_delete 等系统调用。
timerfd
timerfd 是基于文件描述符的定时器接口,直接由 hrtimer 驱动。它通过 timerfd_create 创建定时器并返回一个文件描述符,进程可以使用 select、poll、epoll 等 I/O 多路复用机制来监听该文件描述符的可读事件,从而实现异步定时通知。
以上就是 Linux 时间子系统从硬件基础到用户接口的全栈解析。理解这套复杂而精密的机制,对于进行内核开发、驱动调试或构建高性能应用都至关重要。如果你对底层技术原理有浓厚的兴趣,欢迎在云栈社区的计算机基础板块深入探讨。