1. 中断基础概念:什么是中断、中断上下文、中断号与IRQ

大家好,我是你们的Linux内核驱动讲师。今天咱们来聊聊中断——这个在驱动开发里躲不开、绕不过的核心话题。

说实话,我刚入行那会儿,对中断的理解就是“CPU停下手里的事,去处理别的事”。这个说法没错,但太粗糙了。你想想看,如果中断处理不好,整个系统可能就崩了。我在项目中就遇到过因为中断处理耗时太长,导致系统响应变慢,最后被客户投诉的惨痛经历。

1.1 什么是中断

中断,说白了就是一种硬件通知机制。当外设(比如网卡、键盘、定时器)需要CPU关注时,它会通过一根物理信号线,向CPU发送一个电信号。CPU收到这个信号后,会暂停当前正在执行的程序,转而去执行一个专门的中断处理函数。

为什么要用中断?

  • 提高效率:CPU不用一直轮询外设状态,省下的时间可以干别的活
  • 实时响应:紧急事件能立刻得到处理,比如按键按下、数据包到达
  • 资源节约:不用浪费CPU周期去“查岗”

核心理解:中断是异步事件。CPU不知道它什么时候来,但必须保证来了之后能正确处理。这就像你正在写代码,突然电话响了——你得先接电话,处理完再回来继续写。

1.2 中断上下文

这个概念很多新手容易搞混。我刚开始学的时候也绕了好一阵子。

Linux内核把程序的执行环境分为两种:进程上下文中断上下文

对比项 进程上下文 中断上下文
可睡眠 可以(调用schedule) 绝对不行
可被抢占 可以 不可以(除非中断嵌套)
持有锁 可以使用互斥锁 只能用自旋锁
访问用户空间 可以 不行

嗯,这里要注意:中断上下文里绝对不能睡眠。为什么?因为中断上下文没有进程控制块(task_struct),你没法调度它。我曾经见过一个同事在中断处理函数里调用了kmalloc(GFP_KERNEL),结果系统直接死锁——因为kmalloc可能会触发内存回收,而内存回收需要睡眠。

避坑指南:在中断上下文中,只能使用GFP_ATOMIC标志分配内存。我建议你养成习惯,写中断处理函数时,先问自己一句:“这个操作会睡眠吗?”

1.3 中断号与IRQ

每个中断源都有一个唯一的编号,这就是中断号。在Linux里,我们通常叫它IRQ号(Interrupt ReQuest)。

IRQ号是怎么来的?

  • 硬件层面:芯片厂商在设计SoC时,会给每个外设中断分配一个硬件中断号。比如GPIO中断可能是IRQ 32,定时器中断可能是IRQ 16。
  • 软件层面:Linux内核会把这些硬件中断号映射成软件IRQ号。这个映射关系由中断控制器(如GIC、APIC)管理。

我记得在调试一个ARM平台的项目时,发现设备树里写的中断号和实际硬件对不上。查了两天才发现,是中断控制器级联导致的偏移。从那以后,我每次拿到新板子,第一件事就是去读芯片手册里的中断映射表。

个人经验:查看系统当前的中断分配情况,可以用 cat /proc/interrupts。这个文件会列出每个IRQ号对应的中断次数、CPU亲和性等信息。调试中断问题时,这是我第一个会看的文件。

1.4 中断处理流程(简化版)

一个典型的中断处理流程是这样的:

  1. 外设产生中断信号
  2. 中断控制器接收信号,判断优先级
  3. 中断控制器通知CPU
  4. CPU保存当前上下文(寄存器、程序计数器等)
  5. CPU跳转到中断向量表,找到对应的处理函数
  6. 执行中断处理函数(上半部)
  7. 恢复上下文,继续执行原来的程序

这里有个关键点:中断处理函数要尽量短。为什么?因为中断处理期间,CPU是关中断的(至少在当前CPU上)。如果你处理太久,其他中断就进不来了。

我见过一个极端的案例:某驱动工程师在中断处理函数里做了500ms的数据处理,结果导致网卡丢包率飙升到30%。后来改成用下半部(tasklet或workqueue)处理,丢包率直接降到0.1%。

1.5 中断号分配示例

在设备树里,中断号的配置通常长这样:

// 设备树片段
uart0: serial@10000000 {
    compatible = "my-company,uart";
    reg = <0x10000000 0x1000>;
    interrupts = <0 33 4>;  // 中断控制器类型、中断号、触发方式
    interrupt-parent = <&gic>;
};

这里的 33 就是硬件中断号。但到了Linux内核里,它可能会被映射成另一个IRQ号。你可以在驱动里用 platform_get_irq() 来获取这个IRQ号:

int irq_num;
irq_num = platform_get_irq(pdev, 0);
if (irq_num < 0) {
    dev_err(&pdev->dev, "Failed to get IRQ\n");
    return irq_num;
}

// 注册中断处理函数
ret = request_irq(irq_num, my_irq_handler, 
                  IRQF_TRIGGER_RISING, 
                  "my_uart", dev);

关键点request_irq() 的最后一个参数是设备指针,用于中断共享时的区分。千万别传NULL,否则中断共享时会出问题。我吃过这个亏,调试了整整一个下午。

1.6 小结

好了,咱们把中断的基础概念捋了一遍:

  • 中断是硬件通知CPU的异步机制
  • 中断上下文不能睡眠,只能使用自旋锁
  • IRQ号是中断的唯一标识,从设备树或硬件手册获取
  • 中断处理要快,复杂逻辑交给下半部

下一章,咱们会深入中断处理函数的编写,包括上半部和下半部的分工。到时候我会分享一个我实际项目中用过的中断处理框架,保证实用。

记住一句话:中断处理就像接电话——快速记录,挂断后再处理细节