【linux内核专栏 08】时间与定时器

本篇定位 :定时器从裸机"一个 mtimecmp"跳到"hrtimer + clocksource + tick + NOHZ"多层。本篇把"Linux 时间子系统"对你已知的增量讲透。读完能写 hrtimer 驱动、能配 NOHZ 省电、能解释 FreeRTOS tick 和 Linux tick 的差异。
FreeRTOS tick 是 Linux 时间子系统的简化版

FreeRTOS tick:你配 mtimecmp,周期中断 → xTaskIncrementTick → 调度。Linux 时间子系统复杂得多:clocksource (时钟源)+ clock_event (时钟事件)+ tick (调度心跳)+ hrtimer (高精度定时器)+ NOHZ(tickless 省电)。核心增量:① 多时钟源抽象;② 高精度定时器(纳秒级,不是固定 tick);③ tickless 省电(空闲时停 tick)。


一、时间子系统全景

复制代码
┌─────────────────────────────────────┐
│ 硬件时钟源(每个 SoC 多个)            │
│  - RISC-V: rdtime 寄存器(高精度计数)│
│  - ARM: Generic Timer(物理计数器)   │
│  - HPET / TSC(x86)                  │
└────────────┬────────────────────────┘
             │
   ┌─────────▼──────────┐
   │ clocksource        │  ← 读时间(单调递增计数器)
   │ (只读,提供 ns)     │
   └─────────┬──────────┘
             │
   ┌─────────▼──────────┐
   │ timekeeping        │  ← 维护当前时间(墙上时间/单调时间)
   └─────────┬──────────┘
             │
   ┌─────────▼──────────┐    ┌──────────────────┐
   │ tick 设备          │<───│ clock_event      │  ← 触发下次中断
   │ (周期/单次事件)    │    │ (可编程中断源)   │
   └─────────┬──────────┘    └──────────────────┘
             │
   ┌─────────▼──────────┐
   │ tick: 调度心跳     │  ← 周期中断,Scheduler_tick
   │ hrtimer: 高精度定时│  ← 红黑树,ns 级
   └────────────────────┘

三个核心抽象

抽象 作用 硬件
clocksource 读时间(单调递增计数器) RISC-V rdtime / ARM CNTVCT
clock_event_device 触发中断(可编程) RISC-V sbi_set_timer / ARM CNTV_TVAL
tick_device 基于 clock_event,周期或单次 调度心跳

二、clocksource:读时间

2.1 clocksource 是什么

  • 一个单调递增的计数器,提供"当前 ns"
  • 只读,不触发中断
  • RISC-V:rdtime 指令读 time CSR(你 \[05-中断系统 PLIC-CLINT-AIA] 学的 CLINT mtime)

2.2 RISC-V clocksource

c 复制代码
// arch/riscv/kernel/time.c(简化)
static u64 riscv_clocksource_read(struct clocksource *cs) {
    return get_cycles();   // 读 rdtime
}

static struct clocksource riscv_clocksource = {
    .name       = "riscv_clocksource",
    .rating     = 300,
    .read       = riscv_clocksource_read,
    .mask       = CLOCKSOURCE_MASK(64),
    .flags      = CLOCK_SOURCE_IS_CONTINUOUS,
};

2.3 多 clocksource 与 rating

  • 一个 SoC 可能有多个时钟源(TSC + HPET / rdtime + 外部 RTC)
  • 每个 clocksource 有 rating(质量分),内核选最高的
  • 运行时可切换(若更高 rating 的可用)

2.4 timekeeping:维护墙上时间

  • 基于 clocksource,维护 xtime(墙上时间,UTC)
  • 和 monotonic(自启动单调时间)
  • NTP 调整频率
bash 复制代码
# 看当前 clocksource
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
# arch_sys_counter (RISC-V rdtime)

# 可用 clocksource
cat /sys/devices/system/clocksource/clocksource0/available_clocksource

三、clock_event_device:触发中断

3.1 是什么

  • 可编程中断源:设"下次多久后中断"
  • 用于 tick(周期)或 hrtimer(单次精确)
  • RISC-V:SBI 调用设定时器(sbi_set_timer),底层写 mtimecmp

3.2 RISC-V timer 驱动

c 复制代码
// drivers/clocksource/timer-riscv.c(简化)
static int riscv_timer_set_next_event(unsigned long delta,
                                      struct clock_event_device *ce) {
    // 设下次中断 = 当前 rdtime + delta
    sbi_set_timer(get_cycles() + delta);
    return 0;
}

static struct clock_event_device riscv_clock_event = {
    .name           = "riscv_timer_clockevent",
    .features       = CLOCK_EVT_FEAT_ONESHOT,  // 单次模式
    .rating         = 100,
    .set_next_event = riscv_timer_set_next_event,
};

