这篇文章 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
– 提供系统当前使用时钟源
实际看一下:
$ cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm
我们可以看到有三个注册的时钟源在这个系统里:
tsc
– Time Stamp Counter;hpet
– High Precision Event Timer;acpi_pm
– ACPI Power Management Timer.
现在让我们看看第二个文件,它提供了最佳时钟源(在系统中具有最佳评级的时钟源)
$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource
tsc
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
的输出中获得它的频率。 例如,对于系统中的第一个处理器:
$ cat /proc/cpuinfo
...
model name : Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz
...
尽管英特尔手册说时间戳计数器的频率, 虽然恒定,但不一定是处理器的最大合格频率,也不一定是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 我们可以看到输出顺序和上面一致:
$ 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.094369] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 1911260446275000 ns
[ 0.186498] clocksource: Switched to clocksource hpet
[ 0.196827] clocksource: acpi_pm: mask: 0xffffff max_cycles: 0xffffff, max_idle_ns: 2085701024 ns
[ 1.413685] tsc: Refined TSC clocksource calibration: 3999.981 MHz
[ 1.413688] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x73509721780, max_idle_ns: 881591102108 ns
[ 2.413748] clocksource: Switched to clocksource tsc
第一个时钟源 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
函数,则会看到在初始化所有特定于体系结构的东西之后,禁用了早期控制台,并且时间管理子系统已经准备就绪,请调用以下函数:
if (late_time_init)
late_time_init();
在早期的jiffy计数器已经初始化之后,它将对后期体系结构特定的计时器进行初始化。 x86体系结构的late_time_init
函数的定义位于 arch/x86/kernel/time.c。 看起来很简单:
static __init void x86_late_time_init(void)
{
/*
* Before PIT/HPET init, select the interrupt mode. This is required
* to make the decision whether PIT should be initialized correct.
*/
x86_init.irqs.intr_mode_select();
/* Setup the legacy timers */
x86_init.timers.timer_init();
/*
* After PIT/HPET timers init, set up the final interrupt mode for
* delivering IRQs.
*/
x86_init.irqs.intr_mode_init();
tsc_init();
}
对于通过
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
结构的定义来验证这一点:
struct x86_init_ops x86_init __initdata = {
...
...
...
.timers = {
.setup_percpu_clockev = setup_boot_APIC_clock,
.timer_init = hpet_time_init,
.wallclock_init = x86_init_noop,
},
...
...
...
如果我们不能启用High Precision Event Timer
,hpet_time_init
可以设置programmable interval timer ,并为启用的计时器设置默认计时器IRQ
void __init hpet_time_init(void)
{
if (!hpet_enable()) {
if (!pit_timer_init())
return;
}
setup_default_timer_irq();
}
首先,通过hpet_enable
函数通过调用is_hpet_capable
函数, 检查我们是否能够在系统中启用高精度事件计时器
,如果可以,我们为其映射一个虚拟地址空间:
int __init hpet_enable(void)
{
if (!is_hpet_capable())
return 0;
hpet_set_mapping();
}
is_hpet_capable
检查是否在kernel命令行设置hpet=disable
,hpet_address
从 ACPI HPET 表中获取. hpet_set_mapping
函数为时间寄存器映射虚拟地址空间:
static inline void hpet_set_mapping(void)
{
hpet_virt_address = ioremap(hpet_address, HPET_MMAP_SIZE);
}
/arch/x86/kernel/hpet.c
可以参考文章 IA-PC HPET (High Precision Event Timers) Specification:
Timer寄存器空间1024字节
因此 HPET_MMAP_SIZE
是 1024
字节:
#define HPET_MMAP_SIZE 1024
/arch/x86/include/asm/hpet.h
High Precision Event Timer
获取虚拟空间后,可以通过读取HPET_ID
获取相关的值:
id = hpet_readl(HPET_ID);
last = (id & HPET_ID_NUMBER) >> HPET_ID_NUMBER_SHIFT;
我们需要获取此数字,以便为HPET
的General Configuration Register
分配正确的空间量:
cfg = hpet_readl(HPET_CFG);
hpet_boot_cfg = kmalloc((last + 2) * sizeof(*hpet_boot_cfg), GFP_KERNEL);
在为HPET
的配置寄存器分配空间之后,我们允许主计数器运行,并允许定时器中断(如果通过将所有定时器的配置寄存器中的HPET_CFG_ENABLE
标识位,设置1
允许中断). 最后,我们只需调用hpet_clocksource_register
函数来注册新的时钟源:
if (hpet_clocksource_register())
goto out_nohpet;
下面的调用已经很熟悉了函数了
clocksource_register_hz(&clocksource_hpet, (u32)hpet_freq);
clocksource_hpet
是频率是250
的clocksource
结构 (refined_jiffies
时钟源频率 2
), hpet
and read_hpet
回调函数用于 HPET
的读取原子计数器.
static struct clocksource clocksource_jiffies = {
.name = "jiffies",
.rating = 1, /* lowest valid rating*/
.read = jiffies_read,
.mask = CLOCKSOURCE_MASK(32),
.mult = TICK_NSEC << JIFFIES_SHIFT, /* details above */
.shift = JIFFIES_SHIFT,
.max_cycles = 10,
};
...
static struct clocksource refined_jiffies;
...
int register_refined_jiffies(long cycles_per_second)
{
refined_jiffies = clocksource_jiffies;
refined_jiffies.name = "refined-jiffies";
refined_jiffies.rating++;
...
/kernel/time/jiffies.c
文中提到的refined_jiffies
时钟源的定义, 可以看到是在jiffies
基础上进行加1操作
static struct clocksource clocksource_hpet = {
.name = "hpet",
.rating = 250,
.read = read_hpet,
.mask = HPET_MASK,
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
.resume = hpet_resume_counter,
};
与原文相比较没有了
.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();
setup_default_timer_irq
函数检测已经存在的legacy
中断号,或者说是否支持i8259 , IRQ0 的设置依赖于此。
从这一刻开始, High Precision Event Timer 时钟源完成了linux 内核的 clock source
框架的注册,可以通过read_hpet
, 从通用内核代码中使用:
static cycle_t read_hpet(struct clocksource *cs)
{
return (cycle_t)hpet_readl(HPET_COUNTER);
}
这个函数只是从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
变量的值开始的:
static int __init init_acpi_pm_clocksource(void)
{
...
...
...
if (!pmtmr_ioport)
return -ENODEV;
...
...
...
变量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
格式表示:
static int __init acpi_parse_fadt(struct acpi_table_header *table)
{
#ifdef CONFIG_X86_PM_TIMER
...
...
...
pmtmr_ioport = acpi_gbl_FADT.xpm_timer_block.address;
...
...
...
#endif
return 0;
}
因此,如果Linux内核关闭CONFIG_X86_PM_TIMER
选项或者acpi_parse_fadt
函数出现问题,我们将无法从 init_acpi_pm_clocksource
访问Power Management Timer
并.换句话说,如果pmtmr_ioport
变量不为0, 我们将检查此计数器的频率并通过调用以下命令注册时钟源:
clocksource_register_hz(&clocksource_acpi_pm, PMTMR_TICKS_PER_SEC);
在调用clocksource_register_hs
之后,acpi_pm
时钟源将被注册到Linux内核的clocksource
框架中:
static struct clocksource clocksource_acpi_pm = {
.name = "acpi_pm",
.rating = 200,
.read = acpi_pm_read,
.mask = (u64)ACPI_PM_MASK,
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
/drivers/clocksource/acpi_pm.c
在频率为200
情况下acpi_pm_read
读取acpi_pm
时钟源提供的原子计数器。 acpi_pm_read
函数仅执行read_pmtmr
函数:
static u64 acpi_pm_read(struct clocksource *cs)
{
return (u64)read_pmtmr();
}
上面函数读取 Power Management Timer
的值. 相关数据结构如下:
+-------------------------------+----------------------------------+
| | |
| upper eight bits of a | running count of the |
| 32-bit power management timer | power management timer |
| | |
+-------------------------------+----------------------------------+
31 E_TMR_VAL 24 TMR_VAL 0
数据存储再 Fixed ACPI Description Table
ACPI 中我们将它存储到pmtmr_ioport
.因此 read_pmtmr
的实现很简单:
static inline u32 read_pmtmr(void)
{
return inl(pmtmr_ioport) & ACPI_PM_MASK;
}
我们仅读取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
:
void __init tsc_init(void)
{
u64 lpj;
int cpu;
if (!boot_cpu_has(X86_FEATURE_TSC)) {
setup_clear_cpu_cap(X86_FEATURE_TSC_DEADLINE_TIMER);
return;
}
...
...
...
boot_cpu_has
宏定义扩展为 宏cpu_has
:
#define boot_cpu_has(bit) cpu_has(&boot_cpu_data, bit)
#define cpu_has(c, bit) \
(__builtin_constant_p(bit) && REQUIRED_MASK_BIT_SET(bit) ? 1 : \
test_cpu_cap(c, bit))
检查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特性的各种控制寄存器。
void __init tsc_early_init(void)
{
if (!boot_cpu_has(X86_FEATURE_TSC))
return;
/* Don't change UV TSC multi-chassis synchronization */
if (is_early_uv_system())
return;
if (!determine_cpu_tsc_frequencies(true))
return;
loops_per_jiffy = get_loops_per_jiffy();
tsc_enable_sched_clock();
}
...
void __init tsc_init(void)
...
if (!tsc_khz) {
/* We failed to determine frequencies earlier, try again */
if (!determine_cpu_tsc_frequencies(false)) {
mark_tsc_unstable("could not calculate TSC khz");
setup_clear_cpu_cap(X86_FEATURE_TSC_DEADLINE_TIMER);
return;
}
tsc_enable_sched_clock();
}
cyc2ns_init_secondary_cpus();
...
因为只有第一个引导程序处理器才会调用tsc_init
。 在此之后,对所有cpu上的TSC
是否可信和同步进行判断:
避免在
TSC
没有正确同步的多套接字系统上出现时间差异,从而排除TSC
用于计时。但是,这也会阻止使用TSC作为sched clock(),这是不必要的,因为核心sched clock()实现可以很好地处理基于TSC的非同步sched时钟。
- if (tsc_disabled > 0)
- return;
+ if (unsynchronized_tsc()) {
+ mark_tsc_unstable("TSCs unsynchronized");
+ return;
+ }
...
...
...
check_system_tsc_reliable();
原文中的
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
的实际注册发生在:
static int __init init_tsc_clocksource(void)
{
if (!boot_cpu_has(X86_FEATURE_TSC) || !tsc_khz)
return 0;
if (tsc_unstable)
goto unreg;
if (tsc_clocksource_reliable || no_tsc_watchdog)
clocksource_tsc.flags &= ~CLOCK_SOURCE_MUST_VERIFY;
if (boot_cpu_has(X86_FEATURE_NONSTOP_TSC_S3))
clocksource_tsc.flags |= CLOCK_SOURCE_SUSPEND_NONSTOP;
/*
* When TSC frequency is known (retrieved via MSR or CPUID), we skip
* the refined calibration and directly register it as a clocksource.
*/
if (boot_cpu_has(X86_FEATURE_TSC_KNOWN_FREQ)) {
if (boot_cpu_has(X86_FEATURE_ART))
art_related_clocksource = &clocksource_tsc;
clocksource_register_khz(&clocksource_tsc, tsc_khz);
unreg:
clocksource_unregister(&clocksource_tsc_early);
return 0;
}
schedule_delayed_work(&tsc_irqwork, 0);
return 0;
}
在设备初始化 device
initcall. 调用期间调用此函数。 我们这样做是为了确保在 High Precision Event Timer 时钟源之后注册时间戳计数器时钟源。
在这三个时钟源之后,所有这三个时钟源都将被注册到时钟源框架中,并且Time Stamp Counter
将被选择为活动时钟,因为它在其他时钟源中具有最高的频率.
static struct clocksource clocksource_tsc = {
.name = "tsc",
.rating = 300,
.read = read_tsc,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS | CLOCK_SOURCE_MUST_VERIFY,
.archdata = { .vclock_mode = VCLOCK_TSC },
};
小结
这是本章 chapter 第六部分的结尾,它描述了Linux内核中与计时器和计时器管理相关的内容。 在上一部分中,您熟悉了clockevents
框架。 在这一部分中,我们继续学习Linux内核中与时间管理相关的内容,并了解了x86 architecture中使用的三种不同的时钟源。 下一部分将是本章的最后一部分,我们将看到一些与用户空间有关的内容,即,如何在Linux内核中实现一些与时间有关的系统调用system calls.
如果您有任何问题或建议,请随时在twitter 0xAX上ping我,给我发电子邮件email或者只创建问题。
请注意,英语不是我的第一语言,我很抱歉给您带来不便。如果您发现任何错误,请将PR发送至linux-insides。
Be First to Comment