这篇文章 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 MID,Intel 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设备。
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_time
和get_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
字段描述了计数器的不同属性,指向suspend
和resume
函数的指针等等。
让我们看一下在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,
};
我们可以在这里看到默认名称的定义 – jiffies
。接下来是rating
字段,它允许通过指定硬件可用的时钟源管理代码选择最佳注册时钟源。 rating
可能具有以下设置为以下值:
1-99
– 仅用于启动和测试目的;100-199
– 实用的功能,但不是理想的。200-299
– 正确可用的clockource。300-399
– 一个相当快速和准确的时钟源。400-499
– 理想的时钟源。必要时必须使用; 例如,时间戳计数器的rating
为300
,但高精度事件计时器的rating
是250
。下一个字段是read
– 它是指向允许它读取clocksource的循环值的函数的指针;换句话说,它只返回u64
类型的jiffies
变量:
static u64 jiffies_read(struct clocksource * cs)
{
return(u64)jiffies;
}
备注:
函数返回值调整 由circle_t ->u64
下一个字段是mask
值,它确保来自非64位
计数器的计数器值之间的减法不需要特殊的溢出逻辑。在我们的例子中,掩码是 CLOCKSOURCE_MASK(32)
值为0xffffffff
并且它是32
位。这意味着jiffy
在42
秒后回绕到零:
>>> 0xffffffff
4294967295
# 42 nanoseconds
>>> 42 * pow(10, -9)
4.2000000000000006e-08
# 43 nanoseconds
>>> 43 * pow(10, -9)
4.3e-08
接下来的两个字段mult
和shift
用于将clocksource的周期转换为每个周期的纳秒。当内核调用clocksource.read
函数时,这个函数返回一个machine
时间单位的值,用我们刚才看到的u64
数据类型表示。要将此返回值转换为纳秒,我们需要这两个字段:mult
和shift
。 clocksource
提供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_SHIFT
和TICK_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
结构的定义,我们对jiffies
和clocksource
了解得很多,现在是时候回到我们函数的实现了。在本部分的开头,我们已经停止了以下的调用:
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_jiffies
和clocksource_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_jiffies
的rating
为 – 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位值的机器完成的。 如果我们可以访问jiffies
或jiffies_64
变量,我们可以将它转换为human
时间单位。为了得到一秒,我们可以使用以下表达式:
jiffies / HZ
所以,如果我们知道这一点,我们可以获得任何时间单位。例如:
/ *从现在开始三十秒* /
jiffies + 30 * HZ
/ *从现在开始两分钟* /
jiffies + 120 * HZ
/ *从现在起一毫秒* /
jiffies + HZ / 1000
就这样。
结论
第一部分总结了Linux内核中与时间和时间管理相关的概念。我们首先遇到了两个概念及其初始化:jiffies
和clocksource
。在下一部分中,我们将继续深入探讨这个有趣的主题,正如我在本部分已经写过的那样,我们将尝试深入理解Linux内核中这些和其他时间管理概念。
如果您有任何问题或建议,请随时在twitter 0xAX上ping我,给我发电子邮件email或者只创建问题。
请注意,英语不是我的第一语言,我很抱歉给您带来不便。如果您发现任何错误,请将PR发送至linux-insides。
Be First to Comment