4、中断与定时器初始化:local_irq_enable的时机、时钟中断丢失导致的启动hang
中断和定时器,是内核启动的「心跳」。
我见过太多新手,甚至是有几年经验的工程师,在这块栽跟头。说白了,中断开早了,系统乱套;开晚了,系统死等。时钟中断一旦丢了,整个系统就像没了脉搏的人,直接「hang」在那里,一动不动。
今天咱们就聊聊这两个核心问题:local_irq_enable() 到底该什么时候调用?时钟中断丢了又该怎么查?
4.1 local_irq_enable 的「黄金窗口」
先问个问题:你会在初始化完中断控制器后,立刻打开本地中断吗?
我以前也这么干过。结果呢?中断一开,外设的中断请求像潮水一样涌进来。可这时候,内存管理还没准备好,进程调度器还没初始化,甚至连中断处理函数都还没注册完。系统直接 panic,或者更惨——悄无声息地死掉。
我个人习惯,把中断使能时机放在下面几个关键点之后:
- 中断控制器初始化完成(比如 GIC、APIC 的配置)
- 内核栈和当前进程的 task_struct 已建立(中断上下文需要它)
- 关键的中断处理函数已注册(至少 timer、IPI 这些不能少)
- 内存分配器基本可用(比如 slab 分配器已初始化)
你想想看,如果第 3 条没满足,时钟中断来了,内核去找 handler,结果发现是个空指针——这不就崩了吗?
实战经验:我在调试某 ARM64 开发板时,发现内核在 start_kernel() 里 early_trap_init() 之后立刻开了中断。结果串口中断来了,但 console 驱动还没初始化,打印信息全丢了。排查了整整两天,最后发现是中断使能位置太靠前。
4.2 时钟中断丢失:启动 hang 的头号元凶
时钟中断,是内核调度、时间管理、延迟执行的基础。如果它丢了,系统会怎么样?
嗯,最典型的现象就是:内核打印完 "Calibrating delay loop..." 之后,就再也没有然后了。
为什么会这样?因为内核在启动过程中,有很多地方需要「忙等」一段时间。比如 calibrate_delay() 函数,它依赖 jiffies 的更新。如果时钟中断没来,jiffies 永远不变,这个循环就永远跑不完。
💡 小技巧:遇到启动 hang,先看最后一条打印信息。如果停在和时间校准相关的地方,十有八九是时钟中断的问题。
4.3 时钟中断丢失的常见原因
我总结了一下,时钟中断丢失通常有这几种情况:
| 原因分类 | 具体表现 | 排查方向 |
|---|---|---|
| 中断控制器配置错误 | 时钟源的中断号没正确映射到 CPU | 检查 dts 或 ACPI 中的 interrupt 属性 |
| 时钟源本身未启动 | 定时器硬件没开始计数 | 查看 clocksource 驱动中的 ->init() 是否被调用 |
| 中断被意外屏蔽 | local_irq_enable() 之后又被别的代码关了 | 用 WARN_ON(irqs_disabled()) 打点排查 |
| 中断处理函数未注册 | request_irq() 没调用或返回错误 | 检查 /proc/interrupts 或内核日志 |
| CPU 间中断(IPI)问题 | 多核系统中,时钟中断只发给了某个离线 CPU | 检查 CPU 热插拔路径中的中断亲和性设置 |
我曾经遇到过一个特别隐蔽的 case:某 MIPS 平台,时钟中断用的是 GPIO 模拟的。结果 GPIO 控制器在时钟中断注册之后才初始化,导致中断信号根本送不到 CPU。嗯,这种「依赖顺序」问题,在嵌入式平台上特别常见。
4.4 核心流程:中断与定时器初始化顺序
下面这张图,是我自己画的中断与定时器初始化核心流程。每次调试启动 hang,我都会对照着它来排查。
从这张图可以清楚看到:local_irq_enable() 必须在时钟中断注册完成之后,才能调用。 否则,时钟中断来了,内核找不到处理函数,要么 panic,要么直接忽略——然后系统就 hang 了。
4.5 调试技巧:如何定位时钟中断丢失
如果你遇到启动 hang,怀疑是时钟中断的问题,可以按下面的步骤来:
- 加打印,确认时钟中断 handler 是否被调用
// 在时钟中断处理函数入口加 static irqreturn_t timer_interrupt(int irq, void *dev_id) { pr_info("timer irq received at jiffies=%lu\n", jiffies); // ... 原有处理逻辑 } - 检查中断使能状态
// 在怀疑的位置插入 if (irqs_disabled()) { pr_warn("IRQs are disabled at %s:%d\n", __FILE__, __LINE__); } - 确认时钟源是否在计数
// 读取时钟源寄存器,看数值是否变化 u32 cnt = readl(timer_base + TIMER_CNT); pr_info("timer counter = 0x%x\n", cnt); // 等一小会儿再读一次,看是否变化 - 检查 /proc/interrupts(如果系统能进 shell)
cat /proc/interrupts | grep timer // 看 timer 中断的计数是否在增长
💡 我的经验:有一次调试,我发现时钟中断 handler 被调用了,但 jiffies 就是不更新。最后发现是 tick_handle_periodic() 里有个条件判断没满足,导致 jiffies_64 没被递增。这种「假性丢失」比真丢失更难查,建议在 jiffies 更新路径上也加个打印。
4.6 避坑指南:我踩过的几个坑
- 坑一:local_irq_enable 放在 time_init() 之前
我曾经在某个 ARM 平台上,把 local_irq_enable() 放在了 time_init() 前面。结果 time_init() 里需要等待硬件就绪,但等待函数依赖 jiffies,jiffies 又依赖时钟中断——死锁了。
- 坑二:多核系统中,只在一个核上注册了时钟中断
启动时只有 boot CPU 在跑,看起来没问题。但等 secondary CPU 上线后,它没有时钟中断,调度器无法工作。系统能启动,但跑一会儿就 hang。
- 坑三:中断控制器级联配置错误
有些 SoC 用两个 GIC 级联,时钟中断挂在 secondary GIC 上。如果 primary GIC 的转发配置不对,中断根本到不了 CPU。
好了,关于中断与定时器初始化,核心就是把握好 local_irq_enable 的时机,以及确保时钟中断不丢失。下次遇到启动 hang,先按这个思路排查,大概率能省下半天时间。