LogoLogo
  • 简介
  • 引导
    • 从引导加载程序内核
    • 在内核安装代码的第一步
    • 视频模式初始化和转换到保护模式
    • 过渡到 64 位模式
    • 内核解压缩
  • 初始化
    • 内核解压之后的首要步骤
    • 早期的中断和异常控制
    • 在到达内核入口之前最后的准备
    • 内核入口 - start_kernel
    • 体系架构初始化
    • 进一步初始化指定体系架构
    • 最后对指定体系架构初始化
    • 调度器初始化
    • RCU 初始化
    • 初始化结束
  • 中断
    • 中断和中断处理第一部分
    • 深入 Linux 内核中的中断
    • 初步中断处理
    • 中断处理
    • 异常处理的实现
    • 处理不可屏蔽中断
    • 深入外部硬件中断
    • IRQs的非早期初始化
    • Softirq, Tasklets and Workqueues
    • 最后一部分
  • 系统调用
    • 系统调用概念简介
    • Linux 内核如何处理系统调用
    • vsyscall and vDSO
    • Linux 内核如何运行程序
    • open 系统调用的实现
    • Linux 资源限制
  • 定时器和时钟管理
    • 简介
    • 时钟源框架简介
    • The tick broadcast framework and dyntick
    • 定时器介绍
    • Clockevents 框架简介
    • x86 相关的时钟源
    • Linux 内核中与时钟相关的系统调用
  • 同步原语
    • 自旋锁简介
    • 队列自旋锁
    • 信号量
    • 互斥锁
    • 读者/写者信号量
    • 顺序锁
    • RCU
    • Lockdep
  • 内存管理
    • 内存块
    • 固定映射地址和 ioremap
    • kmemcheck
  • 控制组
    • 控制组简介
  • SMP
  • 概念
    • 每个 CPU 的变量
    • CPU 掩码
    • initcall 机制
    • Linux 内核的通知链
  • Linux 内核中的数据结构
    • 双向链表
    • 基数树
    • 位数组
  • 理论
    • 分页
    • ELF 文件格式
    • 內联汇编
    • CPUID
    • MSR
  • Initial ram disk
  • 杂项
    • Linux 内核开发
    • 内核编译方法
    • 链接器
    • 用户空间的程序启动过程
    • 书写并提交你第一个内核补丁
  • 内核数据结构
    • 中断描述符表
  • 有帮助的链接
  • 贡献者
由 GitBook 提供支持
在本页
  • x86_64相关的时钟资源
  • High Precision Event Timer
  • ACPI PM timer
  • Time Stamp Counter
  • Conclusion
  • 链接
  1. 定时器和时钟管理

x86 相关的时钟源

上一页Clockevents 框架简介下一页Linux 内核中与时钟相关的系统调用

最后更新于1年前

x86_64相关的时钟资源

这是的第六部分,描述了Linux内核中的定时器和时间管理的相关内容。上一节中,我们了解了clockevents框架,现在继续深入研究Linux内核中的时间管理相关内容,本节将讲述x86架构中时钟源的实现(更多关于时钟源的概念可以参考本章第二节)。

首先,我们需要知道x86架构上可以使用哪些时钟源。这个问题很容易从或文件/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 - ;

  • hpet - ;

  • acpi_pm - .

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

$ cat /sys/devices/system/clocksource/clocksource0/current_clocksource 
tsc

作者的系统中是。由可知,系统中最好的时钟源是具有最佳(最高)等级的时钟源,或者说是具有最高频率的时钟源。

电源管理时钟的频率是3.579545MHz。而的频率至少是10MHz,而的频率取决于处理器。例如在较早的处理器上,TSC用来计算处理器内部的时钟周期,就是说当处理器的频率比生变化时,其频率也会发生变化。这种现象在较新的处理器上有所改善。新的处理器有一个不变的时间戳计数器,无论处理器在什么状态下都会以恒定的速率递增。我们可以在/proc/cpuinfo的输出中获得它的频率。例如:

$ cat /proc/cpuinfo
...
model name	: Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz
...

而尽管英特尔的开发者手册说,TSC的频率虽然是恒定的,但不一定是处理器的最大频率或者品牌名称中中给出的频率。总之,可以发现,TSC远超ACPI PM计时器以及HPET的频率,而且具有最佳速度或最高频率的时钟源是系统中当前正在使用的时钟。

正如上面所述,本节将会涵盖所有这三个时钟源,将按照它们初始化的顺序来逐一分析。

  • 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

if (late_time_init)
	late_time_init();
static __init void x86_late_time_init(void)
{
	x86_init.timers.timer_init();
	tsc_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,
   },
   ...
   ...
   ...
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();
}
hpet_virt_address = ioremap_nocache(hpet_address, HPET_MMAP_SIZE);

时钟寄存器空间有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 },
};
setup_default_timer_irq();
static cycle_t read_hpet(struct clocksource *cs)
{
	return (cycle_t)hpet_readl(HPET_COUNTER);
}

该函数读取并返回Main Counter Register中的原子计数器。

ACPI PM timer

