内核时间管理基础知识
本文将简要解释一些基本的内核时间管理抽象概念。它部分涉及内核树中通常在drivers/clocksource
中找到的驱动程序,但代码可能分布在整个内核中。
如果你在内核源代码中使用grep
命令,你会发现一些特定于体系结构的时钟源、时钟事件的实现,以及一些类似的体系结构特定的sched_clock()
函数覆盖和一些延迟定时器。
为了为你的平台提供时间管理,时钟源提供了基本的时间线,而时钟事件在这个时间线上的某些点上触发中断,提供诸如高分辨率定时器之类的功能。sched_clock()
用于调度和时间戳,延迟定时器使用硬件计数器提供准确的延迟源。
时钟源
时钟源的目的是为系统提供一个时间线,告诉你当前时间。例如,在Linux系统上发出date
命令最终会读取时钟源来确定确切的时间。
通常,时钟源是一个单调的原子计数器,它提供从0到(2^n)-1的n位计数,并在达到最大值后重新从0开始计数。只要系统运行,它理想情况下永远不会停止计数。在系统暂停期间它可能会停止计数。
时钟源的分辨率应尽可能高,频率应尽可能稳定和正确,与真实世界的挂钟相比。它不应该在时间上不可预测地来回移动,也不应该偶尔丢失几个周期。
它必须免疫硬件中发生的影响,例如在总线上以两个阶段读取计数器寄存器,首先读取最低的16位,然后在第二个总线周期中读取更高的16位,计数器位在两次读取之间可能被更新,导致计数器产生非常奇怪的值。
当时钟源的挂钟精度不令人满意时,有各种技巧和时间管理代码层用于例如将用户可见时间与系统中的RTC时钟或使用NTP的网络时间服务器进行同步,但它们基本上只是更新针对时钟源的偏移量,这为系统提供了基本的时间线。这些措施不会影响时钟源本身,它们只是使系统适应了时钟源的缺陷。
时钟源结构应提供一种将提供的计数器转换为纳秒值的方法,作为无符号长长整型(unsigned 64位)数字。由于这个操作可能经常被调用,严格的数学计算并不理想:相反,使用乘法和移位等算术运算将数字尽可能接近纳秒值,因此在clocksource_cyc2ns()
中你会找到:
ns ~= (clocksource * mult) >> shift
你会在时钟源代码中找到一些辅助函数,旨在帮助提供这些mult
和shift
值,例如clocksource_khz2mult()
、clocksource_hz2mult()
,它们有助于从固定的移位确定mult
因子,以及clocksource_register_hz()
和clocksource_register_khz()
,它们将使用时钟源的频率作为唯一输入来帮助分配shift
和mult
因子。
对于从单个I/O内存位置访问的真正简单的时钟源,现在甚至有clocksource_mmio_init()
,它将获取一个内存位置、位宽、一个参数,告诉寄存器中的计数器是递增还是递减,以及定时器时钟速率,然后产生所有必要的参数。
由于比如一个100 MHz的32位计数器在大约43秒后会重新从0开始计数,处理时钟源的代码将不得不对此进行补偿。这就是为什么时钟源结构还包含一个'mask'成员,告诉源的多少位是有效的。这样,时间管理代码就知道计数器何时会重新开始计数,并且可以在重新开始计数点的两侧插入必要的补偿代码,以使系统时间线保持单调。
时钟事件
时钟事件在概念上与时钟源相反:它们接受所需的时间规范值,并计算要插入硬件定时器寄存器的值。
时钟事件与时钟源是正交的。相同的硬件和寄存器范围可以用于时钟事件,但它本质上是一个不同的东西。驱动时钟事件的硬件必须能够触发中断,以便在系统时间线上触发事件。在SMP系统上,最理想的(也是习惯的)是每个CPU核心都有一个这样的事件驱动定时器,以便每个核心可以独立于任何其他核心触发事件。
你会注意到时钟事件设备代码基于相同的基本思想,使用mult
和shift
算术来将计数器转换为纳秒,你会再次找到相同系列的辅助函数,用于分配这些值。然而,时钟事件驱动程序不需要'mask'属性:系统不会尝试计划超出时钟事件时间范围的事件。
sched_clock()
除了时钟源和时钟事件之外,内核中还有一个特殊的弱函数,名为sched_clock()
。这个函数应返回自系统启动以来的纳秒数。一个体系结构可能会提供自己的sched_clock()
实现,也可能不提供。如果没有提供本地实现,系统节拍计数器将被用作sched_clock()
。
顾名思义,sched_clock()
用于调度系统,例如在CFS调度器中确定某个进程的绝对时间片。它还用于printk
时间戳,当你选择在printk
中包含时间信息时,例如用于启动图表。
与时钟源相比,sched_clock()
必须非常快:它被调用的频率更高,特别是由调度器调用。如果你必须在精度和时钟源之间进行权衡,你可以在sched_clock()
中牺牲精度以换取速度。但它需要一些与时钟源相同的基本特征,即它应该是单调的。
sched_clock()
函数只能在无符号长长整型边界上进行包装,即在64位之后。由于这是一个纳秒值,这意味着它在大约585年后会重新开始计数。(对于大多数实际系统来说,这意味着"永远"。)
如果一个体系结构没有提供自己的此函数的实现,它将回退到使用节拍数,使其最大分辨率为体系结构的节拍频率的1/HZ。这将影响调度的准确性,并可能会在系统基准测试中显示出来。
驱动sched_clock()
的时钟在系统暂停/休眠期间可能会停止或重置为零。这对它服务的系统上的调度事件并不重要。但它可能会导致printk()
中的有趣时间戳。
sched_clock()
函数应该可以在任何上下文中调用,包括中断和NMI安全,并在任何上下文中返回合理的值。
一些体系结构可能具有有限的时间源集,并且缺少一个好的计数器来推导64位纳秒值,因此例如在ARM体系结构上,已经创建了特殊的辅助函数,用于从16位或32位计数器提供sched_clock()
纳秒基数。有时也会使用与时钟源相同的计数器来实现此目的。
在SMP系统上,对性能来说,sched_clock()
可以在每个CPU上独立调用而不会有任何同步性能损失是至关重要的。一些硬件(例如x86 TSC)会导致sched_clock()
函数在系统上的不同CPU之间漂移。内核可以通过启用CONFIG_HAVE_UNSTABLE_SCHED_CLOCK
选项来解决这个问题。这是使sched_clock()
与普通时钟源不同的另一个方面。
延迟定时器(仅适用于某些体系结构)
在具有可变CPU频率的系统上,各种内核延迟函数有时会表现出奇怪的行为。基本上,这些延迟通常使用硬循环来延迟一定数量的节拍分数,使用在启动时校准的“lpj”(每节拍循环次数)值。
希望在校准此值时你的系统正在以最大频率运行:当频率降低到全频率的一半时,任何延迟()将是原来的两倍长。通常这不会有影响,因为你通常请求的延迟量或更多。但基本上在这些系统上,语义是相当不可预测的。
进入基于定时器的延迟。使用这些,可以使用定时器读取来提供所需的延迟,而不是使用硬编码的循环。
这是通过声明一个struct delay_timer
并为这个延迟定时器分配适当的函数指针和速率设置来实现的。
这在一些体系结构上可用,如OpenRISC或ARM。