深入解析 Linux 内核 clocksource 框架:从 jiffies 到通用时钟源管理(linux-insides 时间管理系列之二)

发布时间:2026/10/1 2:44:59
深入解析 Linux 内核 clocksource 框架:从 jiffies 到通用时钟源管理(linux-insides 时间管理系列之二) 文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载导读本文是 linux-insides 仓库 Timers 章节 的第二部分围绕 Linux 内核时间管理中的clocksource框架展开。上一篇 linux-timers-1.md 引入了jiffies与clocksource两个核心概念本篇将系统拆解clocksource框架的完整脉络先复习jiffies的典型用法再剖析clocksource结构体的全部字段随后跟踪一个新时钟源从mult/shift换算参数计算、按评级入队、看门狗挂载到最佳时钟源切换的完整注册链路最后演示通过 sysfs 接口在运行时查询与解绑时钟源。读完本文你将能够从源码级理解 Linux 内核为什么需要独立的时钟源抽象层以及一个时钟源是如何被注册、评分、选中和暴露给用户空间的。回顾jiffies 及其在内核中的典型用法jiffies是定义于 include/linux/jiffies.h 对应的内核头文件中的全局变量它在每次定时器中断时递增记录了系统启动以来发生的tick总数。只要知道定时器中断频率即编译期常量HZ就能把jiffies换算成人类时间单位。HZ的值等于CONFIG_HZ内核配置选项的值在 x86_64 架构的默认配置中CONFIG_HZ_1000y这意味着 x86_64 下CONFIG_HZ默认为1000。因此用jiffies / HZ即可得到自内核启动以来经过的秒数也就是系统 uptime运行时长。由于HZ表示每秒的定时器中断次数我们可以用它方便地设置未来的某个时刻这是内核中极为常见的惯用法/* one minute from now */ unsigned long later jiffies 60*HZ; /* five minutes from now */ unsigned long later jiffies 5*60*HZ;内核里大量超时逻辑都建立在这种jiffies N*HZ的写法上。例如在 arch/x86/kernel/smpboot.c 对应的do_boot_cpu函数中内核等待应用处理器AP应答时设置了十秒超时if (!boot_error) { timeout jiffies 10*HZ; while (time_before(jiffies, timeout)) { ... ... ... udelay(100); } ... ... ... }这里把jiffies 10*HZ赋给timeout含义就是十秒超时随后进入循环用time_before宏比较当前jiffies与超时点。再如 sound/isa/sscape.c 中 Ensoniq Soundscape Elite 声卡驱动的obp_startup_ack函数它等待板载处理器返回启动确认序列支持任意毫秒级超时static int obp_startup_ack(struct soundscape *s, unsigned timeout) { unsigned long end_time jiffies msecs_to_jiffies(timeout); do { ... ... ... x host_read_unsafe(s-io_base); ... ... ... if (x 0xfe || x 0xff) return 1; msleep(10); } while (time_before(jiffies, end_time)); return 0; }注意这里使用了msecs_to_jiffies辅助函数把毫秒换算成jiffies这正是jiffies接口灵活性的体现它既是当前时刻也是可参与加减运算的超时基准。HZ的具体取值决定了定时器中断的密度。下图展示了CONFIG_HZ在内核配置工具menuconfig中的可选项100/250/300/1000 Hzjiffies虽然无处不在但它的分辨率受限于定时器中断频率。HZ的合理范围是100到1000Hz因此标准jiffies接口只能保证110毫秒的分辨率。上一篇还介绍了基于 i8253/i8254 可编程间隔定时器 tick 率约1193182Hz的refined_jiffies时钟源可提供约1微秒的分辨率。而当前 Linux 内核中纳秒才是时钟源时间值单位的首选。正是这种分辨率诉求不断升级的现实催生了更通用的抽象层——clocksource框架。为什么需要 clocksource 框架clocksource概念代表 Linux 内核中时钟源管理的通用 API。内核为什么需要单独的框架来管理它因为时间测量手段的精度是硬件相关的。内核获取时间信息有两个基本来源实时时钟RTC非易失性设备架构无关的 RTC 驱动位于内核 drivers/rtc 目录各架构还可提供架构相关的 RTC 驱动例如 x86 的CMOS/RTCarch/x86/kernel/rtc.c。系统定时器以周期性频率触发中断的定时器。对 IBM PC 兼容机而言经典代表是可编程间隔定时器PIT。问题在于不同架构拥有各自的定时器硬件甚至同一架构如 x86也同时存在时间戳计数器TSC频率可等于处理器主频、高精度事件定时器HPET至少 10 MHz 的 64 位计数器等多种时钟源。早期每个架构都各自实现一套时间管理代码导致大量相似实现被反复复制。clocksource框架正是为解决这一问题而生的抽象层它将大量 timekeeping 代码移入架构无关的公共部分架构相关部分被压缩为定义并管理低层硬件时钟源框架要求每个时钟源以单调递增的值表示时间当前首选纳秒框架允许系统配置与运行时在多个硬件时钟设备之间选择、访问和缩放不同的时钟源。一句话概括clocksource框架让内核摆脱为每种硬件重写时间管理的困境把差异化收敛到一个个可注册、可评级、可替换的时钟源对象上。clocksource 结构体框架的基石clocksource框架的基础是定义于 include/linux/clocksource.h 的clocksource结构体。上一篇已瞥见其中若干字段这里给出完整定义并逐一剖析struct clocksource { cycle_t (*read)(struct clocksource *cs); cycle_t mask; u32 mult; u32 shift; u64 max_idle_ns; u32 maxadj; #ifdef CONFIG_ARCH_CLOCKSOURCE_DATA struct arch_clocksource_data archdata; #endif u64 max_cycles; const char *name; struct list_head list; int rating; int (*enable)(struct clocksource *cs); void (*disable)(struct clocksource *cs); unsigned long flags; void (*suspend)(struct clocksource *cs); void (*resume)(struct clocksource *cs); #ifdef CONFIG_CLOCKSOURCE_WATCHDOG struct list_head wd_list; cycle_t cs_last; cycle_t wd_last; #endif struct module *owner; } ____cacheline_aligned;读取与换算字段read、mask、mult、shiftread是指向读取函数的指针返回框架挑选出的最佳计数器值。例如读取jiffies的clocksource_jiffiesstatic struct clocksource clocksource_jiffies { ... .read jiffies_read, ... }其中jiffies_read只是简单返回jiffiesstatic cycle_t jiffies_read(struct clocksource *cs) { return (cycle_t) jiffies; }读取时间戳计数器TSC的clocksource_tsc则使用read_tscstatic struct clocksource clocksource_tsc { ... .read read_tsc, ... };mask用于保证非 64 位计数器之间的减法运算无需特殊溢出逻辑。例如jiffies的掩码是0xffffffff32 位这意味着jiffy在约 42 秒后回绕归零但内核的time_before/time_after宏依赖这种回绕语义正常比较。mult与shift是时钟源换算的核心它们把计数器抽象的机器时间单位转换为纳秒。换算公式为((u64) cycles * mult) shift;即先乘mult再右移shift。以标准jiffies时钟源为例其mult NSEC_PER_JIFFY JIFFIES_SHIFTshift JIFFIES_SHIFT其中NSEC_PER_JIFFY与JIFFIES_SHIFT都随HZ取值变化——可见mult/shift与中断频率HZ深度绑定。空闲与精度保护字段max_idle_ns、maxadj、max_cyclesmax_idle_ns64 位表示时钟源允许的最大空闲时间纳秒。该字段在启用CONFIG_NO_HZ时至关重要动态 tick 允许内核休眠超过单个 tick 的时长甚至理论上可无限休眠max_idle_ns就是这个休眠上限。maxadj是mult的最大调整值。上述换算公式并非 100% 精确实际取的是尽可能接近纳秒的数值maxadj帮助修正误差并让 clocksource API 避免mult在被调整后发生溢出。max_cycles表示在潜在溢出之前可安全乘算的最大周期值。生命周期回调enable、disable、suspend、resume这四个函数指针分别负责enable—— 可选启用时钟源disable—— 可选禁用时钟源suspend—— 时钟源的挂起函数resume—— 时钟源的恢复函数。架构相关与看门狗字段archdata、wd_list、cs_last、wd_lastarchdata类型arch_clocksource_data受CONFIG_ARCH_CLOCKSOURCE_DATA控制目前仅对 x86 和 IA64 架构生效存放架构特定数据。例如 x86 用它记录 vDSO 时钟模式struct arch_clocksource_data { int vclock_mode; };其中vclock_mode可取以下值之一#define VCLOCK_NONE 0 #define VCLOCK_TSC 1 #define VCLOCK_HPET 2 #define VCLOCK_PVCLOCK 3wd_list、cs_last、wd_last三个字段受CONFIG_CLOCKSOURCE_WATCHDOG控制包含看门狗watchdog相关数据。看门狗是用于检测计算机故障并从中恢复的定时器在内核源码中只有 arch/x86/Kconfig 配置了该选项。为什么 x86/x86_64 需要它所有 x86 处理器都有一个特殊的 64 位寄存器——时间戳计数器它保存自复位以来的周期数但 TSC 有时需要对照另一个时钟源进行校验。本部分暂不深入看门狗定时器的初始化待掌握更多定时器知识后再展开。标识与管理字段name、list、rating、flags、ownername时钟源的名称字符串list用于把时钟源链入全局clocksource_list链表rating时钟源的评级帮助内核挑选最佳时钟源。评级区间约定如下评级范围含义1-99仅适用于启动与测试目的100-199可实际使用但非理想选择200-299正确且可用的时钟源300-399相当快速且准确的时钟源400-499理想时钟源可用时必选例如 TSC 的评级为300HPET 的评级为250标准jiffies仅为1最低共同分母保证所有系统可用refined_jiffies为2。flags描述计数器不同属性的标志位owner指向拥有该时钟源的可加载内核模块的引用。至此clocksource结构体的所有字段都已梳理完毕这为理解框架的注册与选择机制打下了基础。新时钟源的注册三条 API 殊途同归上一篇见过的__clocksource_register定义于 include/linux/clocksource.h其实现只是对__clocksource_register_scale的一次调用static inline int __clocksource_register(struct clocksource *cs) { return __clocksource_register_scale(cs, 1, 0); }clocksource框架还提供了另外两个面向不同频率单位的注册辅助函数static inline int clocksource_register_hz(struct clocksource *cs, u32 hz) { return __clocksource_register_scale(cs, 1, hz); } static inline int clocksource_register_khz(struct clocksource *cs, u32 khz) { return __clocksource_register_scale(cs, 1000, khz); }三个函数都返回__clocksource_register_scale的结果只是参数集不同。以clocksource_register_khz的三个参数为例cs—— 要安装的时钟源scale—— 时钟源的缩放因子。换句话说将该参数乘以频率就得到时钟源的hzfreq—— 除以scale之后的时钟源频率。也就是说clocksource_register_hz按赫兹注册clocksource_register_khz按千赫兹注册而__clocksource_register以频率为0调用——这用于那些自行定义mult/shift的时钟源如jiffies系列框架无需再替它们计算换算参数。__clocksource_register_scale 的实现链路__clocksource_register_scale定义于 kernel/time/clocksource.c核心实现如下int __clocksource_register_scale(struct clocksource *cs, u32 scale, u32 freq) { __clocksource_update_freq_scale(cs, scale, freq); mutex_lock(clocksource_mutex); clocksource_enqueue(cs); clocksource_enqueue_watchdog(cs); clocksource_select(); mutex_unlock(clocksource_mutex); return 0; }流程分两段先更新时钟源频率计算换算参数再在互斥锁保护下完成入队、看门狗挂载与最佳源选择。第一步__clocksource_update_freq_scale 计算 mult/shift该函数同样位于 kernel/time/clocksource.c。首先检查传入频率是否非零——如果是零如__clocksource_register(cs, 1, 0)的情况说明时钟源自带mult/shift无需计算否则需要为给定时钟源推导mult和shiftvoid __clocksource_update_freq_scale(struct clocksource *cs, u32 scale, u32 freq) { u64 sec; if (freq) { sec cs-mask; do_div(sec, freq); do_div(sec, scale); if (!sec) sec 1; else if (sec 600 cs-mask UINT_MAX) sec 600; clocks_calc_mult_shift(cs-mult, cs-shift, freq, NSEC_PER_SEC / scale, sec * scale); } ... ... ... }这段代码的核心是计算计数器溢出前能安全运行的最大秒数用cs-mask时钟源有效位的最大比特数填充sec第一次do_div(sec, freq)mask / freq得到该时钟源在每秒freq次 tick 下的最大秒数第二次do_div(sec, scale)再除以缩放因子1Hz 或10^3Hz 千赫兹得到按缩放因子修正后的最大秒数随后对结果做钳制若为0则置1若超过600且mask大于UINT_MAX则置600——这两个值代表时钟源允许的最大休眠秒数调用clocks_calc_mult_shift计算出cs-mult和cs-shift其输入包括频率、每缩放单位纳秒数NSEC_PER_SEC / scale与最大秒数。函数末尾还会校验刚算出的mult在调整后不会溢出更新max_idle_ns与max_cycles即可转换为时钟源计数器的最大纳秒数并把结果打印到内核缓冲区pr_info(%s: mask: 0x%llx max_cycles: 0x%llx, max_idle_ns: %lld ns\n, cs-name, cs-mask, cs-max_cycles, cs-max_idle_ns);这段输出可在dmesg中直接观察到$ dmesg | grep clocksource: [ 0.000000] clocksource: refined-jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 1910969940391419 ns [ 0.000000] clocksource: hpet: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 133484882848 ns [ 0.094084] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 1911260446275000 ns [ 0.205302] clocksource: acpi_pm: mask: 0xffffff max_cycles: 0xffffff, max_idle_ns: 2085701024 ns [ 1.452979] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x7350b459580, max_idle_ns: 881591204237 ns从输出可见不同时钟源的差异jiffies/refined-jiffies/hpet是 32 位掩码0xffffffffacpi_pm是 24 位掩码0xffffff而tsc是完整 64 位掩码0xffffffffffffffff对应更大的max_cycles与更长的空闲预算。第二步互斥保护下的入队、看门狗与选择__clocksource_update_freq_scale完成后回到__clocksource_register_scale注册新时钟源mutex_lock(clocksource_mutex); clocksource_enqueue(cs); clocksource_enqueue_watchdog(cs); clocksource_select(); mutex_unlock(clocksource_mutex);clocksource_mutex互斥锁的作用是保护两个全局状态当前选中的curr_clocksource变量以及包含所有已注册时钟源的clocksource_list链表。clocksource_enqueue遍历clocksource_list中所有已注册时钟源按评级rating从高到低找到插入位置/* * Enqueue the clocksource sorted by rating */ static void clocksource_enqueue(struct clocksource *cs) { struct list_head *entry clocksource_list; struct clocksource *tmp; list_for_each_entry(tmp, clocksource_list, list) { /* Keep track of the place, where to insert */ if (tmp-rating cs-rating) break; entry tmp-list; } list_add(cs-list, entry); }clocksource_enqueue_watchdog与前者类似但根据时钟源标志把新时钟源插入wd_list并按需启动新的看门狗定时器。看门狗相关细节留待后续部分展开。clocksource_select如其名从已注册时钟源中选出最佳者static void clocksource_select(void) { return __clocksource_select(false); }__clocksource_select的布尔参数决定如何遍历clocksource_list此处传false表示遍历全部条目。由于clocksource_enqueue保证最佳评级的时钟源排在链表最前框架可以轻松取到它然后切换到该源if (curr_clocksource ! best !timekeeping_notify(best)) { pr_info(Switched to clocksource %s\n, best-name); curr_clocksource best; }切换结果同样出现在dmesg中$ dmesg | grep Switched [ 0.199688] clocksource: Switched to clocksource hpet [ 2.452966] clocksource: Switched to clocksource tsc注意同一台机器上出现了两个被切换的时钟源本例为hpet和tsc。确实如此特定硬件上可能同时存在多个时钟源内核知晓全部已注册源并在每次注册新源后若其评级更优则切换过去——这正是rating机制的价值所在。sysfs 接口运行时查看与解绑时钟源kernel/time/clocksource.c 文件底部还提供了 sysfs 接口主要初始化工作位于init_clocksource_sysfs函数它作为设备initcall被调用static struct bus_type clocksource_subsys { .name clocksource, .dev_name clocksource, }; static int __init init_clocksource_sysfs(void) { int error subsys_system_register(clocksource_subsys, NULL); if (!error) error device_register(device_clocksource); if (!error) error device_create_file( device_clocksource, dev_attr_current_clocksource); if (!error) error device_create_file(device_clocksource, dev_attr_unbind_clocksource); if (!error) error device_create_file( device_clocksource, dev_attr_available_clocksource); return error; } device_initcall(init_clocksource_sysfs);首先通过subsys_system_register注册clocksource子系统执行后系统将出现如下目录$ pwd /sys/devices/system/clocksource接着注册device_clocksource设备static struct device device_clocksource { .id 0, .bus clocksource_subsys, };并创建三个属性文件dev_attr_current_clocksource—— 当前系统使用的时钟源dev_attr_unbind_clocksource—— 允许解绑unbind时钟源的接口dev_attr_available_clocksource—— 系统中可用的时钟源列表。init_clocksource_sysfs执行后即可在用户空间查询时钟源信息。查看可用时钟源$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource tsc hpet acpi_pm查看当前时钟源$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource tsc这两个命令是排查时钟源选择问题的第一手工具先看系统有哪些源可选再看当前用的是哪一个必要时可通过unbind_clocksource接口解绑有问题的源促使内核切换到备选源。小结上一篇认识了jiffies与clocksource两个概念本篇则完成了从用法到机制的跨越先通过do_boot_cpu、obp_startup_ack等实例掌握jiffies超时编程范式再完整剖析clocksource结构体的每个字段读取、掩码、换算、空闲预算、架构数据、看门狗与生命周期回调最后沿__clocksource_register_scale → __clocksource_update_freq_scale → clocksource_enqueue → clocksource_select这条链路看清了计算换算参数 → 按评级入队 → 挂载看门狗 → 选择并切换最佳源的完整注册流程并通过 sysfs 接口演示了运行时查询与解绑时钟源的方法。当然clocksource框架的 API 不止于此例如clocksource_unregister可从clocksource_list中移除给定时钟源但本文聚焦的核心链路已经足以支撑读者继续深入。看门狗定时器的初始化、动态 tick 以及更丰富的定时器机制将在此后的 linux-timers-3.mdtick broadcast 与 dyntick、linux-timers-5.mdclockevents 框架、linux-timers-6.mdx86 相关时钟源等部分继续展开感兴趣的读者可按 Timers/README.md 的章节索引顺序阅读。赞分享文档教程操作系统【免费下载链接】linux-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载相关推荐Linux 内核定时器与时间管理深入解析 clocksource 框架Timers/linux-timers-2 精读Linux 内核定时器与时间管理深入解析 clocksource 框架Timers/linux timers 2 精读 本文是《Linux 内核揭秘》lLinux 内核揭秘定时器与时间管理一——jiffies 与 clocksource 的初始化之旅Linux 内核揭秘定时器与时间管理一——jiffies 与 clocksource 的初始化之旅 导读 本文是《Linux 内核揭秘》linux in深入 Linux 内核定时器与时间管理一jiffies 与 clocksource 的初始化与实战使用深入 Linux 内核定时器与时间管理一jiffies 与 clocksource 的初始化与实战使用 本文基于 linux insides 项目 Tim文档教程操作系统上一篇gogcli 的 gog time 命令终端中的本地时间与多时区处理指南下一篇wagmi Tempo amm.useBurn Hook 实战指南销毁流动性代币并取回底层代币对创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考