x86 相关的时钟源

x86_64相关的时钟资源

这是本章的第六部分,描述了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 

可以看到有三个已注册的时钟资源:

现在来看第二个文件,其中记录了最好的时钟资源(系统中,拥有最高频率的时钟资源):

$ 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的输出中没有看到另外两个熟悉的时钟源,jiffyrefined_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架构的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_inittimer_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_hpetclocksource结构体对象,成员rating250(之前refined_jiffies时钟源的rating2),hpetread_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检查legacyIRQ是否存在,也就是对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 PM timer

第二个时钟源是ACPI Power Management Timer。这个时钟源的实现位于drivers/clocksource/acpi_pm.c源文件中,从fsinitcall中调用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中获取其值。该函数解析 FADTFixed 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,
};

成员rating200,并且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

这第三个也是最后一个时钟源是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;
	}

这个函数在deviceinitcall期间调用。这样做是为了确保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 },
};

Conclusion

这是本章的第六节,描述了Linux内核中的时钟和时钟管理。上一节中,熟悉了clockevents框架。这一节中,继续学习了Linux内核中时钟管理,并且看到了在x86架构中使用的三种不同的时钟源。下一节将是本章的最后一节,将看到一些与用户空间有关的事情,即一些与时间有关的系统调用如何在Linux内核中实现。 如果有问题或建议,请随时在twitter0xAX上与我联系,给我发email或直接创建issue请注意,英语不是我的第一语言,我真的很抱歉给你带来的不便。如果你发现任何错误,请给我发送PR到linux-insides

链接

最后更新于