[toc]
kernel/time/sched_clock.c 调度器时钟(Scheduler Clock) 高性能的调度与追踪时间戳
历史与背景
这项技术是为了解决什么特定问题而诞生的?
kernel/time/sched_clock.c
提供的核心功能 sched_clock()
是为了解决内核中一个对性能极其敏感的需求:以极低的开销,获取一个高分辨率、单调递增的时间戳。
jiffies
虽然开销低,但其分辨率太差(毫秒级),完全无法满足现代调度器的需求。特别是对于CFS(完全公平调度器),它需要精确地知道一个任务在CPU上到底运行了多少纳秒,以便公平地更新其虚拟运行时间(vruntime
)。如果时间测量不准,CFS的公平性就无从谈起。
此外,内核内部的其他高性能子系统也需要类似的时间戳:
- 性能追踪与分析:像
ftrace
和perf
这样的工具,在记录内核事件时,需要为每个事件打上一个极其精确的时间戳,以便分析微秒甚至纳秒级的性能瓶颈。 - 锁竞争分析:内核的锁调试器需要精确测量一个任务在等待一个自旋锁上花费了多长时间。
sched_clock()
就是为了满足这些场景而设计的。它被定位为一个“快速但不一定完美”的时钟,其首要设计目标是读取速度。
它的发展经历了哪些重要的里程碑或版本迭代?
sched_clock()
的发展史,是一部内核与底层硬件时钟源(clocksources)不断“斗智斗勇”的历史,尤其是在x86平台上。
- TSC的引入:在x86架构上,最快、分辨率最高的时钟源是CPU的时间戳计数器(Timestamp Counter, TSC)。早期的
sched_clock()
实现直接依赖于此。 - “不稳定TSC”问题:这是一个巨大的挑战,也是
sched_clock()
复杂化的主要原因。在多核、多插槽(multi-socket)、支持变频(frequency scaling)和深度睡眠状态的现代CPU上,TSC会变得“不稳定”:- 不同CPU核心的TSC可能没有同步,导致一个进程在核心间迁移后,时间戳“回退”或“跳跃”。
- CPU频率变化时,TSC的递增速率可能也会变化。
- CPU进入深度睡眠状态后,TSC可能会停止递增。
- 时钟稳定化与包装:为了解决这些问题,内核发展出了一套复杂的机制。
sched_clock.c
中的代码会与时钟源子系统(kernel/time/clocksource.c
)协作,在系统启动时对硬件TSC进行检查。如果TSC是稳定且同步的(现代CPU大多如此),sched_clock()
就会直接使用它。如果不是,内核会通过复杂的算法对其进行“包装”和“校准”,或者干脆放弃TSC,转而使用一个更慢但更稳定的硬件时钟源(如HPET)。 - 作为通用接口:
sched_clock()
已经从一个调度器专用的内部函数,演变为一个内核通用的、代表“当前CPU上最快的单调时钟”的接口。
目前该技术的社区活跃度和主流应用情况如何?
sched_clock()
是内核时间子系统和调度器子系统的绝对核心。
- 主流应用:它被内核中所有对性能和延迟极其敏感的代码路径所使用:
- CFS调度器:在每次上下文切换和时钟中断时,都会调用
sched_clock()
来精确更新进程的运行时间统计。 - 内核追踪框架(ftrace, perf):所有追踪事件的时间戳都默认来自
sched_clock()
。 - 自旋锁和互斥锁的慢速路径:用于测量锁的争用时间。
- CFS调度器:在每次上下文切换和时钟中断时,都会调用
核心原理与设计
它的核心工作原理是什么?
sched_clock()
本身是一个函数,而不是一个像jiffies
那样的全局变量。它的核心原理是提供一个到系统底层最快、最稳定的硬件计数器的抽象访问接口。
- 抽象层:
sched_clock()
是一个抽象函数。它内部会调用当前系统选定的、最合适的底层时钟源读取函数。 - 硬件时钟源:
- 在现代x86系统上,这通常是直接读取TSC寄存器(通过
rdtsc
汇编指令)。 - 在ARM64系统上,这通常是读取通用的系统计数器(
CNTVCT_EL0
)。 - 在其他架构上,则是该架构提供的最高效的计数器。
- 在现代x86系统上,这通常是直接读取TSC寄存器(通过
- 单调性与Per-CPU特性:
sched_clock()
保证在其返回的值在同一个CPU上是单调递增的。内核通过软件逻辑确保即使底层硬件计数器发生回绕,sched_clock()
的返回值也不会回退。- 一个至关重要的特性:
sched_clock()
不保证在不同CPU之间是同步的。在同一物理时刻,CPU0和CPU1上调用sched_clock()
返回的值可能会有微小的偏差。这是为了追求极致的读取速度而做出的权衡,因为它避免了昂贵的跨CPU同步操作。
- 纳秒单位:无论底层的硬件计数器是什么频率,
sched_clock()
都会通过乘法和移位运算,将其转换为标准的纳秒单位返回。
它的主要优势体现在哪些方面?
- 极低的读取开销:这是其最重要的优势。在理想情况下(如稳定的TSC),调用
sched_clock()
可能只涉及几条汇编指令,几乎和读取一个内存变量一样快。 - 纳秒级高分辨率:提供了内核能从硬件获取的最高时间分辨率。
- 单调性:保证了在单个CPU上的时间测量不会倒退,非常适合测量时间间隔。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 缺乏跨CPU同步保证:这是其最大的“劣势”或说“设计特点”。不能简单地用CPU1上的
sched_clock()
值减去CPU0上的值来精确测量跨CPU的事件延迟。 - 非全局稳定:其起点(0点)是任意的,且在系统重启后会变化。它只适合测量时间差,不适合表示绝对时间点。
- 不是定时器:它是一个读取当前时间的工具,本身不提供任何在未来某个时间点触发事件的功能。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
sched_clock()
是内核内部在性能敏感路径上进行高精度时间间隔测量的首选方案。开发者通常不直接调用它,而是通过使用依赖于它的子系统来间接使用。
- CFS更新vruntime:这是最核心的场景。当一个任务将要被换下CPU时,调度器会这样计算它本次的运行时间:
delta_exec = sched_clock_cpu(cpu) - current->sched_info.last_arrival;
。 - ftrace记录事件:当一个被追踪的内核函数被调用时,ftrace会立即调用
sched_clock()
来获取事件的起始时间戳。 - 测量代码段执行时间:在内核中进行性能分析时,开发者会使用
start = sched_clock(); ... code ...; end = sched_clock(); duration = end - start;
来精确测量代码执行耗时。
是否有不推荐使用该技术的场景?为什么?
- 需要跨CPU同步的场景:如果需要测量一个事件在CPU0上开始,在CPU1上结束的时间间隔,使用
sched_clock()
会引入误差。这种场景应该使用ktime_get()
,它基于CLOCK_MONOTONIC
,是全局同步的,但开销也更高。 - 需要设置未来事件(定时器):应使用
hrtimers
或timer_list
。 - 需要与用户空间或网络对端同步的绝对时间:应使用基于
CLOCK_REALTIME
的时间API。
对比分析
请将其 与 其他相似技术 进行详细对比。
特性 | sched_clock() |
jiffies |
ktime (基于 CLOCK_MONOTONIC ) |
---|---|---|---|
核心用途 | 极低开销的高精度时间间隔测量。 | 低分辨率的内核内部时间流逝和定时器基础。 | 全局同步的高精度时间戳和定时器基础。 |
分辨率 | 高 (纳秒级)。 | 低 (毫秒级, 由HZ 决定)。 |
高 (纳秒级)。 |
开销 | 极低。 | 极低 (读取全局变量)。 | 中等 (比sched_clock 高,需要处理时钟源回绕和同步)。 |
跨CPU同步性 | 不保证。 | 保证 (因为是单一全局变量)。 | 保证 (核心设计目标之一)。 |
单调性 | 保证 (在单个CPU上)。 | 保证 (在回绕前)。 | 保证。 |
适用场景 | 调度器、ftrace、内核性能微调。 | 低精度超时、传统内核定时器。 | 高精度定时器(hrtimers )、需要跨CPU同步的精确测量。 |
include/linux/sched_clock.h
clock_read_data
sched_clock_register
时,写入这块数据内容
1 | /** |
local_clock
1 | static __always_inline u64 local_clock(void) |
kernel/time/sched_clock.c
clock_data
sched_clock_register
时,写入这块数据内容.默认的clock_data只有mult和read_sched_clock
1 | static struct clock_data cd ____cacheline_aligned = { |
jiffy_sched_clock_read
1 | static u64 notrace jiffy_sched_clock_read(void) |
__sched_clock 返回时钟
1 | static __always_inline unsigned long long __sched_clock(void) |
sched_clock 返回调度时间值
1 | unsigned long long notrace sched_clock(void) |
update_clock_read_data 更新读取时钟所需的数据
- 它通过双缓冲机制(odd/even 数据副本)和序列计数器(sequence counter)来确保在多核或中断上下文中读取时钟数据的安全性和一致性。该机制特别适用于高并发环境.
- 双缓冲机制:
- cd.read_data[0] 和 cd.read_data[1] 分别表示偶数副本和奇数副本。
- 在更新过程中,读者会被引导到奇数副本(read_data[1]),而偶数副本(read_data[0])则被安全地更新。
- 更新完成后,读者会被切换回偶数副本,奇数副本作为备用
- 序列计数器的使用
write_seqcount_latch_begin
和 write_seqcou`nt_latch_end 用于管理序列计数器,确保读者在更新过程中不会观察到不一致的数据。write_seqcount_latch
用于切换读者的目标副本(从奇数副本切换回偶数副本)。
1 | /* |
sched_clock_register 调度时钟注册
1 | void __init |
update_sched_clock 原子更新 sched_clock() epoch。
1 | /* |
generic_sched_clock_init
1 | void __init generic_sched_clock_init(void) |
Scheduler Clock Suspend/Resume Handling (sched_clock_suspend, sched_clock_resume)
本代码片段实现了 Linux 内核中 sched_clock
(调度器时钟)在系统核心挂起(suspend)和恢复(resume)期间的关键处理逻辑。其核心功能是通过 syscore_ops
框架,在系统进入睡眠前“冻结”sched_clock
的值,使其在整个挂起过程中返回一个固定不变的时间戳;在系统恢复后,再重新读取硬件时钟,并“解冻”sched_clock
,使其恢复正常计时。这确保了内核调度器及其他依赖 sched_clock
的子系统在电源状态转换期间看到的时间是一致且单调的。
实现原理分析
此机制的核心是一种基于函数指针替换的动态行为改变技术,用于在不修改 sched_clock()
hot-path 代码的情况下,改变其在特定状态下的行为。
Syscore Ops 注册:
sched_clock_syscore_init
通过register_syscore_ops
将sched_clock_ops
注册到内核的系统核心操作链表中。syscore_ops
是一种特殊的电源管理回调,它在设备驱动挂起之前(early suspend)和设备驱动恢复之后(late resume)执行,用于处理最核心的、与具体设备无关的系统状态。时间冻结 (
sched_clock_suspend
):update_sched_clock()
: 首先调用此函数,将当前最新的硬件时钟值同步到sched_clock
的内部状态变量(如epoch_cyc
)中。这捕获了系统挂起前的最后一个精确时间点。hrtimer_cancel(&sched_clock_timer)
: 取消用于周期性更新sched_clock
(例如处理硬件计数器回绕)的高精度定时器。因为系统即将睡眠,这个定时器不再需要。rd->read_sched_clock = suspended_sched_clock_read;
: 这是最关键的一步。它将sched_clock
内部实际用于读取时间的函数指针,从正常的硬件读取函数(cd.actual_read_sched_clock
)切换到了一个特殊的suspended_sched_clock_read
函数。
返回冻结值 (
suspended_sched_clock_read
): 这个被换上的函数行为极其简单:它不访问任何硬件,而是直接返回在sched_clock_suspend
中捕获并保存的epoch_cyc
值。因此,在系统挂起的整个过程中,任何对sched_clock()
的调用都将返回同一个、被“冻结”的时间戳。时间解冻 (
sched_clock_resume
):rd->epoch_cyc = cd.actual_read_sched_clock();
: 恢复后的第一件事就是立即读取底层的硬件时钟,并用这个全新的值更新epoch_cyc
,为sched_clock
建立一个新的时间基点。hrtimer_start(...)
: 重新启动之前被取消的周期性更新定时器。rd->read_sched_clock = cd.actual_read_sched_clock;
: 将函数指针切换回正常的硬件读取函数,sched_clock
从此恢复动态计时。
代码分析
1 | // suspended_sched_clock_read: 在时钟挂起期间使用的时钟读取函数。 |