一、引言:时间作为系统脊柱
操作系统对时间的需求有两条线。一条是事件驱动的------调度器在 tick 处决定是否切换,hrtimer 在到期点唤醒任务;另一条是度量的------第 27 篇讨论的 perf 采样、协议超时、统计周期都依赖定时器中断。在 RISC-V 平台上,这条线由三段组成:硬件寄存器提供基准时间,SBI TIME 扩展充当 S 模式访问的代理,Sstc 扩展把代理绕过,三者通过 Linux 的 timer-riscv 驱动整合成内核可见的时钟源与事件设备。
理解这条链路是排查 tick 漂移、调度延迟与高分辨率定时器失效等问题的前提。
二、硬件层:time 与 mtime
RISC-V 的时间基准通过 time 寄存器读取,特权规范同时定义了 timeh 用于 RV32 的 64-bit 拼装。S 模式与 U 模式读 time CSR 时受 mcounteren.TM 位控制;若 M 模式未开启该位,访问触发非法指令异常。
裸机或 S 模式程序读 64-bit 时间值需要按规范给出的顺序避免撕裂:
c
static inline u64 read_time(void)
{
#if __riscv_xlen == 64
return csr_read(time);
#else
u32 hi1 = csr_read(timeh), lo;
do {
lo = csr_read(time);
u32 hi2 = csr_read(timeh);
if (hi1 == hi2) break;
hi1 = hi2;
} while (1);
return ((u64)hi1 << 32) | lo;
#endif
}
平台真正实现时间基准的方式则交由实现决定。多数 SoC 通过 CLINT 或 ACLINT 把一组 MMIO 寄存器暴露在物理地址空间:MTIME 是全局单调递增 64-bit 计数器,MTIMECMP 按 hart 排列,下一次计时器中断的时刻由 S 模式写入(实际由 SBI 或 Sstc 完成)。两条值得注意的语义:其一,MTIME 通常只在上电时清零,热复位不清;MTIMECMP 的初值因而也是不确定的 ,内核启动时必须先写入合法未来值,否则它会立刻持续触发计时器中断。其二,RV32 上 MTIME/MTIMECMP 是 64-bit 寄存器,S 模式没有原子读/写能力 ,与 time/timeh 拼装同理,固件或驱动需要按上述循环避免撕裂。
三、SBI TIME 扩展
S 模式无法直接写 mtimecmp(被禁止),这与计时器控制必须由 M 模式保留。SBI TIME 扩展(EID 0x54494D45,FID 0)定义了唯一的函数:
c
struct sbiret sbi_set_timer(uint64_t stime_value);
stime_value 是 64-bit 绝对时间值;调用语义是"在 time >= stime_value 时产生计时器中断"。无论 sie.STIE 是否屏蔽中断,调用都会清除挂起的计时器中断请求。要取消已排程的中断而不产生新的中断,要么传 (uint64_t)-1 把触发时间推到"无限未来",要么清 sie.STIE。该函数固定返回 SBI_SUCCESS。
这条规范文本背后是真实开销:M 模式固件需要把多个 S 模式虚拟计时器复用到单一物理 mtimecmp 上。每次 sbi_set_timer 调用都要 M 模式分配资源、更新 mtimecmp,再触发 ecall 返回。KVM 虚拟化场景下路径更长------HS 模式先发到 M 模式,VS 模式则要先发到 HS 再到 M,KVM 用软件定时器模拟 vstimecmp,延迟进一步放大。这正是 Sstc 扩展的动机。
注:SBI 0.2 之前的 legacy EID 0x00 提供同名 sbi_set_timer,当前实现必须以 0x54494D45 为优先。
四、Sstc 扩展:把代理绕过
Sstc 扩展的核心是新增 S 模式 CSR stimecmp(CSR 地址 0x14d),配 menvcfg.STCE 与 mcounteren.TM 双重开关:
| 条件 | 含义 |
|---|---|
menvcfg.STCE == 0 |
Sstc 全局关闭,S 模式写 stimecmp 触发非法指令 |
mcounteren.TM == 0 |
S 模式写 stimecmp 非法;读仍允许 |
| 两者皆 1 | S 模式可直接 csrw stimecmp, t0 |
挂起条件为 time >= stimecmp(按无符号整数比较)。RV32 还配 stimecmph 高半寄存器;超虚拟化扩展另有 vstimecmp。这条路径跳过 SBI ecall 与 M 模式固件,典型场景下计时器中断生成延迟可以下降一个数量级------虚拟化场景因绕开 KVM 模拟收益更大。
五、Linux 侧:timer-riscv 驱动
驱动主体位于 drivers/clocksource/timer-riscv.c。它把硬件时间(无论来自 time CSR 还是 MMIO)注册为 clocksource,把下一事件调度路径注册为 clock_event_device,并在初始化时探测 Sstc:
c
static DEFINE_STATIC_KEY_FALSE(riscv_sstc_available);
static int riscv_clock_next_event(unsigned long delta, struct clock_event_device *ce)
{
u64 next_tval = get_cycles64() + delta;
csr_set(CSR_IE, IE_TIE);
if (static_branch_likely(&riscv_sstc_available))
csr_write(CSR_STIMECMP, next_tval); /* RV64 简写,RV32 还需写 stimecmph */
else
sbi_set_timer(next_tval);
return 0;
}
static int __init riscv_timer_init_dt(struct device_node *n)
{
/* 注册 clocksource 与 clock_event_device */
if (riscv_isa_extension_available(NULL, SSTC)) {
static_branch_enable(&riscv_sstc_available);
pr_info("Timer interrupt in S-mode is available via sstc extension\n");
}
return 0;
}
static_branch_likely 让默认路径(无 Sstc)持续走 SBI 调用,新增路径(Sstc)只需一次额外分支。tickless(NO_HZ)内核在此基础上由 tick-sched.c 调度下一 tick,hrtimer 子系统复用同一路径。Sstc 支持随 v6.8 主线进入内核(提交 7ab52f75),目前主流平台都已默认启用。
六、QEMU 验证路径
QEMU virt 机器默认暴露 CLINT 至 ACLINT 兼容寄存器,Sstc 默认关闭:
bash
qemu-system-riscv64 -machine virt -smp 4 -nographic \
-cpu rv64,sstc=true \
-kernel Image -append "root=/dev/vda rw console=ttyS0"
-cpu rv64,sstc=true 开启 Sstc,启动日志中会打印 Timer interrupt in S-mode is available via sstc extension,表明静态键已被置位。下一步即可在 guest 中观察:
bash
cat /proc/timer_list | head -30
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
# 实测:riscv_clocksource_tsc(基于 CLINT/ACLINT 的 mtime)
比较 SBI 路径与 Sstc 路径的单次调用开销是验证 Sstc 收益的最直接方式:
bash
# 在 guest 中跑一段循环调用计时器接口的基准,查看 cycles 差
perf stat -e cycles,instructions -a ./bench
典型结果:SBI 调用路径单次 sbi_set_timer 落入数十至上百 cycles;切到 stimecmp CSR 写入后下降到个位 cycles。验证边界需明确:
| 验证对象 | QEMU 是否可验 | 说明 |
|---|---|---|
| SBI 链路与 sbi_set_timer 语义 | 可 | 接口与挂起位清除完整建模 |
| Sstc 切换后的延迟下降 | 可 | 比较两条路径即可 |
mtime 频率与漂移 |
部分 | 由 QEMU 节拍换算,不反映真机晶体精度 |
| 跨核虚拟化计时器延迟 | 部分 | 无 KVM 嵌套场景,KVM 路径收益是估算 |
七、度量方法论与协同
时钟子系统单独评估意义有限,与前几篇的能力配合才能形成判断。三个实践要点:
其一,tickless 精度由 mtime 频率与定时器中断粒度共同决定 。常见平台 mtime 跑 10 MHz 到 100 MHz 区间,对应 10 ns 到 100 ns 粒度;hrtimer 与 CONFIG_HIGH_RES_TIMERS 的开启需检查二者匹配。
其二,计时器中断是 perf 采样的时钟基准 (第 27 篇)。Sstc 把中断生成压到个位 cycles,对 perf record -F 100000 这类高频采样更友好------采样周期越短,定时器路径的延迟占比越大。
其三,迁移到 Sstc 后 SBI 路径并未消失 。固件兼容与 fallback 要求两种入口都保留;调试工具(如 perf record、ftrace)仍可能观察到 sbi_set_timer 调用------这是 fallback 在运行,而非缺陷。
八、常见坑位与总结
坑位一:忽略 MTIME/MTIMECMP 的初值不确定性。 上电或热复位后两者残留旧值,内核启动若不显式初始化 MTIMECMP,会立刻进入持续计时器中断循环,表现为系统在 tick 处理上跑满。
坑位二:S 模式直接写 mtimecmp。 触发异常,错误信息可能模糊指向非法指令;应通过 SBI 或 Sstc 设置。
坑位三:用 SBI legacy EID 0x00 路径。 0x2 版本起已替换为 0x54494D45,旧固件可能两者都接受,新固件可能仅支持新 EID。
坑位四:RV32 上 64-bit 读写未配循环。 time 两次读取之间可能发生低位回绕,导致合成 64-bit 值反向跳变。
坑位五:在 QEMU 上做时序精度评估。 mtime 由宿主节拍换算,频率与抖动反映宿主而非真机硬件;评估时钟精度必须以真机数据为准。
三个关键结论:其一,时间子系统是规范强制的最小集合加实现自由的最大集合 ------time/stimecmp 是规范,频率与物理布局是实现。其二,SBI TIME 与 Sstc 是同一职责的两种实现 ------前者通用且兼容,后者高性能但需要硬支持;Linux 通过 static_branch 让两者同时存在。其三,时钟子系统与中断/性能监控相互交织 ------sbi_set_timer 与 stimecmp CSR 都最终落到 mtime 比较,HPM 采样依赖定时器中断通路,任意一段异常都会向上传导。
延伸方向有两个:其一是 IPI 与远程 fence 在新版 SBI 中的边界,与时间子系统一同构成 S 模式跨核交互的完整图景;其二是 ACPI/CPPC 在 RISC-V 平台上的电源与频率缩放路径------定时器精度会被调频策略影响,需要在 SoC 与内核侧协同设计。