4、中断与定时器初始化:local_irq_enable的时机、时钟中断丢失导致的启动hang

中断和定时器,是内核启动的「心跳」。

我见过太多新手,甚至是有几年经验的工程师,在这块栽跟头。说白了,中断开早了,系统乱套;开晚了,系统死等。时钟中断一旦丢了,整个系统就像没了脉搏的人,直接「hang」在那里,一动不动。

今天咱们就聊聊这两个核心问题:local_irq_enable() 到底该什么时候调用?时钟中断丢了又该怎么查?

4.1 local_irq_enable 的「黄金窗口」

先问个问题:你会在初始化完中断控制器后,立刻打开本地中断吗?

我以前也这么干过。结果呢?中断一开,外设的中断请求像潮水一样涌进来。可这时候,内存管理还没准备好,进程调度器还没初始化,甚至连中断处理函数都还没注册完。系统直接 panic,或者更惨——悄无声息地死掉。

⚠️ 核心原则:local_irq_enable() 不是越早越好,而是要等到「中断处理所需的最小基础设施」就绪之后。

我个人习惯,把中断使能时机放在下面几个关键点之后:

  1. 中断控制器初始化完成(比如 GIC、APIC 的配置)
  2. 内核栈和当前进程的 task_struct 已建立(中断上下文需要它)
  3. 关键的中断处理函数已注册(至少 timer、IPI 这些不能少)
  4. 内存分配器基本可用(比如 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,我都会对照着它来排查。

中断与定时器初始化核心流程 1. 早期中断向量设置 (early_trap_init) 2. 中断控制器初始化 (init_IRQ) 3. 时钟源初始化 (clocksource_init) ⚠️ 此时仍不可开中断! 4. 定时器中断注册 (request_irq for timer) 5. local_irq_enable() — 黄金窗口打开 ✅ 此时开中断,时钟中断可正常处理 6. 调度器初始化、控制台初始化等后续工作 ❌ 常见错误位置 ✅ 正确时机

从这张图可以清楚看到:local_irq_enable() 必须在时钟中断注册完成之后,才能调用。 否则,时钟中断来了,内核找不到处理函数,要么 panic,要么直接忽略——然后系统就 hang 了。

4.5 调试技巧:如何定位时钟中断丢失

如果你遇到启动 hang,怀疑是时钟中断的问题,可以按下面的步骤来:

  1. 加打印,确认时钟中断 handler 是否被调用
    // 在时钟中断处理函数入口加
    static irqreturn_t timer_interrupt(int irq, void *dev_id) {
        pr_info("timer irq received at jiffies=%lu\n", jiffies);
        // ... 原有处理逻辑
    }
  2. 检查中断使能状态
    // 在怀疑的位置插入
    if (irqs_disabled()) {
        pr_warn("IRQs are disabled at %s:%d\n", __FILE__, __LINE__);
    }
  3. 确认时钟源是否在计数
    // 读取时钟源寄存器,看数值是否变化
    u32 cnt = readl(timer_base + TIMER_CNT);
    pr_info("timer counter = 0x%x\n", cnt);
    // 等一小会儿再读一次,看是否变化
  4. 检查 /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。

⚠️ 最后提醒:调试启动 hang,耐心是第一位的。别急着改代码,先确认「中断到底来没来」。用 printk 打点,用示波器量引脚,用 JTAG 看寄存器——手段越多,定位越快。

好了,关于中断与定时器初始化,核心就是把握好 local_irq_enable 的时机,以及确保时钟中断不丢失。下次遇到启动 hang,先按这个思路排查,大概率能省下半天时间。