这是本章的第六部分,描述了Linux内核中的定时器和时间管理的相关内容。上一节中,我们了解了clockevents框架,现在继续深入研究Linux内核中的时间管理相关内容,本节将讲述x86架构中时钟源的实现(更多关于时钟源的概念可以参考本章第二节)。
首先,我们需要知道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
作者的系统中是Time Stamp Counter。由本章第二节内容可知,系统中最好的时钟源是具有最佳(最高)等级的时钟源,或者说是具有最高频率的时钟源。
ACPI电源管理时钟的频率是3.579545MHz。而High Precision Event Timer(高精度事件定时器)的频率至少是10MHz,而Time Stamp Counter(时间戳计数器)的频率取决于处理器。例如在较早的处理器上,TSC
用来计算处理器内部的时钟周期,就是说当处理器的频率比生变化时,其频率也会发生变化。这种现象在较新的处理器上有所改善。新的处理器有一个不变的时间戳计数器,无论处理器在什么状态下都会以恒定的速率递增。我们可以在/proc/cpuinfo
的输出中获得它的频率。例如:
$ cat /proc/cpuinfo
...
model name : Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz
...
而尽管英特尔的开发者手册说,TSC
的频率虽然是恒定的,但不一定是处理器的最大频率或者品牌名称中中给出的频率。总之,可以发现,TSC远超ACPI PM
计时器以及HPET
的频率,而且具有最佳速度或最高频率的时钟源是系统中当前正在使用的时钟。
注意到,除了这三个时钟源之外,在/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,那就从它开始。
用于x86架构的HPET的内核代码位于arch/x86/kernel/hpet.c文件中。它的初始化是从调用hpet_enable
函数开始的。这个函数在Linux内核初始化时被调用。从init/main.c文件中的start_kernel
函数中可以发现,在所有那些'架构特有的'的事物被初始化之后,以及'early console'被禁用,并且时间管理子系统已经准备就绪时,调用以下函数。
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)
{
x86_init.timers.timer_init();
tsc_init();
}
可以看到,这里完成x86
相关定时器的初始化和TSC
的初始化。现在来考虑调用函数x86_init.timers.timer_init
。timer_init
指向同一源文件中的hpet_time_init
。可以通过查看 x86_init
结构图的定义来验证这一点。
arch/x86/kernel/x86_init.c:
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,
},
...
...
...
如果HPET
支持没有开启,那么函数hpet_time_init
会初始化programmable interval timer,并且设置默认时钟IRQ:
void __init hpet_time_init(void)
{
if (!hpet_enable())
setup_pit_timer();
setup_default_timer_irq();
}
首先,函数hpet_enable
通过调用is_hpet_capable'检查能否在系统中启用
HPET`,如果可以,我们就为它映射一个虚拟地址空间。
int __init hpet_enable(void)
{
if (!is_hpet_capable())
return 0;
hpet_set_mapping();
}
函数is_hpet_capable
确认没有向内核命令行传递hpet=disable
,并且hpet_address
是来自表ACPI HPET。函数hpet_set_mapping
为时钟相关寄存器映射虚拟地址空间。
hpet_virt_address = ioremap_nocache(hpet_address, HPET_MMAP_SIZE);
IA-PC HPET (High Precision Event Timers) Specification 有讲述:
时钟寄存器空间有1024字节
因此,HPET_MMAP_SIZE
也是 1024
字节。
#define HPET_MMAP_SIZE 1024
在为HPET
映射了虚拟地址空间之后,就可以通过读寄存器HPET_ID
得到时钟号:
id = hpet_readl(HPET_ID);
last = (id & HPET_ID_NUMBER) >> HPET_ID_NUMBER_SHIFT;
这个数字是用来为HPET
的 配置寄存器
分配适当大小的空间。
cfg = hpet_readl(HPET_CFG);
hpet_boot_cfg = kmalloc((last + 2) * sizeof(*hpet_boot_cfg), GFP_KERNEL);
在为 HPET
的配置寄存器分配空间后,主计时钟开始运行,并可以通过配置寄存器的HPET_CFG_ENABLE
位,为每一个时钟设置定时器中断。前提是,所有的时钟都通过配置寄存器中的HPET_CFG_ENABLE
位所启用。最后,我们仅通过调用hpet_clocksource_register
函数来注册新的时钟源。
if (hpet_clocksource_register())
goto out_nohpet;
这个函数调用已经很熟悉了:
clocksource_register_hz(&clocksource_hpet, (u32)hpet_freq);
其中clocksource_hpet
是clocksource
结构体对象,成员rating
是250
(之前refined_jiffies
时钟源的rating
是2
),hpet
和read_hpet
两个回调函数用于读取HPET
提供的原子计数器。
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 },
};
在注册clocksource_hpet
后,可以回看arch/x86/kernel/time.c源文件中的函数hpet_time_init()
。最后一步的调用:
setup_default_timer_irq();
函数setup_default_timer_irq
检查legacy
IRQ是否存在,也就是对i8259的支持,并且配置IRQ0。
代码到这里High Precision Event Timer,时钟源在Linux内核的时钟框架中完成注册,可以在内核中使用read_hpet
。
static cycle_t read_hpet(struct clocksource *cs)
{
return (cycle_t)hpet_readl(HPET_COUNTER);
}
该函数读取并返回Main Counter Register
中的原子计数器。
第二个时钟源是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 Blcok
, 并以结构体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;
}
因此,如果内核配置CONFIG_X86_PM_TIMER
被禁用,或者acpi_parse_fadt
函数出错,就不能访问Power Management Timer
中的寄存器,并从init_acpi_pm_clocksource
返回。也就是说,如果pmtmr_ioport
变量的值不是0,就会检查这个时钟的速率,并通过调用下面这个函数来注册这个时钟源。
clocksource_register_hz(&clocksource_acpi_pm, PMTMR_TICKS_PER_SEC);
调用函数clocksource_register_hs
之后,acpi_pm
时钟源被注册到clocksource
内核框架中:
static struct clocksource clocksource_acpi_pm = {
.name = "acpi_pm",
.rating = 200,
.read = acpi_pm_read,
.mask = (cycle_t)ACPI_PM_MASK,
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
成员rating
是 200
,并且acpi_pm_read
回调函数读apci_pm
时钟源提供的原子计数器。 函数acpi_pm_read
正是执行read_pmtmr
:
static cycle_t acpi_pm_read(struct clocksource *cs)
{
return (cycle_t)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,它的实现位于源文件arch/x86/kernel/tsc.c。前文已经看到过函数x86_late_time_init
,以及Time Stamp Counter的初始化函数,也从这个开始,这个函数调用了tsc_init()
。
在函数tsc_init
开始的地方,可以看到它确认处理器是否支持Time Stamp Counter
:
void __init tsc_init(void)
{
u64 lpj;
int cpu;
if (!cpu_has_tsc) {
setup_clear_cpu_cap(X86_FEATURE_TSC_DEADLINE_TIMER);
return;
}
...
...
...
宏cpu_has_tsc
展开,调用宏cpu_has
macro:
#define cpu_has_tsc boot_cpu_has(X86_FEATURE_TSC)
#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
。如果处理器支持Time Stamp Counter
,通过调用同一源代码文件中的calibrate_tsc
函数来获得TSC
的频率,该函数会尝试从不同的时钟源获得频率,如MSR,通过programmable interval timer校准等等,之后为系统中所有处理器初始化频率和比例因子。
tsc_khz = x86_platform.calibrate_tsc();
cpu_khz = tsc_khz;
for_each_possible_cpu(cpu) {
cyc2ns_init(cpu);
set_cyc2ns_scale(cpu_khz, cpu);
}
因为只有第一个引导处理器会调用 tsc_init
,此后,检查TSC
是否被禁用。
if (tsc_disabled > 0)
return;
...
...
...
check_system_tsc_reliable();
并调用函数check_system_tsc_reliable
,如果bootstrap处理器有X86_FEATURE_TSC_RELIABLE
特性,则设置tsc_clocksource_reliable
。注意,到这里函数tsc_init
结束,但没有注册时钟源。实际注册TSC
时钟源是在:
static int __init init_tsc_clocksource(void)
{
if (!cpu_has_tsc || tsc_disabled > 0 || !tsc_khz)
return 0;
...
...
...
if (boot_cpu_has(X86_FEATURE_TSC_RELIABLE)) {
clocksource_register_khz(&clocksource_tsc, tsc_khz);
return 0;
}
这个函数在device
initcall期间调用。这样做是为了确保TSC
时钟源在HPET时钟源之后被注册。
在这之后,所有三个时钟源都在 clocksource
框架中注册,TSC
时钟源将被选为当前时钟源,因为它其他时钟源中具有最高等级。
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 },
};
这是本章的第六节,描述了Linux内核中的时钟和时钟管理。上一节中,熟悉了clockevents
框架。这一节中,继续学习了Linux内核中时钟管理,并且看到了在x86架构中使用的三种不同的时钟源。下一节将是本章的最后一节,将看到一些与用户空间有关的事情,即一些与时间有关的系统调用如何在Linux内核中实现。
如果有问题或建议,请随时在twitter0xAX上与我联系,给我发email或直接创建issue。
请注意,英语不是我的第一语言,我真的很抱歉给你带来的不便。如果你发现任何错误,请给我发送PR到linux-insides。