358|定时器:HPET 和时钟中断
金句:操作系统的时间,不是”看表”看出来的,是被中断”打断”出来的。每一次时钟中断,内核就知道又过了一个 jiffy——然后决定该让谁上场。
1. 三个时钟硬件
x86 平台有三种时钟硬件,按历史顺序:
1.1 RTC(Real Time Clock)
最古老,最慢,用于关机时也保持时间。
- 独立供电(纽扣电池),即使断电也不丢
- 只能设置较慢的频率(最高 8192Hz)
- 现在只用来记录墙上时间(年/月/日/时/分/秒)
1.2 PIT(Programmable Interval Timer)
8253/8254 芯片,IBM PC 时代的标准定时器。
- 只能设置 3 个独立计数器
- 最高频率约 1.193MHz(时钟晶振 14.31818MHz / 12 分频)
- Linux 早期用它产生系统时钟中断(IRQ 0)
- 现在已被 HPET 和 APIC 取代
1.3 HPET(High Precision Event Timer)
现代 PC 的标准时钟,2007 年后成为标配。
- 至少 10MHz 时钟基准(精度 100ns)
- 最多 32 个比较器(comparator),每个可独立触发不同中断
- 支持 64 位计数器,永不溢出(vs PIT 的 16 位计数器)
- 可以一次性编程多个定时器,不需要轮询
HPET 结构:
┌─────────────────────────────────────┐
│ Main Counter(64 位,10MHz+) │ 统一时钟源
└─────────────────────────────────────┘
├── Comparator 0 → IRQ 2(或映射到 APIC)
├── Comparator 1 → IRQ 3
├── Comparator 2 → IRQ 4
└── ...最多 32 个2. 时钟中断和 jiffies
Linux 内核有一个全局变量 jiffies:
volatile unsigned long jiffies;
// 记录系统启动以来经历的中断次数
// 每次时钟中断(timer tick)自动 +1如果 HZ = 100(常见配置),jiffies 每秒增加 100,即每个 jiffy = 10ms。
jiffies 增长过程:
0 → 1 → 2 → ... → 99 → 100 → ...
|____|← 1 个 jiffy = 10ms →|HZ 的选择:
- HZ=100:桌面系统(每 10ms 响应一次)
- HZ=1000:服务器/实时系统(每 1ms,更精细但 CPU 开销大)
- HZ=300:某些 Unix 系统传统
// 内核里的 HZ 定义(arch/x86/include/asm/param.h)
#ifdef CONFIG_HZ
# define HZ CONFIG_HZ
#else
# define HZ 100
#endif
// jiffies 的读取
#define get_jiffies() (jiffies)
// 从 jiffies 转换到秒/毫秒
int seconds = jiffies / HZ;
int ms = (jiffies * 1000) / HZ;3. 时钟中断的完整路径
硬件层(HPET → CPU)
HPET Comparator 0 触发(达到设置的周期值)
↓
APIC 接收 IRQ 0(时钟中断)
↓
CPU 执行 APIC 中断向量(通常是 0xEC = 236)
↓
CPU 跳到 IDT[236](trap gate)
↓
进入内核定时器中断处理函数内核层(IRQ 0 handler)
// arch/x86/entry/entry_64.S
IRQ_DOMAIN[236]:
irq_entries_start:
pushq $~0 // vector number
interrupt_request_0 // → do_timer(arch/x86/kernel/time.c)// kernel/time/timer.c
DEFINE_PER_CPU(struct tvec_base, tvec_bases);
void do_timer(void)
{
// 1. jiffies +1
++jiffies;
// 2. 更新墙上时间(从 RTC 同步)
update_wall_time();
// 3. 更新 CPU 时间统计
update_process_times(user_mode(get_irq_regs()));
// 4. 调度器运行时间检查(如果时间片用完)
scheduler_tick();
}4. schedule_tick:怎么让进程”时间片耗尽”?
每个进程有一个运行时间片(time slice),由调度器分配。
// kernel/sched/core.c
void scheduler_tick(void)
{
struct rq *rq = this_rq();
struct task_struct *p = current;
if (task_on_rq_queued(p)) {
// 减少当前进程的时间片
p->se.sum_exec_runtime += delta_exec;
p->sevruntime -= delta_exec;
// 时间片是否耗尽?
if (p->se.time_slice <= 0) {
// 重新计算时间片
resched_task(p); // 设置 TIF_NEED_RESCHED 标志
}
}
// 更新 rq 的负载信息
update_cpu_load_active();
}关键:resched_task(p) 设置了进程的 TIF_NEED_RESCHED 标志(thread_info 的 bit 0)。
这意味着:不强制立刻切换,只是打一个标记。等到内核代码的某个安全点(系统调用返回、中断返回),才会真正调用 schedule()。
5. timer wheel:O(1) 的定时器管理
Linux 的软件定时器(set_timeout/add_timer)不是用链表实现,而是用哈希定时器轮(timer wheel)。
struct timer_list {
struct list_head entry;
unsigned long expires; // 过期 jiffies
void (*function)(struct timer_list *);
unsigned long data;
};timer wheel 的结构:
timer_wheel 数组(5 层,每层覆盖不同时间范围):
tv1[256]: 0 ~ 255 jiffies (4KB ~ 2.5s)
tv2[64]: 256 ~ 16383 jiffies (2.5s ~ 163s)
tv3[64]: 16384 ~ 1048575 jiffies (163s ~ 17min)
tv4[64]: 1048576 ~ 67108863 jiffies (17min ~ 18h)
tv5[1]: 67108864+ jiffies (> 18h)每层的每个桶(bucket)是一个定时器链表,按过期时间排序。
插入定时器 O(1):根据 expires 计算该放到哪个桶,插入链表头。
过期检查:每次 do_timer 触发时,检查 tv1[current_jiffies % 256] 桶,如果有定时器过期,执行并删除。
如果低层桶的第一个定时器还有很久才到期,不会检查高层,减少遍历开销。
这就是为什么 Linux 可以管理上万个定时器而不会变慢。
6. 定时器的使用:setTimeout 怎么实现的?
用户态的 setTimeout(func, 1000ms) 背后:
// 用户态
setTimeout(func, 1000); // glibc 的 pthread 库
// glibc 内部(简化)
struct itimerspec t = {
.it_value.tv_sec = 1,
.it_value.tv_nsec = 0
};
timer_create(CLOCK_REALTIME, NULL, &timer_id);
timer_settime(timer_id, 0, &t, NULL);
// 1 秒后 SIGALRM 信号触发内核层(sys_timer_create + sys_timer_settime):
// kernel/time/posix-timers.c
asmlinkage sys_timer_create(clockid_t which_clock,
struct sigevent *event, timer_t *timer_id)
{
// 创建 timer(内核对象)
struct k_itimer *new_timer = alloc_kitimer();
// 注册到 clock 的 timerqueue
posix_timer_event(new_timer, 0);
return 0;
}
asmlinkage sys_timer_settime(timer_t timer_id, int flags,
const struct itimerspec *new_value, ...)
{
// 设置到期时间
new_timer->expires = timespec_to_jiffies(new_value->it_value);
// 加入 timer wheel(下次时钟中断时检查)
internal_add_timer(&new_timer->it_timer);
}到期时,内核发 SIGALRM 给进程,触发用户态的信号处理函数,执行注册的回调。
7. 延迟调度:schedule_timeout 怎么实现的?
Linux 提供一种”睡 N 个 jiffy 后再调度”的机制:
// 内核代码
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(5 * HZ); // 睡 5 秒
// 醒来后从这里继续执行实现原理:
schedule_timeout(5 * HZ):
1. 设置一个 5*HZ 后过期的定时器
2. 调用 schedule()(让出 CPU)
3. 被唤醒后,删除定时器(如果还没触发)
4. 继续执行如果 5 秒内有人调用 wake_up_process(),定时器被删除,进程提前被唤醒。这常用于等待某个事件(有超时上限)。
8. 总结
| 知识点 | 关键结论 |
|---|---|
| HPET | 现代标准高精度时钟,64 位计数器,32 个比较器 |
| 时钟中断 | HPET/APIC → IRQ 0 → do_timer |
| jiffies | 时钟中断次数的累计,每次 +1 |
| HZ | 每秒 jiffies 增量(100/300/1000) |
| schedule_tick | 减少时间片,时间片耗尽设 TIF_NEED_RESCHED |
| timer wheel | 5 层哈希定时器轮,O(1) 插入 |
| 定时器使用 | 过期后发 SIGALRM 信号 |
| schedule_timeout | 睡眠 + 定时器,到期自动唤醒或提前被 wake_up 唤醒 |
下篇预告(359):[待定 — 键盘输入中断到终端显示,或第一个用户态进程 / 上下文切换,选哪个?]
关注公众号「AI不着急」,回复”资料”获取内核学习路线图。
评论