linux-sides-Timers and time management-Introduction

这篇文章 Timers and time management in the Linux kernel. Part 1. 是出自 linux-insides一书中 Timers and time management 章节 Introduction

内核版本比对5.3-rc 进行了相关调整, 增加相关备注

Linux内核中的定时器和时间管理. Part 1.

介绍

这是另一篇文章,它在linux-insides一书中开启了新的篇章。前面的部分描述了系统调用概念,现在是时候开始新篇章了。正如人们可能从标题中理解的那样,本章将专门讨论Linux内核中的“定时器”和“时间管理”。当前章节的主题选择并非偶然。定时器(通常是时间管理)非常重要,并且在Linux内核中广泛使用。 Linux内核使用计时器执行各种任务,例如TCP实现中的不同超时,内核知道当前时间,调度异步函数,下一个事件中断调度和还有更多。 因此,我们将开始学习本部分中不同时间管理相关内容的实现。我们将看到不同类型的计时器以及不同的Linux内核子系统如何使用它们。与往常一样,我们将从Linux内核的最早部分开始,并完成Linux内核的初始化过程。我们已经在特殊的章节-Kernel initialization process中完成了它,它描述了Linux内核的初始化过程,但是你可能还记得我们错过了那里有些东西。其中一个是定时器的初始化。 我们开始吧。

初始化非标准PC硬件时钟

Linux内核解压缩后(更多关于此内容,您可以在内核解压缩部分中阅读)非特定代码开始在init/main.c源代码文件中工作。初始化lock validator后,初始化cgroups并设置canary值,我们可以看到setup_arch函数的调用。 你可能还记得,这个函数(定义在arch/x86/kernel/setup.c)准备/初始化特定于体系结构的东西(例如它为bss部分保留一个位置,为initrd保留一个位置,解析内核命令行以及许多其他事情)。除此之外,我们还可以找到一些时间管理相关的功能。 首先是:

x86_init.timers.wallclock_init();

我们在描述Linux内核初始化的章节中已经看到了x86_init结构。此结构包含指向不同平台的默认设置功能的指针,如 Intel MIDIntel CE4100
x86_init结构定义在arch/x86/kernel/x86_init.c,正如您所看到的,它默认确定标准PC硬件。 我们可以看到,x86_init结构具有x86_init_ops类型,它为平台特定的设置提供了一组函数,如保留标准资源,平台特定的内存设置,中断处理程序的初始化等。这种结构如下所示:

struct x86_init_ops {
    struct x86_init_resources resources;
    struct x86_init_mpparse mpparse;
    struct x86_init_irqs irqs;
    struct x86_init_oem oem;
    struct x86_init_paging paging;
    struct x86_init_timers timers;
    struct x86_init_iommu iommu;
    struct x86_init_pci pci;
    struct x86_hyper_init hyper;
    struct x86_init_acpi acpi;
};

备注
新增以下两个成员

   struct x86_hyper_init hyper; /*x86 hypervisor init functions*/
   struct x86_init_acpi acpi;   /*x86 ACPI init functions*/

注意具有x86_init_timers类型的timers字段。我们可以通过其名称理解该字段与时间管理和计时器有关。 x86_init_timers包含四个字段,这些字段都是在void上返回指针的函数:

  • setup_percpu_clockev - 为boot cpu设置每个cpu时钟事件设备;
  • timer_init - 初始化平台计时器;
  • wallclock_init - 初始化wallclock设备。

备注
x86_init_timers删除以下一个成员

   void (*tsc_pre_init)(void);  

因此,正如我们已经知道的那样,在我们的例子中,wallclock_init执行wallclock设备的初始化。如果我们查看x86_init结构,我们看到wallclock_init指向x86_init_noop

struct x86_init_ops x86_init __initdata = {
    ...
    ...
    ...
    .timers = {
        .wallclock_init         = x86_init_noop,
    },
    ...
    ...
    ...
}

