ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux 内核揭秘:定时器与时间管理(一)——从初始化到 jiffies 与 clocksource 框架

Linux 内核揭秘:定时器与时间管理(一)——从初始化到 jiffies 与 clocksource 框架 文档教程操作系统【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载本文是《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 MIDMobile 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 InterfaceSFI的 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版本x8632 位使用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.cstatic 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 是 300HPET 的 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 是0xffffffff32 位意味着 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_SECHZ/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 是 1refined_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_secondPIT 频率左移 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-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载相关推荐Linux 内核揭秘定时器与时间管理一——jiffies 与 clocksource 的初始化之旅Linux 内核揭秘定时器与时间管理一——jiffies 与 clocksource 的初始化之旅 导读 本文是《Linux 内核揭秘》linux in深入 Linux 内核定时器与时间管理一jiffies 与 clocksource 的初始化与实战使用深入 Linux 内核定时器与时间管理一jiffies 与 clocksource 的初始化与实战使用 本文基于 linux insides 项目 Tim文档教程操作系统内核如何度量时间Linux 内核揭秘linux-insides-zhclocksource、clockevents 与定时器框架详解内核如何度量时间Linux 内核揭秘linux insides zhclocksource、clockevents 与定时器框架详解 这篇文章基于开源项目上一篇如何在Windows上轻松安装Android应用APK Installer完整指南下一篇解决Tokio异步程序中的stdin阻塞问题从原理到最佳实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表