找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5061

积分

0

好友

701

主题
发表于 4 天前 | 查看: 26| 回复: 0

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

Linux时间子系统软件架构图

整个子系统从功能上可分为“定时”与“计时”两大块。定时部分负责在特定时刻触发中断事件;而计时部分则专注于记录和跟踪现实世界的时间线。

定时部分:系统有一个全局计数器(counter),每个 CPU 核心则配备一个硬件定时器(timer)。定时器内置比较器,当预设值与全局计数器值匹配时便触发中断。在软件层面,每个 CPU 的定时器被抽象为时钟事件设备。在高精度模式下,每个时钟事件设备会关联一个本地高精度定时器(hrtimer)管理结构。hrtimer 基于事件触发,使用红黑树来高效管理该 CPU 上所有类型的软件定时任务。每次执行完到期任务后,它都会选取下一个最早到期的任务来设定下次中断时刻。

基于 hrtimer,内核进一步封装了多种类型和精度的软件定时器。例如,为了方便内核调度,定义了节拍定时器作为系统“心跳”,驱动任务调度、负载计算等。为了方便用户空间使用,则提供了 posix-timeralarmtimer_fdnanosleepitimer 等接口。

计时部分:全局计数器在软件上被抽象为时钟源设备。其特点是计数频率高、精度高,且在系统休眠时仍能持续工作,可以通过寄存器高效读取当前计数值。timekeeping 模块作为 Linux 计时维护的核心,它利用时钟源提供持续不断的高精度计时,并依靠 RTC 或网络 NTP 提供真实世界的时间基准,从而维护各种系统时间的准确与可靠。

timekeeping 不仅为内核模块提供了丰富的时间获取接口,也封装了大量系统调用供用户空间使用。特别值得一提的是,它通过 VDSO 技术,允许用户空间程序绕过系统调用,以极高的效率获取系统时间。此外,任务调度、printk/ftrace 时间戳等功能也依赖于时钟源提供的高精度计时。

定时器与时钟源初始化

ARMv8 架构文档描述了其通用定时器(Generic Timer)的典型结构,如下图所示。其中 System counter 是位于“始终供电域”的全局计数器,确保系统休眠时也能正确计数。Timer_x 是 CPU 本地的定时器,每个处理单元(PE)至少有一个专属定时器。所有本地定时器都以 System counter 作为时钟源,共享其计数值以保证时间同步。本地定时器通过中断控制器,向 CPU 发起 PPI(私有外设中断)。

ARMv8 SoC多处理器系统定时器架构图

解析设备树

以高通 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种定时器,用于不同的执行环境:

ARM通用计时器提供的四种定时器类型

这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 的纳秒级差值进行补偿,从而获得高精度时间。

ARM64架构下vDSO结构与时间获取示意图

timekeeping 提供了多种内核接口来获取不同精度和格式的时间,主要分为三类:

  1. 高精度版本:读取硬件计数器,纳秒级精度,速度较慢。
  2. 低精度版本:直接返回上次 tick 更新的 base 值,精度低,速度快。
  3. 高精度快速版本:精度与高精度版本相当,速度介于两者之间。

Linux内核时间获取函数分类表

RTC 时间管理

RTC(实时时钟)在系统下电后依靠电池保持计时。系统上电后,RTC 驱动会通过 timekeeping 提供的 do_settimeofday64 接口来设置系统时间。

不同设备对 RTC 的使用策略不同。有的系统会直接将新时间回写 RTC;而有的系统则将 RTC 视为只读的计时起点,在用户文件中维护一个时间偏移量。下图展示了高通手机 ATS 时间管理机制,RTC 作为只读基准,时间偏移量由用户文件维护。

高通ATS时间同步系统架构图

内核时间戳

内核中依赖时间戳的模块根据自身需求选择不同的时间获取方式:

  • printk:使用调度模块的 sched_clock() 接口,直接读取时钟源寄存器值。因此其时间戳在系统休眠时也会累积。
  • ftrace:默认使用启动时间(boot)作为时间戳,可通过 trace_clock 文件节点修改。

VDSO 系统调用加速

用户态程序频繁获取系统时间(如日志系统)如每次都陷入内核,开销很大。VDSO 机制允许用户空间程序在不进行系统调用的前提下,直接访问一些内核数据和函数。

内核将 timekeeping 维护的部分时间数据在 vdso.so 中创建一个 vdso_data 副本,并在每个 tick 更新时同步此副本。进程加载时会将 vdso.so 映射到自己的地址空间。进程通过 libc 接口读取 vdso_data 中的数据,并结合用户态也可访问的时钟源寄存器值,从而高效地获得高精度时间。

ARM64架构下vDSO详细解剖图

NTP 对时

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 对应一棵红黑树。

Linux内核hrtimer红黑树组织架构图

定时器中断到来时,如果最近到期的任务是“硬” 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) 时间复杂度的插入和查找效率。

CPU定时器时间轮层级结构示意图

下表展示了 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_createtimer_settimetimer_gettimetimer_delete 等系统调用。

timerfd

timerfd 是基于文件描述符的定时器接口,直接由 hrtimer 驱动。它通过 timerfd_create 创建定时器并返回一个文件描述符,进程可以使用 selectpollepoll 等 I/O 多路复用机制来监听该文件描述符的可读事件,从而实现异步定时通知。

以上就是 Linux 时间子系统从硬件基础到用户接口的全栈解析。理解这套复杂而精密的机制,对于进行内核开发、驱动调试或构建高性能应用都至关重要。如果你对底层技术原理有浓厚的兴趣,欢迎在云栈社区的计算机基础板块深入探讨。




上一篇:深度解析 Linux 中断机制:从硬件 GICv3 到内核实现与工作队列
下一篇:高级电子硬件工程师招聘:ARM架构与高速PCB设计,月薪20-40K,加入pamir.ai Pocket Linux Agent团队
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 18:14 , Processed in 0.698981 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表