用于标准PC硬件. x86_init_noop只是一个什么都不做的函数:

void __cpuinit x86_init_noop(void){}

实际上,wallclock_init功能用于Intel MID平台。 x86_init.timers.wallclock_init的初始化位于arch/x86/platform/intel-mid/intel-mid.c源代码文件的函数x86_intel_mid_early_setup中:

void __init x86_intel_mid_early_setup(void)
{
    ...
    ...
    ...
    x86_init.timers.wallclock_init = intel_mid_rtc_init;
    ...
    ...
    ...
}

intel_mid_rtc_init函数的实现在arch/x86/platform/intel-mid /intel_mid_vrtc.c源代码文件,看起来很简单。首先,这个函数解析简单固件接口M-Real-Time-Clock表,用于将这些设备送到sfi_mrtc_array数组并初始化set_timeget_time函数:

void __init intel_mid_rtc_init(void)
{
    unsigned long vrtc_paddr;

    sfi_table_parse(SFI_SIG_MRTC, NULL, NULL, sfi_parse_mrtc);

    vrtc_paddr = sfi_mrtc_array[0].phys_addr;
    if (!sfi_mrtc_num || !vrtc_paddr)
        return;

    vrtc_virt_base = (void __iomem *)set_fixmap_offset_nocache(FIX_LNW_VRTC,
                                vrtc_paddr);

    x86_platform.get_wallclock = vrtc_get_time;
    x86_platform.set_wallclock = vrtc_set_mmss;
}

就是这样,在此之后,基于“Intel MID”的设备将能够从硬件时钟中获得时间。正如我已经写过的那样,标准PC x86_64架构不支持x86_init_noop,在调用此函数时什么都不做。我们刚看到实时时钟Intel MID体系结构,现在是时候回到一般的现在是时候回到x86_64体系结构,并将在那里查看时间管理相关的东西。

熟悉jiffies

如果我们返回setup_arch函数(如你所记得的那样,在arch/x86/kernel/setup.c源代码文件),我们看到下次调用时间管理相关函数:

register_refined_jiffies(CLOCK_TICK_RATE);

在我们查看此函数的实现之前,我们必须了解jiffy。我们可以在维基百科上阅读:

Jiffy是一个非正式术语,表示不具体的非常短暂的时间段

这个定义非常类似于Linux内核中的jiffy。有一个带有“jiffies”的全局变量,它保存自系统启动以来发生的滴答数。 Linux内核将此变量设置为零:

extern unsigned long volatile __jiffy_data jiffies;

在初始化过程中。在定时器中断期间,每次都会增加此全局变量。除此之外,在jiffies变量附近我们可以看到类似变量的定义

extern u64 jiffies_64;

实际上,Linux内核中只使用其中一个变量,它取决于处理器类型。x86_64平台是u64类型,x86平台下是unsigned long类型。我们看到这个看arch/x86/kernel/vmlinux.lds.S链接器脚本:

#ifdef CONFIG_X86_32
...
jiffies = jiffies_64;
...
#else
...
jiffies_64 = jiffies;
...
#endif

x86_32的情况下,jiffies将是jiffies_64变量的低32位。我们可以将其想象两个变量的关系,见图示

                    jiffies_64
+-----------------------------------------------------+
|                       |                             |
|                       |                             |
|                       |       jiffies on x86_32   |
|                       |                             |
|                       |                             |
+-----------------------------------------------------+
63                     31                             0

现在我们知道关于jiffies的一点理论,并且可以返回到我们的函数。我们的函数没有特定于体系结构的实现 - register_refined_jiffies。此函数位于通用内核代码 - kernel/time/jiffies.c源代码文件中。 register_refined_jiffies的要点是jiffyclockource的注册。在我们查看register_refined_jiffies函数的实现之前,我们必须知道clocksource是什么。我们可以在评论中看到:

clocksource是自由运行计数器的硬件抽象。 

