这篇文章 Timers and time management in the Linux kernel. Part 6. 是出自 linux-insides一书中 Timers and time management 章节
内核版本比对5.7-rc1 进行了相关调整, 增加相关备注
Linux内核中的定时器和时间管理.Part 6.
x86_64 相关的时钟源
这是chapter的第六部分,它描述了Linux内核中与计时器和时间管理相关的内容。 在之前的part中,我们看到了clockevents框架,现在我们将继续深入探讨与时间管理相关的问题 Linux内核中的内容。 本部分将描述与时钟源相关的x86架构的实现(有关[clocksource]概念的更多信息,您可以在second part 中找到相关信息.
首先,我们必须知道在x86体系结构中可以使用哪些时钟源。 从 sysfs 或者从下面的文件 /sys/devices/system/clocksource/clocksource0/available_clocksource. /sys/devices/system/clocksource/clocksourceN来获取相关信息:
available_clocksource- 提供系统可用是时钟源current_clocksource- 提供系统当前使用时钟源
实际看一下:
| |
我们可以看到有三个注册的时钟源在这个系统里:
tsc- Time Stamp Counter;hpet- High Precision Event Timer;acpi_pm- ACPI Power Management Timer.
现在让我们看看第二个文件,它提供了最佳时钟源(在系统中具有最佳评级的时钟源)
| |
tsc是Time Stamp Counter的简写. second part有过描述, 他描述了Linux Kernel的clocksource框架, 系统最好的时钟源应当是最有最好或最高功率,频率的, 或者说是具有最高frequency.
ACPI电源管理计时器的频率为3.579545 MHz。 High Precision Event Timer的频率至少为10 MHz。 Time Stamp Counter的频率取决于处理器。 例如,在较旧的处理器上,Time Stamp Counter 通过内部处理器时钟周期进行计数。 这意味着当处理器的频率缩放比例改变时,其频率也会改变。 对于较新的处理器,情况已经改变。 较新的处理器具有一个invariant Time Stamp counter,在处理器的所有运行状态下均以恒定速率递增。 实际上,我们可以在/proc/cpuinfo的输出中获得它的频率。 例如,对于系统中的第一个处理器:
| |
尽管英特尔手册说时间戳计数器的频率, 虽然恒定,但不一定是处理器的最大合格频率,也不一定是CPU Brand String中给出的频率,但是无论如何,我们可能会发现它远远超过了ACPI PM或 High Precision Event Timer的频率。 我们可以看到,额定值最高或频率最高的时钟源是系统当前的值。
您可以注意到,除了这三个时钟源之外,我们在/sys/devices/system/clocksource/clocksource0/available_clocksource的输出中看不到另外两个我们熟悉的时钟源。 这些时钟源是jiffy和refined_jiffies。我们看不到它们,因为此字段仅映射高分辨率时钟源,或者换句话说,带有 CLOCK_SOURCE_VALID_FOR_HRES 标识的时钟源。
正如我上面已经写过的,我们将在本部分中考虑所有这三个时钟源。 我们将按照它们的初始化顺序进行考虑,或者:
hpet;acpi_pm;tsc.
通过dmesg 我们可以看到输出顺序和上面一致:
| |
第一个时钟源 High Precision Event Timer, 下面先开始介绍它.
High Precision Event Timer
x86架构的High Precision Event Timer的实现位于 arch/x86/kernel/hpet.c源代码文件中。 它的初始化从hpet_enable函数的调用开始。 在Linux内核初始化期间调用此函数。 如果我们从 init/main.c源代码文件中查看start_kernel函数,则会看到在初始化所有特定于体系结构的东西之后,禁用了早期控制台,并且时间管理子系统已经准备就绪,请调用以下函数:
| |
在早期的jiffy计数器已经初始化之后,它将对后期体系结构特定的计时器进行初始化。 x86体系结构的late_time_init函数的定义位于 arch/x86/kernel/time.c。 看起来很简单:
| |
对于通过
8253芯片提供的PIT时钟源进行优化,在不支持acpi情况下都跳过PIT的初始化 x86/timer: Skip PIT initialization on modern chipsets x86/timer: Don’t skip PIT setup when APIC is disabled or in legacy mode
正如我们看到的那样,它完成了x86相关计时器的初始化和Time Stamp Counter的初始化。 在下一段中将看到的秒数,但是现在让我们考虑x86_init.timers.timer_init函数的调用。 timer_init指向同一源代码文件中的hpet_time_init函数。 我们可以通过查看 arch/x86/kernel/x86_init.c 的x86_init结构的定义来验证这一点:
| |
如果我们不能启用High Precision Event Timer ,hpet_time_init可以设置programmable interval timer ,并为启用的计时器设置默认计时器IRQ
| |
首先,通过hpet_enable函数通过调用is_hpet_capable函数, 检查我们是否能够在系统中启用高精度事件计时器,如果可以,我们为其映射一个虚拟地址空间:
| |
is_hpet_capable 检查是否在kernel命令行设置hpet=disable,hpet_address 从 ACPI HPET 表中获取. hpet_set_mapping 函数为时间寄存器映射虚拟地址空间:
| |
/arch/x86/kernel/hpet.c
可以参考文章 IA-PC HPET (High Precision Event Timers) Specification:
Timer寄存器空间1024字节
因此 HPET_MMAP_SIZE 是 1024 字节:
| |
/arch/x86/include/asm/hpet.h
High Precision Event Timer 获取虚拟空间后,可以通过读取HPET_ID获取相关的值:
| |
我们需要获取此数字,以便为HPET的General Configuration Register分配正确的空间量:
| |
在为HPET的配置寄存器分配空间之后,我们允许主计数器运行,并允许定时器中断(如果通过将所有定时器的配置寄存器中的HPET_CFG_ENABLE标识位,设置1允许中断). 最后,我们只需调用hpet_clocksource_register函数来注册新的时钟源:
| |
下面的调用已经很熟悉了函数了
| |
clocksource_hpet 是频率是250的clocksource结构 (refined_jiffies 时钟源频率 2), hpet and read_hpet 回调函数用于 HPET的读取原子计数器.
| |
/kernel/time/jiffies.c 文中提到的
refined_jiffies时钟源的定义, 可以看到是在jiffies基础上进行加1操作
| |
与原文相比较没有了
.archdata = { .vclock_mode = VCLOCK_HPET },[PATCH] x86: Remove hpet vclock support 从说明可以看到由于HPET缓慢的表现,关闭了通过vdso获取时间的渠道,用户态无法直接调用其提供方法获取时间。
clocksource_hpet 注册后, 从 hpet_time_init() 函数返回 arch/x86/kernel/time.c. 最后一步调用:
| |
setup_default_timer_irq 函数检测已经存在的legacy中断号,或者说是否支持i8259 , IRQ0 的设置依赖于此。
从这一刻开始, High Precision Event Timer 时钟源完成了linux 内核的 clock source 框架的注册,可以通过read_hpet, 从通用内核代码中使用:
| |
这个函数只是从Main Counter Register中读取和返回原子计数器的值.
ACPI PM timer
第二个时钟源ACPI Power Management Timer. 代码实现位于 drivers/clocksource/acpi_pm.c 文件中, fs initcall期间从对 init_acpi_pm_clocksource函数调用开始.
如果我们看一下init_acpi_pm_clocksource函数的实现,我们将看到它是从检查pmtmr_ioport变量的值开始的:
| |
变量pmtmr_ioport 包含Power Management Timer Control Register Block的扩展地址. 通过定义在arch/x86/kernel/acpi/boot.c acpi_parse_fadt 函数获得. 这个函数解析 FADT 或者 Fixed ACPI Description Table ACPI表并尝试获取X_PM_TMR_BLK字段的值,而该字段包含Power Management Timer Control Register Block的扩展地址, 以 Generic Address Structure 格式表示:
| |
因此,如果Linux内核关闭CONFIG_X86_PM_TIMER选项或者acpi_parse_fadt函数出现问题,我们将无法从 init_acpi_pm_clocksource访问Power Management Timer并.换句话说,如果pmtmr_ioport变量不为0, 我们将检查此计数器的频率并通过调用以下命令注册时钟源:
| |
在调用clocksource_register_hs之后,acpi_pm时钟源将被注册到Linux内核的clocksource框架中:
| |
/drivers/clocksource/acpi_pm.c
在频率为200情况下acpi_pm_read读取acpi_pm时钟源提供的原子计数器。 acpi_pm_read函数仅执行read_pmtmr函数:
| |
上面函数读取 Power Management Timer的值. 相关数据结构如下:
| |
数据存储再 Fixed ACPI Description Table ACPI 中我们将它存储到pmtmr_ioport.因此 read_pmtmr的实现很简单:
| |
我们仅读取Power Management Timer数据的低24位.
下面我们开始最后一个时钟源 -Time Stamp Counter.
Time Stamp Counter
本章节最后一个时钟源Time Stamp Counter 实现代码在 arch/x86/kernel/tsc.c 中.我们已经看到了 Time Stamp Counter 初始化是从x86_late_time_init开始 . 这个函数调用 tsc_init() , 实现在 arch/x86/kernel/tsc.c 文件中.
tsc_init函数开始可以看到其检查处理器是否支持Time Stamp Counter:
| |
boot_cpu_has 宏定义扩展为 宏cpu_has:
| |
检查boot_cpu_data信息(我们的例子里是 X86_FEATURE_TSC_DEADLINE_TIMER ) 这些数据在在linux内核初始化的时候被填充,如果处理器支持Time Stamp Counter, 我们可以通过calibrate_tsc获取Time Stamp Counter 的频率, 类似于从 Model Specific Register获取不同源的频率, 并通过programmable interval timer, 我们将初始化系统中所有处理器的频率和比例因子:
模型的寄存器(MSR)是x86指令集中用于调试、程序执行跟踪、计算机性能监视和切换某些CPU特性的各种控制寄存器。
| |
因为只有第一个引导程序处理器才会调用tsc_init。 在此之后,对所有cpu上的TSC是否可信和同步进行判断:
避免在
TSC没有正确同步的多套接字系统上出现时间差异,从而排除TSC用于计时。但是,这也会阻止使用TSC作为sched clock(),这是不必要的,因为核心sched clock()实现可以很好地处理基于TSC的非同步sched时钟。
| |
原文中的
tsc_disabled在 x86/tsc: Redefine notsc to behave as tsc=unstable弃用了, 原因是notsc内核参数禁止sched clock()使用TSC。但是,这个参数并不会阻止内核访问其他地方的tsc, 为了兼容性考虑使用tsc_unstable,
如果引导程序处理器支持X86_FEATURE_TSC_RELIABLE,则调用check_system_tsc_reliable函数,该函数设置tsc_clocksource_reliable。 请注意,我们经历了tsc_init函数,但未注册我们的时钟源。 TSC的实际注册发生在:
| |
在设备初始化 device initcall. 调用期间调用此函数。 我们这样做是为了确保在 High Precision Event Timer 时钟源之后注册时间戳计数器时钟源。
在这三个时钟源之后,所有这三个时钟源都将被注册到时钟源框架中,并且Time Stamp Counter将被选择为活动时钟,因为它在其他时钟源中具有最高的频率.
| |
小结
这是本章 chapter 第六部分的结尾,它描述了Linux内核中与计时器和计时器管理相关的内容。 在上一部分中,您熟悉了clockevents框架。 在这一部分中,我们继续学习Linux内核中与时间管理相关的内容,并了解了x86 architecture中使用的三种不同的时钟源。 下一部分将是本章的最后一部分,我们将看到一些与用户空间有关的内容,即,如何在Linux内核中实现一些与时间有关的系统调用system calls.
如果您有任何问题或建议,请随时在twitter 0xAX上ping我,给我发电子邮件email或者只创建问题。
请注意,英语不是我的第一语言,我很抱歉给您带来不便。如果您发现任何错误,请将PR发送至linux-insides。