static int __init init_acpi_pm_clocksource(void)
{
    ...
    ...
    ...
	if (!pmtmr_ioport)
		return -ENODEV;
    ...
    ...
    ...
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
static inline u32 read_pmtmr(void)
{
	return inl(pmtmr_ioport) & ACPI_PM_MASK;
}

只需要读去寄存器Power Management Timer的值,并且取出第24位。

现在来看本章最后一个时钟源Time Stamp Counter。

Time Stamp Counter

在函数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))
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;
	}
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

链接

注意到,除了这三个时钟源之外,在/sys/devices/system/clocksource/clocksource0/available_clocksource的输出中没有看到另外两个熟悉的时钟源,jiffy和refined_jiffies。之所以看不到它们,是因为这个文件只映射高分辨率的时钟源,也就是带有标志的时钟源。

第一个时钟源是 ,那就从它开始。

用于架构的HPET的内核代码位于文件中。它的初始化是从调用hpet_enable函数开始的。这个函数在Linux内核初始化时被调用。从文件中的start_kernel函数中可以发现,在所有那些'架构特有的'的事物被初始化之后,以及'early console'被禁用,并且时间管理子系统已经准备就绪时,调用以下函数。

该函数在早期jiffy计数器被初始化后,对后期的架构特有的定时器进行初始化。x86架构的late_time_init函数的定义位于 文件中。它看起来这样:

可以看到,这里完成x86相关定时器的初始化和TSC的初始化。现在来考虑调用函数x86_init.timers.timer_init。timer_init指向同一源文件中的hpet_time_init。可以通过查看 x86_init结构图的定义来验证这一点。 :

如果HPET支持没有开启,那么函数hpet_time_init 会初始化,并且设置默认时钟:

函数is_hpet_capable确认没有向内核命令行传递hpet=disable,并且hpet_address是来自表。函数hpet_set_mapping为时钟相关寄存器映射虚拟地址空间。

有讲述:

在注册clocksource_hpet后,可以回看源文件中的函数hpet_time_init()。最后一步的调用:

函数setup_default_timer_irq检查legacyIRQ是否存在,也就是对的支持,并且配置。

代码到这里,时钟源在Linux内核的时钟框架中完成注册,可以在内核中使用read_hpet。

第二个时钟源是。这个时钟源的实现位于源文件中,从fs中调用init_acpi_pm_clocksource函数开始。 如果看一下 init_acpi_pm_clocksource函数的实现,会发现它是从检查 pmtmr_ioport变量的值开始的。

变量pmtmr_ioport包含Power Management Timer Control Register Block的扩展地址。在源文件 中定义的函数acpi_parse_fadt中获取其值。该函数解析 FADT 或 Fixed ACPI Description Table 并获取包含扩展地址的 X_PM_TMR_BLK 字段的值Power Management Timer Control Register Blcok, 并以结构体Generic Address Structure格式表示:

这个寄存器的地址是存在Fixed ACPI Description Table 表中,并且可以通过pmtmr_ioport访问。所以,函数read_pmtmr的实现就非常简单了:

这第三个也是最后一个时钟源是,它的实现位于源文件。前文已经看到过函数x86_late_time_init,以及的初始化函数,也从这个开始,这个函数调用了tsc_init() 。

上面的宏检查在内核初始化时填充的boot_cpu_data数组中的给定位,这里是X86_FEATURE_TSC_DEADLINE_TIMER。如果处理器支持Time Stamp Counter,通过调用同一源代码文件中的calibrate_tsc函数来获得TSC的频率,该函数会尝试从不同的时钟源获得频率,如,通过校准等等,之后为系统中所有处理器初始化频率和比例因子。

这个函数在device期间调用。这样做是为了确保TSC 时钟源在时钟源之后被注册。 在这之后,所有三个时钟源都在 clocksource框架中注册,TSC时钟源将被选为当前时钟源,因为它其他时钟源中具有最高等级。

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

.

本章
sysfs
Time Stamp Counter
High Precision Event Timer
ACPI Power Management Timer
Time Stamp Counter
本章第二节内容
ACPI
High Precision Event Timer(高精度事件定时器)
Time Stamp Counter(时间戳计数器)
CLOCK_SOURCE_VALID_FOR_HRES
High Precision Event Timer
x86
arch/x86/kernel/hpet.c
init/main.c
arch/x86/kernel/time.c
arch/x86/kernel/x86_init.c
programmable interval timer
IRQ
ACPI HPET
IA-PC HPET (High Precision Event Timers) Specification
arch/x86/kernel/time.c
i8259
IRQ0
High Precision Event Timer
ACPI Power Management Timer
drivers/clocksource/acpi_pm.c
initcall
arch/x86/kernel/acpi/boot.c
ACPI
ACPI
Time Stamp Counter
arch/x86/kernel/tsc.c
Time Stamp Counter
MSR
programmable interval timer
initcall
HPET
本章
x86
本章
系统调用
0xAX
email
issue
linux-insides
x86
sysfs
Time Stamp Counter
High Precision Event Timer
ACPI Power Management Timer (PDF)
frequency
dmesg
programmable interval timer
IRQ
IA-PC HPET (High Precision Event Timers) Specification
IRQ0
i8259
initcall
previous part