我不确定,但那个描述并没有对clocksource概念有很好的理解。让我们试着理解它是什么,但我们不会更深入,因为这个主题将在更详细的单独部分中描述。 clocksource的要点是计时抽象或非常简单的说-它为内核提供时间值。我们已经知道了jiffies接口,它表示自系统启动以来发生的滴答数。它由Linux内核中的全局变量表示,并在每个定时器中断后触发其值递增。 Linux内核可以使用jiffies进行时间测量。那么为什么我们需要像clocksource这样的单独环境?实际上,不同的硬件设备提供其功能不同的不同时钟源。用于时间间隔测量的更精确技术的可用性取决于硬件。

例如,x86在芯片上有一个64位计数器,称为时间戳计数器,其频率可以等于处理器频率。或者例如高精度事件计时器,其由至少10 MHz频率的64位计数器组成。两个不同的计时器,他们为x86提供计时服务。如果我们将添加来自其他架构的计时器,这只会使这个问题变得更加复杂。 Linux内核提供了clocksource概念来解决问题。

clocksource概念由Linux内核中的clocksource结构表示。此结构在include/linux/clocksource.h头文件中定义,并包含几个描述的字段时间计数器。例如,它包含 -name字段,它是计数器的名称,flags字段描述了计数器的不同属性,指向suspendresume函数的指针等等。

让我们看一下在kernel/time/jiffies.c源中定义的jiffies的clocksource结构代码文件:

static struct clocksource clocksource_jiffies = {
    .name       = "jiffies",
    .rating     = 1, 
    .read       = jiffies_read,
    .mask       = CLOCKSOURCE_MASK(32),
    .mult       = TICK_NSEC << JIFFIES_SHIFT, 
    .shift      = JIFFIES_SHIFT,
    .max_cycles = 10,
};

备注
.mult 由NSEC_PER_JIFFY 改为 TICK_NSEC

.mask由 xffffffff 改为宏 CLOCKSOURCE_MASK(32)


我们可以在这里看到默认名称的定义 - jiffies。接下来是rating字段,它允许通过指定硬件可用的时钟源管理代码选择最佳注册时钟源。 rating可能具有以下设置为以下值:

  • 1-99 - 仅用于启动和测试目的;
  • 100-199 - 实用的功能,但不是理想的。
  • 200-299 - 正确可用的clockource。
  • 300-399 - 一个相当快速和准确的时钟源。
  • 400-499 - 理想的时钟源。必要时必须使用; 例如,时间戳计数器rating300,但高精度事件计时器rating250。下一个字段是read- 它是指向允许它读取clocksource的循环值的函数的指针;换句话说,它只返回u64类型的jiffies变量:
static u64 jiffies_read(struct clocksource * cs)
{
        return(u64)jiffies;
}

备注
函数返回值调整 由circle_t ->u64


下一个字段是mask值,它确保来自非64位计数器的计数器值之间的减法不需要特殊的溢出逻辑。在我们的例子中,掩码是 CLOCKSOURCE_MASK(32)值为0xffffffff并且它是32位。这意味着jiffy42秒后回绕到零:

>>> 0xffffffff
4294967295
# 42 nanoseconds
>>> 42 * pow(10, -9)
4.2000000000000006e-08
# 43 nanoseconds
>>> 43 * pow(10, -9)
4.3e-08

接下来的两个字段multshift用于将clocksource的周期转换为每个周期的纳秒。当内核调用clocksource.read函数时,这个函数返回一个machine时间单位的值,用我们刚才看到的u64数据类型表示。要将此返回值转换为纳秒,我们需要这两个字段:multshiftclocksource提供clocksource_cyc2ns函数,它将使用以下表达式为我们执行:

((u64) cycles * mult)>> shift;

我们可以看到mult字段等于:

TICK_NSEC << JIFFIES_SHIFT

#define TICK_NSEC       ((NSEC_PER_SEC+HZ/2)/HZ)
#define NSEC_PER_SEC    1000000000L

默认情况下,shift

