Timers and time management in the Linux kernel. Part 6

这篇文章 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 

我们可以看到有三个注册的时钟源在这个系统里:

现在让我们看看第二个文件,它提供了最佳时钟源(在系统中具有最佳评级的时钟源)

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

tscTime Stamp Counter的简写. second part有过描述, 他描述了Linux Kernel的clocksource框架, 系统最好的时钟源应当是最有最好或最高功率,频率的, 或者说是具有最高frequency.

ACPI电源管理计时器的频率为3.579545 MHz。 High Precision Event Timer的频率至少为10 MHzTime 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 PMHigh Precision Event Timer的频率。 我们可以看到,额定值最高或频率最高的时钟源是系统当前的值。

您可以注意到,除了这三个时钟源之外,我们在/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架构的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.cx86_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 Timerhpet_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_addressACPI 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_SIZE1024 字节:

#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;

我们需要获取此数字,以便为HPETGeneral 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 是频率是250clocksource结构 (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_disabledx86/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 &amp;= ~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 = &amp;clocksource_tsc;
        clocksource_register_khz(&amp;clocksource_tsc, tsc_khz);
unreg:
        clocksource_unregister(&amp;clocksource_tsc_early);
        return 0;
    }

    schedule_delayed_work(&amp;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

发表评论

电子邮件地址不会被公开。 必填项已用*标注