3.3 对照你 RISC-V mtimecmp

你裸机 Linux RISC-V timer
写 mtimecmp = mtime + delta sbi_set_timer(rdtime + delta)
中断 → 你 handler 中断 → hrtimer/tick 框架
直接寄存器 经 SBI(因为 Linux 在 S 态,mtimecmp 在 M 态)

Linux RISC-V timer 走 SBI

你裸机在 M 态直接写 mtimecmp。Linux 在 S 态,mtimecmp 是 M 态寄存器,不能直接写------经 SBI 调用(ecall to M),OpenSBI 替你写 mtimecmp。这是RISC-V 专题(S 态不能直接碰 M)在时间子系统的体现。


四、tick:调度心跳

4.1 tick 是什么

  • 周期性中断(默认 100/250/1000 Hz)
  • 每次 tick:① 更新时间统计;② scheduler_tick(检查时间片/调度);③ 处理过期定时器

4.2 tick 频率

bash 复制代码
# 看 CONFIG_HZ
cat /proc/interrupts | grep LOC  # LOC 是 local timer(tick)
# 数每秒增长 = HZ
HZ 周期 用途
100 10ms 服务器(吞吐,少切换)
250 4ms 桌面(默认)
1000 1ms 嵌入式/低延迟
300 ~3.3ms 某些

4.3 scheduler_tick

每次 tick(本核):

  1. 更新当前进程时间统计(utime/stime)
  2. 更新 CFS vruntime
  3. 检查时间片到没 → 设 need_resched
  4. 触发 RCU 回收检查

4.4 对照 FreeRTOS tick

FreeRTOS tick Linux tick
触发 mtimecmp 周期中断 clock_event 周期中断
做什么 xTaskIncrementTick + 调度 scheduler_tick + timekeeping
频率 configTICK_RATE_HZ(常 1000) HZ(100~1000)
时间片 优先级 + 抢占 CFS vruntime
你配 port.c 配 mtimecmp 内核自动(你选 HZ)

嵌入式视角:你 port 层配的就是 Linux tick 底层

FreeRTOS V11.1 port 层配 mtimecmp 周期中断 → Linux RISC-V 的 clock_event 设 mtimecmp(经 SBI)→ 同一硬件机制。增量:Linux 在上面建了 clocksource/timekeeping/hrtimer 多层;FreeRTOS 直接 tick 调度。你的 port 层经验直接迁移。


五、hrtimer:高精度定时器 ⭐

5.1 为什么需要 hrtimer

  • 传统定时器(timer_list)基于 tick,精度 = tick 周期(10ms/4ms/1ms)
  • 有些场景要 ns 级精度(音视频/网络/实时)
  • hrtimer(High-Resolution Timer)独立于 tick,基于 clocksource,ns 级

5.2 hrtimer 数据结构

  • 每 CPU 一个 hrtimer 红黑树(按到期时间排序)
  • 最左节点 = 最近到期
  • clock_event 设单次中断,到点触发 → 遍历过期 hrtimer → 回调

5.3 hrtimer 用法

c 复制代码
struct hrtimer my_timer;

// 回调
enum hrtimer_restart my_callback(struct hrtimer *t) {
    printk("Timer fired!\n");
    // 若要重复,return HRTIMER_RESTART 并重新 forward
    return HRTIMER_NORESTART;
}