#if HZ <34
  #define JIFFIES_SHIFT 6
#elif HZ <67
  #define JIFFIES_SHIFT 7
#else
  #define JIFFIES_SHIFT 8
#endif

jiffies时钟源使用TICK_NSEC乘数转化为以纳秒为单位的时钟频率。


备注
clocksource.read读取的机器的时钟滴答为circle,纳秒为单位时钟频率为F, 每个周期的纳秒为t为:

t = cirle / F

如果使用上述的公式

t = (cycle * mult) >> shift;

运算只要满足以下公式

F = (1 << shift) / mult;

而mult值

mult= TICK_NSEC << shift;

分子等于1, 分母等于一个滴答需要的纳秒数,分别左移shift位,放大${2}^{shift}$倍, 结果为F为单位的频率值


注意,JIFFIES_SHIFTTICK_NSEC的值取决于HZ值。 HZ表示系统计时器的频率。这个宏在include/asm-generic/param.h中定义,取决于CONFIG_HZ内核配置选项。对于每个支持的体系结构,HZ的值不同,但对于x86,它的定义如下:

#define HZ CONFIG_HZ

CONFIG_HZ可以是以下值之一:

 

  •  

这意味着在我们的情况下,定时器中断频率为250HZ或每秒发生250次滴答或每4ms发生一次定时器中断。

我们在clocksource_jiffies结构的定义中可以看到的最后一个字段是 -max_cycles,它保存了最大循环值,可以安全地乘以而不会导致溢出。

好的,我们刚刚看到了clocksource_jiffies结构的定义,我们对jiffiesclocksource了解得很多,现在是时候回到我们函数的实现了。在本部分的开头,我们已经停止了以下的调用:

register_refined_jiffies(CLOCK_TICK_RATE);

函数来自arch/x86/kernel/setup.c源代码文件。

正如我已经写过的那样,register_refined_jiffies函数的主要目的是注册refined_jiffies clocksource。我们已经看到clocksource_jiffies结构代表了标准的jiffies时钟源。现在,如果你查看kernel/time/jiffies.c源代码文件,你会发现另一个时钟源定义:

struct clocksource refined_jiffies;

refined_jiffiesclocksource_jiffies之间有一个区别:基于标准jiffies的时钟源是应该在所有系统上运行的最低公分母时钟源。正如我们所知,每次定时器中断都会使得jiffies的值递增。这意味着基于标准jiffies的时钟源具有与定时器中断频率相同的分辨率。由此我们可以理解,基于标准jiffies的时钟源可能会受到不准确的影响。refined_jiffies使用CLOCK_TICK_RATE作为jiffies shift的基础。

我们来看看这个函数的实现。首先,我们可以看到基于clocksource_jiffies结构的refined_jiffies时钟源:

int register_refined_jiffies(long cycles_per_second)
{
    u64 nsec_per_tick, shift_hz;
    long cycles_per_tick;

    refined_jiffies = clocksource_jiffies;
    refined_jiffies.name = "refined-jiffies";
    refined_jiffies.rating++;
    ...
    ...
    ...

在这里我们可以看到,我们将refined_jiffies的名称更新为refined-jiffies并增加该结构的rating。你还记得,clocksource_jiffiesrating为 - 1,所以我们的refined_jiffies clockource 的rating则为 - 2。这意味着refined_jiffies将是时钟源管理的最佳选择。 在下一步中,我们需要计算每个ticket的周期数:

cycles_per_tick = (cycles_per_second + HZ/2)/ HZ;

请注意,我们使用NSEC_PER_SEC宏作为标准jiffies乘数的基础。这里我们使用cycles_per_second,这是register_refined_jiffies函数的第一个参数。我们已将CLOCK_TICK_RATE宏传递给register_refined_jiffies函数。该宏在arch/x86/include/asm/timex.h头文件中定义并扩展到:

#define CLOCK_TICK_RATE PIT_TICK_RATE

其中PIT_TICK_RATE宏扩展到Intel 8253的频率:

#define PIT_TICK_RATE 1193182ul

在此之后,我们为register_refined_jiffies计算shift_hz,它的值为hz << 8或换句话说系统计时器的频率。我们将cycles_per_second或可编程间隔定时器的频率向左移动到8以获得额外的准确度:

shift_hz =(u64)cycles_per_second << 8;
shift_hz + = cycles_per_tick/2;
do_div(shift_hz,cycles_per_tick);

在下一步中,我们通过将NSEC_PER_SEC左移8来计算每个刻度的秒数,就像我们使用shift_hz一样,并执行与之前相同的计算:

nsec_per_tick =(u64)NSEC_PER_SEC << 8;
nsec_per_tick + =(u32)shift_hz/2;
do_div(nsec_per_tick,(u32)shift_hz);
refined_jiffies.mult = ((u32)nsec_per_tick)<< JIFFIES_SHIFT;

register_refined_jiffies函数的末尾,我们使用在include/linux/clocksource.h中定义的__clocksource_register函数注册新的时钟源头文件并返回:

__clocksource_register(&refined_jiffies);
return 0;

时钟源管理代码提供用于时钟源注册和选择的API。我们可以看到,通过在内核初始化期间调用__clocksource_register函数或从内核模块调用时钟源。在注册期间,时钟源管理代码将使用clocksource.rating字段选择系统中可用的最佳时钟源,我们在为jiffies初始化clocksource结构时已经看到了该字段。

使用jiffies

我们刚刚在前一段中看到了两个基于jiffies的时钟源的初始化:

standard jiffies的时钟源;
refined jiffies时钟源;

如果您不理解这里的计算,请不要担心。起初他们看起来很可怕。很快,我们会一步一步地学习这些东西。所以,我们刚刚看到基于jiffies的时钟源的初始化,我们也知道Linux内核有全局变量jiffies,它保存自内核开始工作以来发生的滴答数。现在,让我们看看如何使用它。要使用jiffies,我们可以使用jiffies全局变量的名称或get_jiffies_64函数的调用。此函数在kernel/time/jiffies.c源代码文件中定义,只返回完整的64位``jiffies的价值:

u64 get_jiffies_64(void)
{
    unsigned long seq;
    u64 ret;

    do {
        seq = read_seqbegin(&jiffies_lock);
        ret = jiffies_64;
    } while (read_seqretry(&jiffies_lock, seq));
    return ret;
}
EXPORT_SYMBOL(get_jiffies_64);

请注意,get_jiffies_64函数未实现为jiffies_read,例如:

static u64 jiffies_read(struct clocksource * cs)
{
    return(u64)jiffies;
}

我们可以看到get_jiffies_64的实现更复杂。使用seqlocks实现jiffies_64变量的读取。实际上这是针对无法原子读取完整64位值的机器完成的。 如果我们可以访问jiffiesjiffies_64变量,我们可以将它转换为human时间单位。为了得到一秒,我们可以使用以下表达式:

jiffies / HZ

所以,如果我们知道这一点,我们可以获得任何时间单位。例如:

/ *从现在开始三十秒* /
jiffies + 30 * HZ

/ *从现在开始两分钟* /
jiffies + 120 * HZ

/ *从现在起一毫秒* /
jiffies + HZ / 1000

就这样。

结论

第一部分总结了Linux内核中与时间和时间管理相关的概念。我们首先遇到了两个概念及其初始化:jiffiesclocksource。在下一部分中,我们将继续深入探讨这个有趣的主题,正如我在本部分已经写过的那样,我们将尝试深入理解Linux内核中这些和其他时间管理概念。

如果您有任何问题或建议,请随时在twitter 0xAX上ping我,给我发电子邮件email或者只创建问题

请注意,英语不是我的第一语言,我很抱歉给您带来不便。如果您发现任何错误,请将PR发送至linux-insides

链接

Be First to Comment

发表评论

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