2. Linux中断子系统架构:从硬件到软件的分层设计
好,咱们接着聊中断。上一章我讲了中断的基本概念,这一章咱们深入内核,看看Linux到底是怎么把中断这件事儿组织起来的。
说白了,中断子系统就是一条从硬件到软件的数据管道。硬件那边一有动静,软件这边就得响应。但问题是——硬件千奇百怪,软件又得统一处理,怎么办?
答案就是:分层。
2.1 三层架构:硬件、通用层、驱动层
我个人习惯把中断子系统分成三层来看:
- 硬件层:CPU、中断控制器(GIC、APIC这些)、外设。它们负责产生和传递中断信号。
- 通用中断层(Generic IRQ Layer):内核的核心代码,跟具体硬件无关。它管理中断号、中断描述符、中断流控。
- 驱动层:每个设备驱动注册自己的中断处理函数。这一层最贴近业务。
你想想看,如果没有这个分层,每个驱动都得自己去操作中断控制器的寄存器,那代码得多乱?而且换个CPU架构,所有驱动都得重写。这显然不现实。
核心思想:通用中断层把硬件差异封装起来,驱动只需要调用 request_irq() 或 request_threaded_irq() 就能注册中断处理函数。至于中断是怎么路由过来的,驱动不用操心。
2.2 核心数据结构:irq_desc
说到分层设计,就绕不开一个核心结构体——irq_desc。它是整个中断子系统的“户口本”。每个中断号都对应一个 irq_desc 实例。
我在项目中遇到过一个问题:系统里注册了上百个中断,突然有一个外设不响应了。我第一反应就是去查这个中断号对应的 irq_desc,看看它的状态是不是被屏蔽了,或者处理函数是不是被覆盖了。
来看一下这个结构体的核心成员(我挑几个重点讲):
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq; // 中断流控处理函数
struct irqaction *action; // 中断处理函数链表
unsigned int status_use_accessors;
struct list_head pending; // 待处理中断链表
struct irq_chip *irq_chip; // 硬件操作函数集
int parent_irq;
struct module *owner;
const char *name;
wait_queue_head_t wait_for_threads;
struct proc_dir_entry *dir;
int nr_actions;
unsigned int threads_handled;
unsigned int threads_active;
struct cpumask *percpu_enabled;
struct cpumask *percpu_affinity;
unsigned long threads_oneshot;
atomic_t threads_active_cnt;
struct task_struct *threads_affinity;
struct irq_thread *threads;
unsigned long irq_count;
unsigned long last_unhandled;
unsigned int irqs_unhandled;
atomic_t threads_handled_last;
raw_spinlock_t lock;
struct cpumask *irq_2_pin;
struct irq_affinity_notify *affinity_notify;
int node;
unsigned int core_internal_state__do_not_mess_with_it;
};
我的经验:调试中断问题时,我最常看的是 action 和 handle_irq 这两个字段。action 是链表,因为多个设备可以共享同一个中断号。handle_irq 是流控函数,它决定了中断是边沿触发还是电平触发,以及怎么处理嵌套中断。
2.3 中断流控:handle_irq 的秘密
你可能会问:为什么需要 handle_irq?直接调用驱动的处理函数不就行了?
嗯,这里要注意。不同的中断控制器,行为不一样。有的支持电平触发,有的支持边沿触发。有的需要自动屏蔽,有的不需要。这些差异就靠 handle_irq 来抽象。
内核预定义了几种标准的流控函数:
| 流控函数 | 适用场景 | 特点 |
|---|---|---|
| handle_simple_irq | 简单中断控制器 | 不做任何硬件操作,直接调用处理函数 |
| handle_level_irq | 电平触发中断 | 进入处理函数前屏蔽中断,处理完再解除屏蔽 |
| handle_edge_irq | 边沿触发中断 | 记录挂起的中断,防止丢失 |
| handle_fasteoi_irq | 支持EOI的中断控制器 | 处理完中断后写EOI寄存器 |
我曾经踩过一个坑:一个网卡驱动用了电平触发,但流控函数选成了 handle_edge_irq。结果就是中断频繁丢失,网络时断时续。查了两天才发现是流控函数配错了。
避坑指南:流控函数的选择必须跟硬件的中断触发方式匹配。电平触发用 handle_level_irq,边沿触发用 handle_edge_irq。混用会导致中断丢失或重复触发。
2.4 从硬件到软件的完整路径
咱们来走一遍完整流程。假设一个GPIO按键按下:
- 硬件阶段:GPIO控制器检测到电平变化,向中断控制器(比如GIC)发送中断信号。
- 中断控制器阶段:GIC解析中断号,通过IRQ线通知CPU。
- CPU阶段:CPU保存现场,跳转到内核的异常向量表,进入
handle_arch_irq或do_IRQ。 - 通用中断层:根据中断号找到对应的
irq_desc,调用handle_irq流控函数。 - 驱动层:流控函数调用
action链表里的处理函数,执行具体的业务逻辑。
你看,从硬件到软件,每一层各司其职。这就是分层设计的魅力。
2.5 核心逻辑流程图
下面我用一张SVG图来展示这个流程,方便你理解:
2.6 irq_desc 的管理机制
内核用一个全局数组来管理所有的 irq_desc:
struct irq_desc *irq_to_desc(unsigned int irq)
{
return (irq < NR_IRQS) ? irq_desc + irq : NULL;
}
这个数组的大小 NR_IRQS 在编译时确定。但实际系统中,中断号可能远小于这个值。所以内核也支持稀疏分配(SPARSE_IRQ),用基数树来管理,节省内存。
我记得有一次在嵌入式设备上,NR_IRQS 配得太大,导致内核映像膨胀了不少。后来改成 SPARSE_IRQ,内存占用降下来了。嗯,这里要注意,如果你的设备中断号不连续,建议开启 SPARSE_IRQ。
2.7 小结
这一章咱们把中断子系统的骨架搭起来了。核心就两点:
- 分层设计:硬件层、通用层、驱动层,各司其职。
- irq_desc:每个中断号的“户口本”,记录了中断的所有信息。
下一章咱们会深入 request_irq 的实现,看看驱动是怎么注册中断处理函数的。到时候你会更清楚 irq_desc 里的 action 链表是怎么工作的。
一句话总结:中断子系统就是靠 irq_desc 这个核心数据结构,把硬件差异封装起来,让驱动开发者只需要关心业务逻辑。