- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides-zh
Linux 内核揭秘
本文是《Linux 内核揭秘》系列"定时器和时钟管理"章节的第一篇。它沿着内核启动路径,从setup_arch中最早的时钟初始化代码出发,系统讲解jiffies全局时钟节拍变量与clocksource时钟源抽象两大基础概念,并深入register_refined_jiffies的实现细节。读完本文,你将能理解 Linux 内核如何感知"时间"、jiffies与HZ的换算关系,以及内核为什么需要clocksource这一层抽象来解决不同硬件计时器差异的问题。
引言:内核为什么离不开定时器
定时器与时间管理在 Linux 内核中无处不在:TCP 协议栈的各种超时、内核感知当前时间、调度异步函数、调度下一次事件中断……几乎所有子系统都依赖某种时间度量手段。正因如此,理解内核的时间管理机制,是读懂内核其余部分的重要前置知识。
本章将从内核启动最早期的时间相关初始化入手,逐步剖析不同类型的定时器及其在内核子系统中的使用方式。在之前的初始化章节中我们沿内核启动路径分析过初始化流程,但当时略过了一些细节——其中就包括定时器的初始化,本文正是要补上这一课。
起点:setup_arch中的时间管理代码
内核解压完成后(详见内核解压),架构无关的通用代码开始工作,入口位于 init/main.c。在完成了 lock validator 初始化、cgroups 初始化以及设置 canary 值之后,内核调用了setup_arch函数。
setup_arch负责准备和初始化架构相关的内容:为 bss 段预留空间、为 initrd 预留空间、解析内核命令行等等。在这个庞大的函数中,我们可以找到若干与时间管理相关的调用——初始化章节的第七部分在收尾setup_arch分析时也曾提到,在保留标准 I/O 资源、初始化机器检查异常之后,就会调用注册 jiffy 的register_refined_jiffies,并注明"内核定时器将有专门章节讨论"——本文就是那个专门章节的展开。
setup_arch中第一个时间相关的调用是:
x86_init.timers.wallclock_init();x86_init结构与平台初始化钩子
x86_init结构体包含指向不同平台(如 Intel MID、Intel CE4100 等)默认设置函数的指针,定义于 arch/x86/kernel/x86_init.c,默认情况下它描述标准 PC 硬件。其类型为x86_init_ops,提供一组平台特定设置函数(保留标准资源、平台内存设置、中断处理器初始化等):
struct x86_init_ops { struct x86_init_resources resources; struct x86_init_mpparse mpparse; struct x86_init_irqs irqs; struct x86_init_oem oem; struct x86_init_paging paging; struct x86_init_timers timers; struct x86_init_iommu iommu; struct x86_init_pci pci; };注意其中的timers字段,类型为x86_init_timers,顾名思义与时间管理和定时器直接相关。它包含四个返回void的函数指针:
| 字段 | 作用 |
|---|---|
setup_percpu_clockev | 为启动 CPU 设置每 CPU 时钟事件设备 |
tsc_pre_init | 在 TSC(时间戳计数器)初始化之前调用的平台函数 |
timer_init | 初始化平台定时器 |
wallclock_init | 初始化墙钟(wallclock)设备 |
在标准 PC 硬件上,x86_init结构体中wallclock_init指向一个空操作函数:
struct x86_init_ops x86_init __initdata = { ... .timers = { .wallclock_init = x86_init_noop, }, ... }而x86_init_noop就是什么都不做的函数:
void __cpuinit x86_init_noop(void) { }Intel MID 平台的墙钟初始化
wallclock_init真正的使用者是 Intel MID(Mobile Internet Device)平台。它的初始化发生在x86_intel_mid_early_setup函数中:
void __init x86_intel_mid_early_setup(void) { ... x86_init.timers.wallclock_init = intel_mid_rtc_init; ... }intel_mid_rtc_init的实现相当简洁:先解析 Simple Firmware Interface(SFI)的 M-Real-Time-Clock 表,把设备信息填入sfi_mrtc_array,随后初始化set_time与get_time函数:
void __init intel_mid_rtc_init(void) { unsigned long vrtc_paddr; sfi_table_parse(SFI_SIG_MRTC, NULL, NULL, sfi_parse_mrtc); vrtc_paddr = sfi_mrtc_array[0].phys_addr; if (!sfi_mrtc_num || !vrtc_paddr) return; vrtc_virt_base = (void __iomem *)set_fixmap_offset_nocache(FIX_LNW_VRTC, vrtc_paddr); x86_platform.get_wallclock = vrtc_get_time; x86_platform.set_wallclock = vrtc_set_mmss; }之后,基于 Intel MID 的设备就能从硬件时钟读取时间了。而标准 PC 的 x86_64 架构则不需要这种处理——x86_init_noop直接返回。看完了 Intel MID 平台的实时时钟初始化,我们回到通用的 x86_64 架构,继续追踪时间管理相关内容。
认识 jiffies
回到setup_arch,紧接着的时间管理相关调用是:
register_refined_jiffies(CLOCK_TICK_RATE);在分析这个函数之前,必须先理解jiffy(瞬时)这个概念。维基百科对 jiffy 的定义是"任何不指定长短的短暂时间段的非正式说法",Linux 内核中的 jiffy 与之类似。内核中有一个全局变量jiffies,保存系统启动以来发生的 tick 数量,初始化时被置为零:
extern unsigned long volatile __jiffy_data jiffies;每次定时器中断发生时,该全局变量都会递增。在jiffies附近还有一个相似的定义:
extern u64 jiffies_64;实际上内核中只有其中一个变量真正被使用,具体取决于处理器类型:x86_64 使用u64版本,x86(32 位)使用unsigned long。这一点可以从 x86 的链接脚本 arch/x86/kernel/vmlinux.lds.S 中看出:
#ifdef CONFIG_X86_32 ... jiffies = jiffies_64; ... #else ... jiffies_64 = jiffies; ... #endif在 x86_32 情况下,jiffies是jiffies_64变量的低 32 位。示意如下:
jiffies_64 +-----------------------------------------------------+ | | | | | | | | jiffies on `x86_32` | | | | | | | +-----------------------------------------------------+ 63 31 0为什么需要 clocksource:硬件计时器差异问题
register_refined_jiffies是通用内核代码,定义在 kernel/time/jiffies.c,没有架构特定实现。它的核心目的是注册 jiffy 时钟源。在深入函数实现前,先要弄清clocksource是什么。内核注释将其描述为"自由运行计数器的硬件抽象"。
clocksource的本质是时间保持(timekeeping)抽象——用最直白的话说,它向内核提供一个时间值。我们已经知道jiffies接口表示系统启动以来的 tick 数,且每次定时器中断递增。既然内核可以用jiffies度量时间,为什么还需要单独的clocksource概念?
答案在于:不同硬件设备提供的时钟源能力差异很大,更精确的时间间隔测量技术是硬件相关的。例如 x86 芯片上有 64 位计数器 TSC(时间戳计数器),其频率可等于处理器频率;又例如 HPET(高精度事件定时器),包含一个至少 10 MHz 频率的 64 位计数器。两者都是 x86 的定时器,能力却大不相同;若再加上其他架构的定时器,问题只会更复杂。clocksource概念正是为解决这个问题而生。
clocksource概念在内核中用clocksource结构体表示,定义于 include/linux/clocksource.h。它包含描述时间计数器的若干字段,如计数器名称name、描述计数器属性的flags、指向suspend/resume函数的指针等等。
clocksource_jiffies结构剖析
来看 jiffies 的 clocksource 结构体(同样定义于 kernel/time/jiffies.c):
static struct clocksource clocksource_jiffies = { .name = "jiffies", .rating = 1, .read = jiffies_read, .mask = 0xffffffff, .mult = NSEC_PER_JIFFY << JIFFIES_SHIFT, .shift = JIFFIES_SHIFT, .max_cycles = 10, };各字段含义如下:
name:默认名称jiffies。rating:评分,供时钟源管理代码为指定硬件挑选最佳的已注册时钟源。rating 的取值分档:
| 分值范围 | 含义 |
|---|---|
| 1-99 | 仅用于启动和测试目的 |
| 100-199 | 可用于实际使用,但并非理想选择 |
| 200-299 | 正确可用的时钟源 |
| 300-399 | 相当快速且精确的时钟源 |
| 400-499 | 理想的时钟源,可用时必选 |
例如 TSC 的 rating 是 300,HPET 的 rating 是 250。
read:指向读取时钟源周期值的函数指针。对 jiffies 而言,它只是以cycle_t类型返回jiffies变量:
static cycle_t jiffies_read(struct clocksource *cs) { return (cycle_t) jiffies; }其中cycle_t就是 64 位无符号类型:
typedef u64 cycle_t;mask:确保非 64 位计数器的计数值相减时无需特殊溢出逻辑。这里 mask 是0xffffffff(32 位),意味着 jiffy 每经过 42 秒左右就会回绕到零:
>>> 0xffffffff 4294967295 # 42 nanoseconds >>> 42 * pow(10, -9) 4.2000000000000006e-08 # 43 nanoseconds >>> 43 * pow(10, -9) 4.3e-08mult与shift:用于把时钟源的周期换算成"每周期纳秒数"。当内核调用clocksource.read时返回的是cycle_t类型的机器时间单位,要转换成纳秒就需要这两个字段。clocksource提供了clocksource_cyc2ns函数完成换算,核心表达式为:
((u64) cycles * mult) >> shift;其中mult默认等于:
NSEC_PER_JIFFY << JIFFIES_SHIFT #define NSEC_PER_JIFFY ((NSEC_PER_SEC+HZ/2)/HZ) #define NSEC_PER_SEC 1000000000Lshift则按 HZ 取值分档:
#if HZ < 34 #define JIFFIES_SHIFT 6 #elif HZ < 67 #define JIFFIES_SHIFT 7 #else #define JIFFIES_SHIFT 8 #endifHZ 与 CONFIG_HZ:系统定时器频率
jiffies时钟源用NSEC_PER_JIFFY乘数指定"每周期纳秒比"。注意JIFFIES_SHIFT和NSEC_PER_JIFFY的值都依赖HZ——HZ表示系统定时器的频率,定义于 include/asm-generic/param.h,取决于CONFIG_HZ内核配置选项。不同架构的 HZ 值不同,x86 上定义为:
#define HZ CONFIG_HZCONFIG_HZ可以在内核配置菜单中选取,可选值如下:
从上图可以看到CONFIG_HZ的四个候选值:100、250、300 和 1000 HZ(图中选中的是 250 HZ)。也就是说,本例中定时器中断频率为 250 HZ——每秒发生 250 次中断,即每 4ms 一次定时器中断。
max_cycles:结构体最后一个字段,保存可以安全相乘而不至于溢出的最大周期值。
register_refined_jiffies:注册精化 jiffies 时钟源
register_refined_jiffies的主要目的是注册refined_jiffies时钟源。前面看到的clocksource_jiffies表示标准的 jiffies 时钟源,而在 kernel/time/jiffies.c 中还有另一个时钟源定义:
struct clocksource refined_jiffies;refined_jiffies与clocksource_jiffies的差别在于:标准 jiffies 时钟源是"最低公分母"时钟源,应当在所有系统上都能工作。由于jiffies全局变量在每次定时器中断时递增,标准 jiffies 时钟源的分辨率与定时器中断频率相同——这意味着它可能遭受精度损失。而refined_jiffies以CLOCK_TICK_RATE作为 jiffies 移位的基数,精度更高。
来看函数实现。首先,refined_jiffies基于clocksource_jiffies结构:
int register_refined_jiffies(long cycles_per_second) { u64 nsec_per_tick, shift_hz; long cycles_per_tick; refined_jiffies = clocksource_jiffies; refined_jiffies.name = "refined-jiffies"; refined_jiffies.rating++; ... ... ...这里把名称更新为refined-jiffies,并把 rating 加一。由于clocksource_jiffies的 rating 是 1,refined_jiffies的 rating 就是 2——在时钟源管理代码看来,refined_jiffies比标准 jiffies 更值得优先选择。
下一步计算每个 tick 的周期数:
cycles_per_tick = (cycles_per_second + HZ/2)/HZ;注意:标准 jiffies 乘数以NSEC_PER_SEC为基数,这里用的却是cycles_per_second——即register_refined_jiffies的第一个参数。调用时传入的是CLOCK_TICK_RATE宏,定义于 arch/x86/include/asm/timex.h:
#define CLOCK_TICK_RATE PIT_TICK_RATE而PIT_TICK_RATE展开为 Intel 8253 可编程间隔定时器(Programmable Interval Timer)的频率:
#define PIT_TICK_RATE 1193182ul接下来计算shift_hz,保存hz << 8(即系统定时器频率)。把cycles_per_second(PIT 频率)左移 8 位是为了获得额外精度:
shift_hz = (u64)cycles_per_second << 8; shift_hz += cycles_per_tick/2; do_div(shift_hz, cycles_per_tick);随后同样把NSEC_PER_SEC左移 8 位,计算每个 tick 对应的秒数:
nsec_per_tick = (u64)NSEC_PER_SEC << 8; nsec_per_tick += (u32)shift_hz/2; do_div(nsec_per_tick, (u32)shift_hz);最后设置mult并注册新时钟源:
refined_jiffies.mult = ((u32)nsec_per_tick) << JIFFIES_SHIFT; ... __clocksource_register(&refined_jiffies); return 0;__clocksource_register定义于 include/linux/clocksource.h。时钟源管理代码提供了时钟源注册与选择的 API:时钟源在内核初始化期间或由内核模块调用__clocksource_register注册。注册时,管理代码会依据前面见过的clocksource.rating字段选出系统中最优的时钟源。
如果你觉得这里的计算看起来有点吓人,不必担心——初始化章节的第七部分对setup_arch收尾时只是简要提到这一注册动作,而后续章节(如时钟源框架简介、x86 相关的时钟源)会逐步深入这些细节。
使用 jiffies:读取与时间换算
前面我们看到了两类 jiffies 时钟源的初始化:
- 标准
jiffies时钟源; - 精化的
refined_jiffies时钟源。
同时我们也知道,内核有一个全局变量jiffies保存系统启动以来的 tick 数。现在来看如何在实际代码中使用它。使用方式有两种:直接引用jiffies全局变量,或调用get_jiffies_64函数(定义于 kernel/time/jiffies.c)。后者返回jiffies的完整 64 位值:
u64 get_jiffies_64(void) { unsigned long seq; u64 ret; do { seq = read_seqbegin(&jiffies_lock); ret = jiffies_64; } while (read_seqretry(&jiffies_lock, seq)); return ret; } EXPORT_SYMBOL(get_jiffies_64);注意get_jiffies_64的实现与jiffies_read那种简单返回不同——它用顺序锁(seqlock)保护对jiffies_64的读取。这是为了应对无法原子读取完整 64 位值的机器(例如 32 位平台)。
一旦能访问jiffies或jiffies_64,就可以把它换算成人类可读的时间单位。要得到一秒,可以用:
jiffies / HZ基于这个公式,可以推算任意时间单位:
/* Thirty seconds from now */ jiffies + 30*HZ /* Two minutes from now */ jiffies + 120*HZ /* One millisecond from now */ jiffies + HZ / 1000jiffies 这种"节拍计数"在内核中被广泛使用:例如 RCU 初始化章节的第九部分在计算jiffies_till_first_fqs与jiffies_till_next_fqs时,就用jiffies推算到下一次 force-quiescent-state 的间隔,默认值均为ULONG_MAX,算出后才赋予实际节拍数——这是 jiffies 在真实内核子系统(而非时钟子系统本身)中应用的典型例子。
总结与后续章节
本部分覆盖了 Linux 内核中与时间、时间管理相关的首批概念及其初始化:jiffies与clocksource。我们从setup_arch中的x86_init.timers.wallclock_init出发,看到了标准 PC 与 Intel MID 平台在墙钟初始化上的差异;随后理解了jiffies/jiffies_64全局变量、HZ与CONFIG_HZ的配置关系、clocksource抽象的必要性,以及register_refined_jiffies如何以 PIT 频率为基数注册精度更高的refined-jiffies时钟源;最后学习了读取 jiffies 的get_jiffies_64(含 seqlock 保护)与jiffies / HZ的时间换算方法。
下一部分将继续深入这一主题。在本仓库中,后续章节包括:时钟源框架简介(深入clocksource框架)、tick broadcast framework 与 dyntick、定时器介绍、clockevents 框架简介、x86 相关的时钟源以及时钟相关的系统调用。整个章节的索引见 Timers/README.md。
- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides-zh
Linux 内核揭秘
相关推荐
Linux 内核揭秘:定时器与时间管理(一)——jiffies 与 clocksource 的初始化之旅
Linux 内核揭秘:定时器与时间管理(一)——jiffies 与 clocksource 的初始化之旅 导读 本文是《Linux 内核揭秘》(linux in
深入 Linux 内核定时器与时间管理(一):jiffies 与 clocksource 的初始化与实战使用
深入 Linux 内核定时器与时间管理(一):jiffies 与 clocksource 的初始化与实战使用 本文基于 linux insides 项目 Tim
文档教程操作系统内核如何度量时间?Linux 内核揭秘(linux-insides-zh)clocksource、clockevents 与定时器框架详解
内核如何度量时间?Linux 内核揭秘(linux insides zh)clocksource、clockevents 与定时器框架详解 这篇文章基于开源项目
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考