// 初始化 + 启动
hrtimer_init(&my_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_timer.function = my_callback;
hrtimer_start(&my_timer, ms_to_ktime(100), HRTIMER_MODE_REL);  // 100ms 后

// 取消
hrtimer_cancel(&my_timer);

5.4 周期 hrtimer

c 复制代码
enum hrtimer_restart my_periodic(struct hrtimer *t) {
    // 重新设下次
    hrtimer_forward_now(t, ms_to_ktime(100));  // 再 100ms
    do_work();
    return HRTIMER_RESTART;
}

5.5 hrtimer 回调的约束

  • 在硬中断上下文(或 softirq,看模式)
  • 不能睡
  • 要快(影响精度)
  • 要睡用 hrtimer 的 softirq 模式或 workqueue 推迟

!important hrtimer 是你写定时驱动的首选

你裸机用 mtimecmp 做周期任务。Linux 用 hrtimer ------ns 级精度,红黑树管理,API 清晰。写驱动要周期任务(轮询/超时),优先 hrtimer。要睡的活从回调里 schedule_work 推迟。


六、timer_list(低精度定时器)

6.1 老接口,简单但精度低

c 复制代码
struct timer_list my_timer;
timer_setup(&my_timer, my_fn, 0);
my_timer.expires = jiffies + msecs_to_jiffies(100);  // 100ms 后(对齐 tick)
add_timer(&my_timer);

// 或
mod_timer(&my_timer, jiffies + HZ);  // 改到期
del_timer(&my_timer);
  • 基于 jiffies(tick 计数),精度 = tick
  • 新代码优先 hrtimer,老代码常用 timer_list

七、jiffies vs ktime

7.1 jiffies

  • 内核启动以来的 tick 数
  • HZ 次/秒,即 jiffies / HZ = 秒
  • 简单但精度低(tick 级)
c 复制代码
unsigned long timeout = jiffies + HZ;   // 1 秒后
if (time_after(jiffies, timeout)) { ... }  // 超时

7.2 ktime_t

  • 64 位 ns 级时间
  • hrtimer 用
c 复制代码
ktime_t kt = ktime_get();                    // 当前单调 ns
ktime_t t = ms_to_ktime(100);                // 100ms 转 ktime
s64 ns = ktime_to_ns(kt);
u64 us = ktime_get_ns() / 1000;              // 微秒

7.3 选哪个

场景 用
tick 级够(timer_list/超时判断) jiffies
ns 级(hrtimer/精确测量) ktime
墙上时间(用户态) gettimeofday/clock_gettime

八、延时:忙等 vs 睡眠

8.1 忙等(短延时,不能睡时用)

c 复制代码
udelay(10);    // 10 微秒忙等
mdelay(5);     // 5 毫秒忙等(不要长,占 CPU)
ndelay(100);   // 100 纳秒
  • 基于 CPU 自旋计数,不准但简单
  • 中断上下文/持锁时用(不能睡)
  • 别长(浪费 CPU)

8.2 睡眠(可睡时用)

c 复制代码
msleep(100);          // 100ms 睡(精度 tick)
msleep_interruptible(100);  // 可被信号打断
usleep_range(100, 200);     // 100-200us 睡(更省电,允许调度器合并)
schedule_timeout(timeout);  // 通用
  • 让出 CPU(不忙等)
  • 精度取决于调度(tick 级,usleep_range 稍好)
  • 进程上下文用

8.3 决策

复制代码
能睡? 
  是 ──> msleep / usleep_range / schedule_timeout
  否(中断/持锁) ──> udelay / mdelay(短)

中断里只能 udelay,不能 msleep

\[05-中断与下半部] 学的"中断不能睡"------中断里要延时只能 udelay(忙等)。msleep 会 schedule,中断里 schedule = panic。持自旋锁同理只能 udelay。


九、NOHZ(tickless,省电)⭐

9.1 为什么 NOHZ

  • tick 每 HZ 次中断,即使 CPU 空闲也打断
  • 空闲时 tick 浪费电(尤其嵌入式/移动)
  • NOHZ:CPU 空闲时停 tick,直到下次 hrtimer 或中断

9.2 NOHZ_IDLE(空闲 tickless)

  • CPU 进 idle 时关 tick
  • 下次中断(若有 hrtimer/IO)才唤醒
  • 省电,嵌入式必开

9.3 NOHZ_FULL(全 tickless)

  • 即使 CPU 跑任务也不 tick(除非该 CPU 只有任务)
  • 用于实时/HPC(减少 tick 干扰)
  • 需至少一个 CPU 保留做 timekeeping(dedicated housekeeping CPU)

9.4 配置

bash 复制代码
# 看 NOHZ
cat /proc/cmdline | grep nohz
# nohz=on / nohz_full=1-3(CPU1-3 全 tickless,CPU0 做 housekeeping)

# 看是否启用
cat /sys/devices/system/cpu/vulnerabilities/...  # 不相关,看 NOHZ:
zcat /proc/config.gz | grep NOHZ
# CONFIG_NO_HZ_IDLE=y / CONFIG_NO_HZ_FULL=y

9.5 对照 FreeRTOS tickless

FreeRTOS tickless Linux NOHZ_IDLE
空闲时 configUSE_TICKLESS_IDLE NOHZ_IDLE
停 tick portSUPPRESS_TICKS_AND_SLEEP 内核自动
配 mtimecmp 重设 你 port 层实现 clock_event 自动
省电 是 是

嵌入式视角:你 FreeRTOS tickless 就是 Linux NOHZ

FreeRTOS tickless idle(port 层 portSUPPRESS_TICKS_AND_SLEEP)------停 tick,设下次 mtimecmp,进 WFI。Linux NOHZ_IDLE 同构 ------idle 时停 tick,设下次 clock_event,进 idle(WFI)。你 port 层实现的 tickless,就是 Linux NOHZ 的简化版。增量:Linux 多了 NOHZ_FULL(任务也停 tick)。


十、用户态时间 API

10.1 时间获取

c 复制代码
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);    // 单调时间(自启动)
clock_gettime(CLOCK_REALTIME, &ts);     // 墙上时间(可被 NTP 调)
gettimeofday(&tv, NULL);                // 墙上时间(老接口)

