1. 中断和异常
在Linux驱动开发中,中断和异常是处理器响应异步事件和同步事件的两种核心机制。理解两者的区别,是掌握中断管理的基础。
1.1 中断
中断是由处理器外部设备(如网卡、键盘、定时器)产生的异步信号,用于通知处理器有事件需要处理。这样的事件是CPU芯片内部或外部的硬件电路产生的电信号,中断的发生与当前执行的指令无关,处理器在任意时刻都可能收到中断请求。
中断的主要特点包括:
- 异步性:中断与当前指令流无关,随时可能发生。
- 外部来源:中断通常由硬件设备触发,如I/O设备、时钟等。
- 可屏蔽:处理器可以通过中断屏蔽寄存器暂时关闭某些中断。
中断按照触发方式的不同,可以分为同步中断和异步中断两类:
- 同步中断(Synchronous Interrupt):由处理器内部在执行指令过程中产生,与当前正在执行的指令严格对应。例如除零、缺页、非法指令等异常都属于同步中断。同步中断发生时,处理器会暂停当前指令的执行,转而处理该事件。
- 异步中断(Asynchronous Interrupt):由处理器外部设备产生,与当前执行的指令流无关,随时可能发生。例如网卡收到数据、定时器到期、键盘输入等都属于异步中断。异步中断通常可以通过中断屏蔽寄存器暂时关闭。
在Linux内核中,同步中断通常对应异常处理路径,而异步中断则对应传统意义上的中断处理路径,两者在上下文和调度方式上有所区别。
1.2 异常
异常是由处理器内部在执行指令过程中检测到的同步事件,例如除零、缺页、非法指令等。异常与当前正在执行的指令直接相关,处理器在检测到异常时会暂停当前指令的执行,转而处理异常。另一方面,异常通常是由编程错误或那些必须要内核来处理的异常条件产生的,例如访问非法内存地址、执行特权指令等,这些情况需要内核介入进行错误处理或资源回收。
编程错误:内核是通过把它以每个 Unix 程序员都熟知的某个信号发送给当前进程来处理异常的。
内核处理异常:内核执行需要从异常条件恢复的所有步骤,比如缺页异常,或汇编指令如int或sysenter要求一个内核服务。
异常的主要特点包括:
- 同步性:异常与当前指令的执行严格对应,发生在特定指令的执行过程中。
- 内部来源:异常由处理器内部机制触发,而非外部设备。
- 不可屏蔽:异常通常无法通过屏蔽寄存器关闭,处理器必须响应。
中断与异常的区别
| 对比项 | 中断(Interrupt) | 异常(Exception) |
|---|---|---|
| 触发来源 | 外部硬件设备 | 处理器内部指令执行 |
| 同步性 | 异步,与指令流无关 | 同步,与当前指令相关 |
| 是否可屏蔽 | 可屏蔽 | 不可屏蔽 |
| 典型示例 | 网卡收到数据、定时器到期 | 除零、缺页、非法指令 |
在Linux内核中,中断和异常统一通过中断描述符表(IDT)进行管理,但两者的处理路径和上下文有所不同。中断处理通常运行在中断上下文,而异常处理则可能触发进程调度或内核错误处理流程。
2. 中断请求线
中断请求线(Interrupt Request Line,IRQ)是硬件设备与处理器之间用于传递中断信号的物理连接线。每个需要中断服务的外部设备都会通过一条独立的中断请求线向处理器发出中断请求信号,处理器检测到该信号后,会根据中断请求线的编号查找到对应的中断处理程序并执行。
处理器完成的步骤:
- 监听中断请求线,检测信号,如果检测到两个或以上的中断信号,选择编号低的那个信号线
- 如果一条中断线上产生了信号
a)把收到的信号转变为一个相应的向量。
b)把这个向量存到中断控制器的一个 I/O 端口寄存器,以便 CPU 通过数据线可以读取那个端口里的内容。
c)往处理器的INTR引脚输出一个电平,产生中断。
d)一直等到CPU通过往可编程中断控制器的一个端口写ACK, 当这种情况发生时,清掉INTR线。
3. 内核中断处理模型
这是 Linux 内核中中断请求(IRQ)描述符的数据结构关系图。
- irq_desc 数组:系统用一个数组管理所有中断,每个元素对应一个中断号。
- irq_desc_t 结构体:数组元素的具体结构,主要包含两个核心成员:
- handler:指向底层硬件中断控制器的操作函数,用于响应或屏蔽该中断。
- action:是一个链表头,指向注册到该中断上的所有设备驱动处理程序。
- irqaction 结构体:代表某个具体设备驱动注册的中断处理动作,通过next指针形成链表。这意味着多个设备可以共享同一个硬件中断线。
3.1irq_desc_t结构体
在Linux内核中,irq_desc_t(即struct irq_desc)是描述一条中断请求线(IRQ)全部状态和关联信息的数据结构。内核为每一条中断请求线都维护一个对应的irq_desc实例,它集中记录了该中断的触发方式、处理动作、状态标志以及统计信息,是中断子系统进行路由、分发和管理的核心载体。
struct irq_desc结构体定义如下:
// 简化后的核心定义,基于主流内核版本 (include/linux/irqdesc.h) struct irq_desc { struct irq_data irq_data; // 底层硬件相关数据:包含 irq_chip, irq_domain, hwirq 等 unsigned int __percpu *kstat_irqs; // 每CPU的中断统计计数 irq_flow_handler_t handle_irq; // 高层中断流处理函数入口 (如 handle_level_irq, handle_edge_irq) struct irqaction *action; // 中断处理函数链表 (由 request_irq 注册的具体驱动 handler) unsigned int status_use_accessors; // 中断状态标志 (使用 accessor 函数访问) unsigned int core_internal_state__do_not_mess_with_it; // 内部状态,禁止直接修改 unsigned int depth; // 中断禁用嵌套深度 (disable_irq 调用次数) unsigned int wake_depth; // 唤醒源启用嵌套深度 unsigned int irq_count; // 中断发生总次数 unsigned long last_unhandled; // 上一次未处理中断的时间戳 unsigned int irqs_unhandled; // 未处理中断计数 (用于检测坏中断) raw_spinlock_t lock; // 保护此结构体的自旋锁 struct cpumask *percpu_enabled; // 每CPU启用掩码 #ifdef CONFIG_SMP const struct cpumask *affinity_hint; // CPU 亲和性提示 struct irq_affinity_notify *affinity_notify; #ifdef CONFIG_GENERIC_PENDING_IRQ cpumask_var_t pending_mask; #endif #endif unsigned long threads_oneshot; atomic_t threads_active; wait_queue_head_t wait_for_threads; #ifdef CONFIG_PROC_FS struct proc_dir_entry *dir; // /proc/irq/NNN 目录项 #endif int parent_irq; // 父中断号 (用于级联中断控制器) struct module *owner; // 拥有此中断的模块 const char *name; // 中断名称 (显示在 /proc/interrupts) } ____cacheline_internodealigned_in_smp;该结构体的主要成员及参数说明如下:
- irq_data:保存与硬件相关的中断信息,包括中断号(irq)、中断控制器(irq_chip)以及硬件相关的私有数据(hwirq)等,用于驱动层与中断控制器之间的交互。
- handle_irq:中断处理的高层入口函数指针,内核根据中断的触发类型(如电平触发、边沿触发)选择对应的处理函数,例如
handle_level_irq、handle_edge_irq等。 - action:指向
irqaction结构体的链表头,每个注册到该中断号上的中断处理程序(通过request_irq注册)都会以irqaction节点的形式挂接在该链表上。 - status:记录中断当前的状态标志,例如是否被禁用(IRQ_DISABLED)、是否正在处理(IRQ_INPROGRESS)、是否被屏蔽(IRQ_MASKED)等,用于内核判断中断的可用性和处理进度。
- depth:用于管理中断禁用与启用的嵌套计数。每次调用
disable_irq时 depth 增加,调用enable_irq时 depth 减少,只有 depth 为 0 时中断才会真正被启用。 - irq_count:记录该中断被触发的累计次数,用于统计和调试,帮助开发者判断中断是否发生异常频繁的触发。
- last_unhandled:记录上一次未处理中断的时间戳,配合
irq_count可用于检测中断风暴(interrupt storm)等异常情况。 - lock:自旋锁,用于保护该
irq_desc结构体内部的并发访问,确保多个 CPU 同时操作同一中断描述符时的数据一致性。 - kstat_irqs:按 CPU 统计的中断次数数组,记录每个 CPU 上该中断被处理的次数,用于多核环境下的负载分析和性能监控。
通过irq_desc结构体,Linux 内核将硬件中断信号与软件处理逻辑解耦:底层中断控制器负责信号的采集与转发,而上层则通过irq_desc统一管理中断的注册、分发、屏蔽和统计,从而为驱动开发者提供了一套清晰、可扩展的中断管理接口。
中断请求线的状态描述:
在Linux内核中,每条中断请求线(IRQ)都对应一个irq_desc结构体,该结构体通过一系列状态标志和计数器来描述中断请求线的当前状态。这些状态信息用于内核判断中断的可用性、处理进度以及是否发生异常,是中断子系统进行调度和管理的重要依据。
中断请求线的主要状态描述如下:
- IRQ_DISABLED:表示该中断请求线当前已被禁用。内核通过
disable_irq或disable_irq_nosync接口关闭中断时,会设置该标志,此时即使硬件产生中断信号,内核也不会调度对应的处理程序。 - IRQ_INPROGRESS:表示该中断请求线上有一个中断正在被处理。该标志用于防止同一中断在处理完成之前被重复触发,避免嵌套重入导致的数据竞争。
- IRQ_MASKED:表示该中断请求线已被屏蔽。屏蔽操作通常由中断控制器完成,屏蔽后硬件中断信号无法送达处理器,常用于临界区保护或中断嵌套控制。
- IRQ_WAITING:表示内核正在等待该中断请求线上产生一次有效的中断信号。该状态常用于中断探测(probe)阶段,用于确认硬件设备是否能够正确产生中断。
- IRQ_PENDING:表示该中断请求线上有尚未处理的中断信号等待分发。当多个中断同时到达或中断被暂时屏蔽时,未处理的中断会被标记为挂起状态,待条件允许时再行处理。
- IRQ_ACTIVE:表示该中断请求线当前处于活动状态,即中断处理程序正在执行或即将执行。该标志与
IRQ_INPROGRESS配合使用,用于跟踪中断处理的活跃程度。 - IRQ_REPLAY:表示该中断请求线上有一个中断信号需要重新发送。该标志通常用于中断控制器在检测到中断信号丢失或处理失败时,要求重新触发一次中断,以确保中断事件不会因信号丢失而被遗漏。
- IRQ_AUTODETECT:表示该中断请求线正处于自动探测(autodetect)状态。该标志用于内核在启动阶段自动检测设备所使用的中断号,通过试探性地触发中断来确认设备实际占用的中断请求线,常用于设备驱动初始化时的中断资源探测。
- IRQ_NOAUTOEN:表示该中断请求线不会在
request_irq时被自动启用。需要驱动开发者显式调用enable_irq才能开启该中断,常用于需要手动控制中断使能时机的场景。 - IRQ_NOREQUEST:表示该中断请求线不允许被
request_irq请求注册。该标志通常用于系统保留的中断号或已被特殊用途占用的中断线,防止驱动误注册导致冲突。 - IRQ_NOPROBE:表示该中断请求线不允许被中断探测(probe)机制自动检测。该标志用于保护某些特殊的中断线,避免探测过程干扰其正常工作。
- IRQ_NOTHREAD:表示该中断请求线不支持线程化中断处理。对于该中断,内核不会为其创建中断线程,处理程序始终运行在中断上下文而非内核线程中。
- IRQ_PER_CPU:表示该中断请求线是每 CPU 类型的中断,即每个 CPU 都有独立的中断实例。该标志常见于本地定时器中断、IPI(处理器间中断)等场景,中断处理不会跨 CPU 迁移。
- IRQ_LEVEL:表示该中断请求线采用电平触发方式。只要设备保持中断信号为有效电平,中断就会持续触发,直到设备清除该电平信号。
- IRQ_EDGE:表示该中断请求线采用边沿触发方式。中断在信号电平发生跳变(上升沿或下降沿)时触发一次,适合脉冲型中断信号的设备。
- IRQ_NESTED_THREAD:表示该中断请求线支持嵌套线程化处理。该标志用于需要嵌套调用其他中断线程的场景,常见于某些 GPIO 或总线级联的中断控制器。
- IRQ_POLL_INPROGRESS:表示该中断请求线当前正在被轮询处理。该标志用于轮询模式下标记中断正在被处理,防止轮询与硬件中断处理之间产生冲突。
- IRQ_NO_BALANCING:表示该中断请求线不允许进行 CPU 负载均衡。内核不会将该中断在多个 CPU 之间动态迁移,常用于对延迟敏感或绑定特定 CPU 的中断。
- IRQ_IS_POLLED:表示该中断请求线已被配置为轮询模式。该标志表明该中断不依赖硬件信号触发,而是由内核定时轮询检测设备状态,常见于某些无法产生硬件中断的设备。
- IRQ_MSI:表示该中断请求线基于消息信号中断(MSI)机制。该标志表明中断通过写入特定内存地址的消息来触发,而非传统的物理中断引脚,常见于 PCIe 设备。
- IRQ_MSIX:表示该中断请求线基于 MSI-X 机制。MSI-X 是 MSI 的扩展版本,支持更多数量的中断向量,每个向量可独立配置,适用于需要大量中断通道的高性能设备。
此外,irq_desc结构体中的depth字段用于管理中断禁用与启用的嵌套计数,irq_count记录中断被触发的累计次数,last_unhandled记录上一次未处理中断的时间戳。这些状态信息共同构成了中断请求线的完整状态描述,帮助内核在复杂的多中断环境中做出正确的调度决策。
3.2 irqaction结构体
在Linux内核中,irqaction(即struct irqaction)是描述一条中断请求线上具体中断处理动作的数据结构。与irq_desc描述整条中断请求线的全局状态不同,irqaction更侧重于记录某个设备驱动通过request_irq注册到该中断号上的具体处理函数及其相关信息。每个中断号上可以挂接多个irqaction节点,它们以链表形式组织在irq_desc.action上,从而实现多个设备共享同一条中断请求线的能力。
struct irqaction结构体定义如下:
// 简化后的核心定义,基于主流内核版本 (include/linux/interrupt.h) struct irqaction { irq_handler_t handler; // 中断处理函数指针,由驱动通过 request_irq 注册 void *dev_id; // 设备标识,用于区分共享同一中断号的多个设备 void *percpu_dev_id; // 每CPU的设备标识,用于每CPU类型的中断 struct irqaction *next; // 指向下一个 irqaction 节点,构成共享中断链表 irq_handler_t thread_fn; // 线程化中断处理函数,运行在内核线程中 struct task_struct *thread; // 中断线程的 task_struct 指针 unsigned int irq; // 该 irqaction 对应的中断号 unsigned int flags; // 中断标志位,如 IRQF_SHARED、IRQF_TRIGGER_* 等 unsigned long thread_flags; // 中断线程的标志位 unsigned long thread_mask; // 中断线程的屏蔽掩码 const char *name; // 中断名称,显示在 /proc/interrupts 中 struct proc_dir_entry *dir; // /proc/irq/NNN/action_name 目录项 } ____cacheline_internodealigned_in_smp;该结构体的主要成员及参数说明如下:
- handler:中断处理函数指针,是驱动通过
request_irq注册的核心回调函数。当该中断号上产生中断信号时,内核会调用该函数进行实际的中断处理。 - dev_id:设备标识指针,用于在共享中断场景下区分不同的设备。当多个设备共享同一条中断请求线时,内核通过比较
dev_id来确定中断信号具体属于哪个设备。 - next:指向下一个
irqaction节点的指针,多个irqaction通过该指针构成链表,挂接在irq_desc.action上,实现中断共享。 - thread_fn:线程化中断处理函数指针。当使用线程化中断(threaded IRQ)时,该函数运行在内核线程上下文中,用于执行耗时较长的处理逻辑,避免阻塞中断上下文。
- thread:指向中断线程的
task_struct指针,用于管理线程化中断对应的内核线程。 - irq:该
irqaction所绑定的中断号,用于标识该处理动作属于哪条中断请求线。 - flags:中断标志位,记录该中断的触发方式(电平触发、边沿触发)以及是否允许共享(
IRQF_SHARED)等属性,由request_irq - name:中断名称字符串,通常为设备驱动名称,显示在
/proc/interrupts中,便于开发者查看中断的注册情况。
通过irqaction结构体,Linux 内核将设备驱动的中断处理逻辑与中断请求线解耦:驱动只需通过request_irq注册一个irqaction节点,内核便会在中断发生时自动调用对应的handler函数,从而为驱动开发者提供了一套简洁、可扩展的中断注册与分发接口。
在Linux内核中,irqaction结构体通过flags字段记录中断处理动作的注册属性与运行状态。这些标志位由驱动在调用request_irq时通过 flags 参数传入,内核根据这些标志决定中断的触发方式、共享策略以及线程化处理方式,是中断注册与分发的重要依据。
irqaction 结构体的flags字段描述如下:
- SA_INTERRUPT:表示该中断处理程序是一个快速中断处理程序(fast interrupt handler)。在旧版Linux内核中,该标志用于标记中断处理程序在中断上下文执行时是否关闭本地中断。设置该标志后,内核在处理该中断时会屏蔽本地CPU上的其他中断,以确保快速处理路径不被干扰;未设置该标志时,则允许中断嵌套。在现代内核中,该标志已被
IRQF_DISABLED取代,但在部分历史代码和驱动中仍可见到。 - SA_SHIRQ:表示该中断允许被多个设备共享。当多个设备共用同一条中断请求线时,必须设置该标志,内核会将多个
irqaction节点挂接在同一链表上,并在中断发生时依次调用各节点的handler进行匹配。该标志与IRQF_SHARED含义一致,是旧版内核中用于声明共享中断的标志。 - SA_SAMPLE_RANDOM:表示该中断可以作为内核随机数生成器的熵源。设置该标志后,内核会将中断发生的时间间隔等信息收集起来,用于增强系统随机数池的熵,从而提高随机数的质量。该标志通常用于定时器中断、键盘中断等具有较强随机性的中断源,与
IRQF_SAMPLE_RANDOM含义一致。
4. 中断相关函数
4.1 注册中断
在Linux驱动开发中,注册中断处理函数是设备驱动与内核中断子系统建立联系的关键步骤。驱动通过request_irq函数将中断处理程序注册到指定的中断请求线上,内核随后会在该中断发生时自动调用对应的处理函数。理解request_irq的函数原型和参数含义,是编写可靠中断驱动的基础。
request_irq函数定义如下:
// 定义于 include/linux/interrupt.h int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);该函数的参数说明如下:
- irq:要注册的中断号,即中断请求线(IRQ)的编号。该编号通常由硬件平台决定,驱动可以通过
platform_get_irq、gpio_to_irq等接口获取,也可以直接使用设备树或板级描述中指定的中断号。 - handler:中断处理函数指针,类型为
irq_handler_t,即irqreturn_t (*handler)(int irq, void *dev_id)。当中断发生时,内核会调用该函数进行实际的中断处理。处理函数运行在中断上下文,应尽量保持简短,耗时操作应放到底半部(bottom half)或工作队列中执行。 - flags:中断标志位,用于指定中断的触发方式、共享策略等属性。常用标志包括
IRQF_TRIGGER_RISING(上升沿触发)、IRQF_TRIGGER_FALLING(下降沿触发)、IRQF_TRIGGER_HIGH(高电平触发)、IRQF_TRIGGER_LOW(低电平触发)以及IRQF_SHARED(允许共享中断)等。多个标志可以通过按位或(|)组合使用。 - name:中断名称字符串,通常为设备驱动名称。该名称会显示在
/proc/interrupts中,便于开发者查看系统中各中断的注册情况,也用于中断共享时的匹配和调试。 - dev:设备标识指针,用于在共享中断场景下区分不同的设备。当多个设备共享同一条中断请求线时,内核通过比较
dev参数来确定中断信号具体属于哪个设备。该参数通常传入设备结构体指针(如struct device *或自定义的设备私有数据指针)。
函数返回值说明:request_irq成功注册时返回 0;如果注册失败,则返回一个负的错误码,常见错误码包括-EBUSY(中断号已被占用且未设置IRQF_SHARED)、-EINVAL(参数无效,如中断号非法)等。驱动在调用request_irq后应检查返回值,并在失败时进行相应的错误处理。
中断处理函数类型:
在Linux内核中,中断处理函数通过irq_handler_t类型定义,其原型为irqreturn_t (*handler)(int irq, void *dev_id)。该函数是驱动通过request_irq注册到中断请求线上的核心回调,当中断发生时由内核自动调用。
irq_handler_t函数类型定义如下:
// 定义于 include/linux/interrupt.h typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);中断处理函数的参数说明如下:
- irq:触发该中断处理函数的中断号,即中断请求线(IRQ)的编号。该参数由内核在中断发生时自动传入,驱动可以在处理函数中通过该参数判断当前处理的是哪条中断请求线,尤其在多个中断号共用同一个处理函数时非常有用。
- dev_id:设备标识指针,即调用
request_irq时传入的dev参数。该指针用于在共享中断场景下区分不同的设备,内核在调用处理函数时会原样传回该指针,驱动可以将其转换为自定义的设备结构体指针,从而访问设备相关的私有数据。
中断处理函数的返回值类型为irqreturn_t,其取值说明如下:
- IRQ_NONE:表示该中断不是本设备产生的,即设备未识别该中断信号。在共享中断场景下,内核会继续调用链表上的下一个
irqaction节点进行匹配。 - IRQ_HANDLED:表示该中断已被本设备成功处理。内核收到该返回值后,会认为中断已被正确响应,不再需要进一步处理。
- IRQ_WAKE_THREAD:表示需要唤醒线程化中断处理函数(
thread_fn)来执行后续处理。该返回值用于线程化中断(threaded IRQ)场景,当中断处理函数检测到需要执行耗时操作时,返回该值以将工作交给内核线程处理。
在编写中断处理函数时,需要注意以下几点:
- 中断处理函数运行在中断上下文,不能调用可能睡眠的函数,如
kmalloc(GFP_KERNEL)、mutex_lock等。 - 处理函数应尽量简短,只完成必要的硬件操作和状态记录,耗时逻辑应放到底半部(bottom half)、工作队列或线程化中断中执行。
- 在共享中断场景下,处理函数应首先检查
dev_id对应的设备是否确实产生了中断,若未产生则返回IRQ_NONE,避免误处理其他设备的中断。
4.2 处理中断
在Linux驱动开发中,中断处理操作函数用于对已注册的中断进行动态管理,包括禁用、启用和释放中断等操作。这些函数是驱动在运行时控制中断行为的重要接口,理解其函数原型、作用和参数含义,是编写健壮中断驱动的基础。
disable_irq函数定义如下:
// 定义于 include/linux/interrupt.h void disable_irq(unsigned int irq);作用:禁用指定的中断请求线。调用该函数后,即使硬件产生中断信号,内核也不会调度对应的中断处理程序。该函数会等待当前正在执行的中断处理程序完成后再返回,因此可以安全地在进程上下文中调用。
参数说明:
- irq:要禁用的中断号,即中断请求线(IRQ)的编号。该编号必须与调用
request_irq时使用的中断号一致。
注意事项:每次调用disable_irq都会使irq_desc结构体中的depth字段加 1,只有depth减为 0 时中断才会真正被重新启用。因此,disable_irq与enable_irq必须成对调用,且调用次数要匹配,否则中断可能一直处于禁用状态。
disable_irq_nosync函数定义如下:
// 定义于 include/linux/interrupt.h void disable_irq_nosync(unsigned int irq);作用:禁用指定的中断请求线,但不会等待当前正在执行的中断处理程序完成。该函数立即返回,适用于中断处理程序内部或对延迟敏感的场景,可以避免因等待处理程序完成而引入的阻塞。
参数说明:
- irq:要禁用的中断号,即中断请求线(IRQ)的编号。
注意事项:由于disable_irq_nosync不等待中断处理程序完成,调用后可能仍有中断处理程序正在执行。因此,该函数不能用于需要确保中断处理完全停止的场景,否则可能引发数据竞争。在中断处理程序内部禁用自身所在的中断时,应优先使用该函数以避免死锁。
enable_irq函数定义如下:
// 定义于 include/linux/interrupt.h void enable_irq(unsigned int irq);作用:重新启用指定的中断请求线。调用该函数后,内核会恢复对该中断的调度,硬件产生的中断信号将再次触发对应的中断处理程序。该函数通常与disable_irq或disable_irq_nosync配对使用。
参数说明:
- irq:要启用的中断号,即中断请求线(IRQ)的编号。
注意事项:每次调用enable_irq都会使irq_desc结构体中的depth字段减 1。只有当depth减为 0 时,中断才会真正被启用。如果enable_irq的调用次数多于disable_irq,会导致depth变为负数,内核会打印警告信息,因此必须保证两者调用次数严格匹配。
free_irq函数定义如下:
// 定义于 include/linux/interrupt.h void free_irq(unsigned int irq, void *dev_id);作用:释放指定的中断请求线,注销之前通过request_irq注册的中断处理程序。调用该函数后,该中断号上的irqaction节点会从irq_desc.action链表中移除,内核不再调度对应的中断处理函数。该函数通常在驱动卸载或设备移除时调用,用于释放中断资源。
参数说明:
- irq:要释放的中断号,即中断请求线(IRQ)的编号,必须与
request_irq时使用的中断号一致。 - dev_id:设备标识指针,必须与
request_irq时传入的dev参数一致。在共享中断场景下,内核通过该参数精确匹配要释放的irqaction节点;如果传入 NULL,则释放该中断号上所有注册的处理程序。
注意事项:调用free_irq时,必须确保该中断号上对应的中断处理程序已经不再被调用。如果中断处理程序正在执行,free_irq会等待其完成后再释放资源。此外,free_irq不能从中断上下文调用,只能在进程上下文中使用。
5. 中断处理函数
在Linux驱动开发中,中断处理函数是设备驱动响应硬件事件的核心回调。与普通函数相比,中断处理函数运行在特殊的中断上下文,其编写规则和调用方式都有严格限制。理解中断处理函数与普通函数的区别、中断的上半部与下半部分,以及中断函数的编写格式,是编写可靠中断驱动的基础。
中断处理函数与普通函数的区别:
- 运行上下文不同:普通函数运行在进程上下文,可以访问用户空间数据、可以睡眠;中断处理函数运行在中断上下文,与任何进程无关,不能访问用户空间,也不能睡眠或调用可能睡眠的函数。
- 触发方式不同:普通函数由程序显式调用,执行时机由代码逻辑决定;中断处理函数由硬件中断信号触发,执行时机具有异步性,随时可能被打断。
- 执行环境不同:普通函数运行在完整的进程环境中,拥有独立的栈和调度信息;中断处理函数运行在中断栈上,不参与进程调度,执行期间会屏蔽本地CPU上的部分中断。
- 可调用接口不同:普通函数可以调用
kmalloc(GFP_KERNEL)、mutex_lock、copy_to_user等接口;中断处理函数只能调用kmalloc(GFP_ATOMIC)、spin_lock等不睡眠的接口,不能调用任何可能睡眠或访问用户空间的函数。 - 返回方式不同:普通函数通过
return返回计算结果;中断处理函数返回irqreturn_t类型,即IRQ_NONE、IRQ_HANDLED或IRQ_WAKE_THREAD,用于告知内核中断是否被正确处理。
中断的上半部(Top Half)与下半部(Bottom Half):
中断处理函数运行在中断上下文,要求执行时间尽可能短。为了在快速响应中断的同时完成耗时逻辑,Linux内核将中断处理划分为上半部和下半部两部分。
- 上半部(Top Half):即中断处理函数本身,由
request_irq注册的handler回调。上半部在中断上下文执行,要求快速完成,只处理时间敏感的操作,如读取硬件状态寄存器、清除中断标志、记录必要数据等。上半部执行期间,本地CPU上的中断通常被屏蔽,因此必须尽快返回。 - 下半部(Bottom Half):用于处理上半部中来不及完成的耗时逻辑,如数据处理、协议解析、唤醒等待进程等。下半部运行在相对宽松的上下文中,可以延迟执行,避免长时间占用中断上下文。Linux内核提供了多种下半部机制,包括软中断(softirq)、tasklet、工作队列(workqueue)以及线程化中断(threaded IRQ)。
上半部与下半部的分工原则是:上半部只做必要的紧急处理,其余工作全部交给下半部。这样既能保证中断的快速响应,又能避免因处理耗时逻辑而长时间屏蔽中断,从而提高系统的实时性和吞吐量。
中断处理函数的编写格式:
中断处理函数的原型由irq_handler_t类型定义,其标准格式如下:
// 中断处理函数标准原型 static irqreturn_t my_irq_handler(int irq, void *dev_id) { // 1. 检查设备是否确实产生了中断(共享中断场景下必须) struct my_device *dev = (struct my_device *)dev_id; if (!(readl(dev->regs + INT_STATUS) & MY_DEV_INT_PENDING)) return IRQ_NONE; // 2. 清除硬件中断标志,防止中断重复触发 writel(MY_DEV_INT_PENDING, dev->regs + INT_CLEAR); // 3. 记录必要状态,读取硬件数据(上半部只做紧急处理) dev->int_count++; // 4. 将耗时逻辑交给下半部处理 schedule_work(&dev->work); // 5. 返回处理结果 return IRQ_HANDLED; }中断处理函数的编写规则如下:
- 函数签名固定:中断处理函数必须符合
irqreturn_t (*handler)(int irq, void *dev_id)原型,参数irq为中断号,dev_id为注册时传入的设备标识指针。 - 不能睡眠:中断处理函数运行在中断上下文,不能调用
kmalloc(GFP_KERNEL)、mutex_lock、msleep、copy_to_user等可能睡眠或访问用户空间的函数。需要分配内存时应使用kmalloc(GFP_ATOMIC),需要加锁时应使用spin_lock而非mutex_lock。 - 执行要快:中断处理函数应尽量简短,只完成必要的硬件操作和状态记录。耗时逻辑应通过软中断、tasklet、工作队列或线程化中断放到下半部执行。
- 共享中断要检查设备状态:在共享中断场景下,多个设备共用同一条中断请求线,处理函数必须首先检查
dev_id对应的设备是否确实产生了中断。若未产生,应返回IRQ_NONE,让内核继续调用链表上的下一个处理函数。 - 正确返回处理结果:中断处理函数必须返回
irqreturn_t类型。设备识别并处理了中断返回IRQ_HANDLED;未识别该中断返回IRQ_NONE;需要唤醒线程化中断处理函数时返回IRQ_WAKE_THREAD。 - 使用合适的下半部机制:根据耗时逻辑的特点选择下半部机制。对时间要求较高且不允许睡眠的场景使用软中断或tasklet;允许睡眠、需要较长处理时间的场景使用工作队列;需要与进程上下文交互的场景使用线程化中断。
下面给出一个完整的中断处理函数注册与编写示例:
#include <linux/module.h> #include <linux/interrupt.h> #include <linux/workqueue.h> #include <linux/io.h> struct my_device { void __iomem *regs; // 设备寄存器基地址 unsigned long int_count; // 中断计数 struct work_struct work; // 下半部工作队列 }; // 下半部:工作队列处理函数 static void my_irq_work(struct work_struct *work) { struct my_device *dev = container_of(work, struct my_device, work); // 在这里执行耗时逻辑,如数据处理、协议解析等 pr_info("my_device: processed %lu interrupts\n", dev->int_count); } // 上半部:中断处理函数 static irqreturn_t my_irq_handler(int irq, void *dev_id) { struct my_device *dev = (struct my_device *)dev_id; // 检查设备是否确实产生了中断 if (!(readl(dev->regs + 0x00) & 0x1)) return IRQ_NONE; // 清除中断标志,防止重复触发 writel(0x1, dev->regs + 0x04); // 记录中断次数 dev->int_count++; // 将耗时逻辑交给下半部 schedule_work(&dev->work); return IRQ_HANDLED; } // 驱动初始化:注册中断处理函数 static int my_probe(struct platform_device *pdev) { struct my_device *dev; int irq, ret; dev = kzalloc(sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; irq = platform_get_irq(pdev, 0); if (irq < 0) { kfree(dev); return irq; } INIT_WORK(&dev->work, my_irq_work); // 注册中断处理函数 ret = request_irq(irq, my_irq_handler, IRQF_TRIGGER_RISING, "my_device", dev); if (ret) { kfree(dev); return ret; } return 0; } // 驱动卸载:释放中断 static int my_remove(struct platform_device *pdev) { int irq = platform_get_irq(pdev, 0); free_irq(irq, NULL); return 0; }通过以上示例可以看出,一个规范的中断处理函数应当遵循「快速响应、及时清除中断标志、耗时逻辑下沉到下半部、正确返回处理结果」的原则。只有严格遵循这些编写规则,才能保证中断驱动的稳定性和系统整体的实时性。
6. 总结
本文围绕 Linux 驱动开发中的中断管理,系统梳理了从基础概念到实际编码的完整知识链路。中断和异常是处理器响应异步事件与同步事件的两种核心机制,中断由外部硬件设备触发、具有异步性和可屏蔽性,异常则由处理器内部指令执行产生、具有同步性和不可屏蔽性,两者统一通过中断描述符表(IDT)进行管理。
中断请求线(IRQ)是硬件设备与处理器之间传递中断信号的物理连接,处理器通过监听、向量转换、端口读取和 ACK 确认等步骤完成一次中断信号的接收。在内核中断处理模型中,irq_desc结构体描述整条中断请求线的全局状态,irqaction结构体记录具体设备驱动注册的中断处理动作,两者通过链表关联,共同支撑起中断的注册、分发、屏蔽和统计。
在中断相关函数层面,request_irq负责注册中断处理函数,disable_irq、disable_irq_nosync、enable_irq和free_irq分别用于禁用、启用和释放中断资源,使用时必须注意depth嵌套计数与调用次数的严格匹配。
中断处理函数运行在中断上下文,与普通函数在运行环境、触发方式、可调用接口和返回方式上存在本质区别。为兼顾快速响应与耗时逻辑处理,Linux 内核将中断处理划分为上半部与下半部:上半部只做紧急处理,耗时逻辑下沉到软中断、tasklet、工作队列或线程化中断中执行。编写中断处理函数时,应遵循函数签名固定、不能睡眠、执行要快、共享中断检查设备状态、正确返回处理结果以及选用合适下半部机制等规则。
掌握这些中断管理知识,是编写稳定、高效 Linux 设备驱动的基础。建议读者在理解原理后,结合真实硬件平台动手实践,通过/proc/interrupts观察中断注册情况,逐步加深对中断子系统的理解。