// 高精度
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
  • CLOCK_MONOTONIC:单调(不回退),适合测耗时
  • CLOCK_REALTIME:墙上(可被改),适合时间戳

10.2 VDSO 加速

gettimeofday/clock_gettime(CLOCK_MONOTONIC|REALTIME) 走 VDSO(06 篇),不进内核。

10.3 命令行

bash 复制代码
date            # 墙上时间
uptime          # 启动以来
time ./cmd      # 测耗时

十一、延时与定时速查

需求 API 上下文
ns 级定时器 hrtimer_start 回调在硬中断
tick 级定时器 timer_list / add_timer 回调在 softirq
us 短延时 udelay 任意(忙等)
ms 短延时 mdelay 任意(忙等,别长)
ms 睡眠 msleep 进程
us 睡眠 usleep_range 进程
超时等待 schedule_timeout 进程
用户态时间 clock_gettime 用户
当前 ns ktime_get 内核
当前 jiffies jiffies 内核

十二、对照总表

概念 FreeRTOS RISC-V bare-metal Linux
时钟源 mtime rdtime/mtime clocksource(rdtime)
周期中断 tick(mtimecmp) 你配 mtimecmp tick(clock_event)
高精度定时 软定时器(基于 tick) 你写 hrtimer(ns 级)
tickless tickless idle 你写 WFI NOHZ_IDLE/FULL
延时忙等 N/A 你写 udelay/mdelay
延时睡眠 vTaskDelay N/A msleep/usleep_range
时间表示 tick count rdtime 值 jiffies / ktime
设置定时器 port 层 写 mtimecmp SBI 调用(经 M)

十三、本篇小结

  • 时间子系统三抽象:clocksource (读时间)+ clock_event (触发中断)+ tick_device(周期/单次)
  • RISC-V:clocksource 用 rdtime,clock_event 经 SBI 写 mtimecmp(Linux 在 S 态,不能直接写 M 态)
  • tick :周期中断,scheduler_tick 驱动调度;HZ=100~1000
  • hrtimer:ns 级高精度定时器,红黑树,新代码首选(回调在硬中断,不能睡)
  • timer_list:tick 级低精度,老接口
  • jiffies(tick 计数)vs ktime(ns),按精度选
  • 延时:udelay/mdelay 忙等(中断/持锁用),msleep/usleep_range 睡眠(进程用)
  • NOHZ:空闲停 tick 省电(NOHZ_IDLE),全 tickless(NOHZ_FULL,实时/HPC)
  • 用户态 clock_gettime 走 VDSO,不进内核
  • FreeRTOS tickless idle = Linux NOHZ_IDLE 同构

速查表

想干啥 用什么
读当前 ns ktime_get_ns()
读当前 jiffies jiffies
100ms 后的 jiffies jiffies + msecs_to_jiffies(100)
ns 级定时器 hrtimer_start(t, ns, MODE_REL)
tick 级定时器 timer_setup + add_timer
中断里延时 udelay(us)
进程里睡眠 msleep(ms) / usleep_range(min,max)
超时等待 schedule_timeout(jiffies)
看当前 clocksource cat /sys/devices/system/clocksource/clocksource0/current_clocksource
看 HZ zcat /proc/config.gz | grep CONFIG_HZ
配 NOHZ cmdline nohz=on / nohz_full=CPUs
用户态测耗时 clock_gettime(CLOCK_MONOTONIC)
看定时器 cat /proc/timer_list

💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。

如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。


相关推荐
想做小南娘,发现自己是女生喵1 小时前
Linux之Ubuntu入门篇知识总结
linux·运维·服务器
CHEEVEN_QY2 小时前
搅拌摩擦焊FSW工艺参数对焊缝成形影响的实验研究10.1
linux·服务器·数据库
帷幕落秋2 小时前
Redis哨兵模式
运维·数据库
H.莓飛2 小时前
【数据结构】链表_OJ题
linux·数据结构·算法·链表
木白CPP3 小时前
USB(一): USB 概述&&介绍
linux·网络·嵌入式硬件
熬夜敲代码的猫3 小时前
Linux:进程概念
linux
刘胡子大叔3 小时前
1Panel 部署 ThinkPHP8 踩坑实录
linux·ubuntu
anew___3 小时前
《从零手写操作系统 (21):用户态C运行时——从裸ELF到printf》
linux·服务器·c语言
吴声子夜歌3 小时前
Nginx应用与运维——Nginx编译及部署(部署)
java·运维·nginx