中断及其处理
I 中断基本概念
1.中断的定义
- 中断是指CPU在执行正常程序时,被外部或内部事件触发而暂时停止当前程序,转去执行特定处理程序(中断服务程序),处理完成后再返回原程序继续执行的机制。
- 中断是操作系统与硬件交互的核心方式,用于快速响应硬件事件(如键盘输入、磁盘IO完成、定时器触发等)。
2.中断的作用
- 实时响应硬件事件:硬件设备(如网卡、鼠标)的事件具有随机性,中断机制确保CPU能立即处理,避免等待导致的效率低下。
- 提高CPU利用率:无需CPU轮询硬件状态,硬件就绪后主动通过中断通知CPU,减少无效消耗。
- 支持多设备并发:通过中断向量区分不同设备,实现多硬件设备的独立响应。
3.中断的分类
-
按中断来源分类
- 外部中断: 由CPU外部硬件设备触发,如键盘按键、网卡接收数据、定时器超时
- 内部中断: 由CPU内部事件触发,如除零错误、调试断点、系统调用(软中断)
-
按可屏蔽性分类
- 可屏蔽中断(Maskable):可通过CPU的中断屏蔽寄存器(如IF标志)屏蔽,如网卡中断、硬盘IO中断
- 不可屏蔽中断(NMI):无法屏蔽,必须立即响应,如内存错误、电源故障等致命事件 |
4.Linux中的中断类型
- 硬中断(Hardware Interrupt):由硬件设备直接触发的中断(如外部中断),需快速处理,每个硬件中断都有对应的处理函数。
- 软中断(Software Interrupt):由软件主动触发的中断(如系统调用
int 0x80
或syscall
,或硬件中断的下半部处理),通常用于延迟处理非紧急任务。 - 异常(Exception):CPU执行指令时产生的内部中断(如除零、页错误),属于特殊中断,通常对应错误处理。
5.相关术语
- 中断请求(IRQ):硬件设备向中断控制器发送的请求信号,ARM中由GIC(如GIC-400)统一管理。
- 中断向量(Interrupt Vector):用于唯一标识中断类型,ARM架构中称为IRQ/FIQ号(通常为0-1023),由GIC(Generic Interrupt Controller)管理。
- 中断描述符表(IDT):存储每个中断向量对应的处理程序入口地址、权限等信息;ARM使用异常向量表(Exception Vector Table)
- 中断上下文(Interrupt Context):CPU 执行中断服务程序时的上下文环境,Linux分上半部(
handle_arch_irq
)和下半部(如tasklet
或threaded_irq
)。
II Linux中断处理
1.上半部和下半部
Linux中断处理分为上半部(Top Half)和下半部(Bottom Half):
- 上半部在关中断下快速处理紧急任务,此时不会被其他中断打断
- 下半部通过软中断/任务队列等机制延迟处理非紧急任务,以平衡响应速度,此时可以被其他中断打断。
2.中断注册函数
- request_irq函数的主要作用是向内核注册中断服务程序,建立中断号与中断处理函数之间的映射关系
int request_irq(
unsigned int irq, // 中断号,用于标识特定的中断源
irq_handler_t handler, // 中断处理函数,即中断上半部的处理函数,当中断发生时被调用
unsigned long flags, // 中断标志,用于定义中断的属性和触发方式等特性
const char *name, // 设备名称,用于标识该中断对应的设备,可在/proc/interrupts中查看,方便调试和管理
void *dev_id // 设备标识,在共享中断时用于区分不同设备,非共享中断时可设为NULL
);
- 当中断发生时,会调用handler函数,该函数即为中断的上半部
- 中断下半部(软中断、工作队列、worklet实现的下半部)的处理通常在中断处理函数handler中触发
3.中断上半部的执行
中断上半部是中断服务的核心部分,特点是“快”(在极短时间内完成),主要流程如下:
- 屏蔽当前中断,防止同一中断嵌套触发。
- 确认中断源(如读取设备寄存器,确认是否为该设备触发)。
- 清除硬件中断标志(通知设备“已收到中断”,避免重复触发)。
- 触发下半部处理(如标记软中断、tasklet、工作队列等)。
- 恢复中断屏蔽状态,退出上半部。
例如在网卡中断中,上半部仅需读取“数据就绪”标志,启动DMA将数据从网卡缓冲区复制到内存,然后触发下半部,自身快速结束,具体的数据处理在下半部中完成。
4.中断下半部的执行
中断下半部主要用来处理一些上半部没有完成的耗时或者不太紧急的工作,下半部可以被其他中断打断,下半部可以通过软中断、tasklet、workqueue或者thread irq来实现。
(1)软中断
- 软中断在编译时静态定义,通过
enum
列出所有类型(如HI_SOFTIRQ
,TIMER_SOFTIRQ
,NET_TX_SOFTIRQ
等),最多支持32个。 - 在上半部中断处理函数中通过函数
raise_softirq(int nr)
触发下半部软件中断:void raise_softirq(unsigned int nr);
- 使用
open_softirq()
函数注册软中断处理程序:void open_softirq(int nr, void (*action)(struct softirq_action *));
(2)tasklet
- Tasklet基于软中断实现(
HI_SOFTIRQ
和TASKLET_SOFTIRQ
),同一CPU上串行执行,保证数据安全。 - 通过
DECLARE_TASKLET
宏定义tasklet:DECLARE_TASKLET(my_tasklet, func, data); // 默认启用状态 DECLARE_TASKLET_DISABLED(my_tasklet, func, data); // 初始禁用状态
- 参数my_tasklet:需要初始化的tasklet结构体变量。
- 参数func:指向中断处理函数的指针,函数原型为void (*func)(unsigned long data)(参数是DECLARE_TASKLET中传入的data)。
- 参数data:传递给中断处理函数的设备标识符(
dev_id
),通常为设备结构体指针。 - 在中断上半部处理函数中,当完成紧急且必要的操作后,调用tasklet_schedule()函数,并将通过宏定义得到的 tasklet 结构体指针作为参数传入,从而触发tasklet。
(3)工作队列(Workqueue)
-
工作队列将任务交给内核线程执行,允许睡眠和长时间阻塞,适用于复杂任务(如磁盘IO)。
-
结构体work_struct
每个需要延迟处理的任务被封装为一个work_struct结构体,定义在<linux/workqueue.h>中:struct work_struct { atomic_long_t data; // 传递给处理函数的参数 struct list_head entry; // 用于链接到工作队列的链表节点 work_func_t func; // 工作项的处理函数(核心) struct workqueue_struct *wq; // 所属的工作队列 struct list_head pending; // 用于挂起状态管理 };
func:指向实际处理任务的函数,原型为
typedef void (*work_func_t)(struct work_struct *work);
-
结构体workqueue_struct
管理一组内核线程(worker线程)和待处理工作项的容器,核心结构体:struct workqueue_struct { struct list_head pwqs; // 关联的per-CPU工作队列集合 struct list_head list; // 全局工作队列链表 const char *name; // 工作队列名称(如"events") struct worker *rescuer; // 用于救援卡住的工作项 int flags; // 工作队列属性(如WQ_UNBOUND表示不绑定CPU) };
-
工作线程(worker)
内核线程实体,负责从工作队列中取出工作项并执行,每个worker对应一个内核线程
-
定义并初始化工作项
// 定义工作项处理函数 static void my_work_handler(struct work_struct *work) { // 执行下半部任务(如读取设备数据、通知用户空间等) printk(KERN_INFO "Processing work item: %p\n", work); // 允许睡眠(如调用msleep()、kmalloc(GFP_KERNEL)等) } // 定义并初始化工作项(静态初始化) static DECLARE_WORK(my_work, my_work_handler); // 或动态初始化:INIT_WORK(&my_work, my_work_handler);
-
在上半部处理函数中调度工作项
调用schedule_work()或queue_work()将工作项加入待处理队列:// 上半部中断处理函数中 static irqreturn_t my_irq_handler(int irq, void *dev_id) { // 1. 处理紧急硬件操作(如清除中断标志、读取状态寄存器) // 2. 调度下半部工作项 schedule_work(&my_work); // 加入默认工作队列(events) // 或使用自定义队列:queue_work(my_wq, &my_work); return IRQ_HANDLED; }
5.线程化中断
-
线程化中断(Threaded IRQ)是 Linux 内核中一种将中断处理逻辑部分或全部迁移到内核线程的机制,既保留了上半部快速响应硬件的特性,又允许下半部执行耗时操作(可睡眠、可阻塞),是平衡实时性与功能性的折中方案。
-
结构体irqaction:
struct irqaction { irq_handler_t handler; // 上半部处理函数(快速响应) unsigned long flags; // 中断标志(含线程化相关标志) const char *name; // 中断名称(/proc/interrupts中显示) void *dev_id; // 设备标识符(用于共享中断) struct irqaction *next; // 共享中断链表 int irq; // 中断号 irq_handler_t thread_fn; // 线程化处理函数(下半部核心) struct task_struct *thread; // 关联的内核线程 unsigned long thread_flags; // 线程行为标志 };
-
注册线程化中断
通过request_threaded_irq注册线程中断:int request_threaded_irq(unsigned int irq, irq_handler_t handler, // 上半部函数 irq_handler_t thread_fn, // 线程函数 unsigned long flags, // 含IRQF_ONESHOT等 const char *name, // 中断名称 void *dev_id); // 设备指针
-
中断处理
注册线程化中断之后,内核会为该中断创建一个专用的内核线程(kthread),当硬件中断触发时:- 先执行
handler
快速处理硬件相关操作(如清除中断标志) - 若
handler
返回IRQ_WAKE_THREAD
,则唤醒关联的内核线程执行thread_fn
- 线程化处理函数
thread_fn
可执行耗时操作(如数据处理、睡眠等待)
- 先执行
6.中断整体流程总结
- 硬件设备触发中断,通过中断引脚发送电信号至中断控制器
- 中断控制器对中断进行优先级仲裁,向CPU发送中断信号
- CPU检测到中断信号后,保存当前程序上下文(寄存器、程序计数器),从中断控制器获取中断向量。
- 根据中断向量查询IDT,执行中断上半部处理程序(快速完成设备确认、状态清除等关键操作)。
- 上半部通过标记软中断/调度tasklet/提交工作队列等方式触发下半部处理。
- CPU恢复保存的上下文,继续执行被中断的原程序指令。
III Linux中断相关数据结构
1.中断描述符irq_desc
(1)原型:
struct irq_desc {
struct irq_common_data irq_common_data; // 跨架构通用数据(含中断号、状态)
struct irq_data irq_data; // 中断控制器接口数据
unsigned int __percpu *kstat_irqs; // 每CPU中断计数器
irq_flow_handler_t handle_irq; // 中断流处理函数(硬件响应逻辑)
struct irqaction *action; // 中断处理函数链表
unsigned int status_use_accessors; // 中断状态(如IRQ_ACTIVE)
unsigned int depth; // 中断禁用嵌套深度
const char *name; // 中断名称(如"PCIe MSI")
};
- irq_common_data:存储中断号、触发方式等通用信息,为跨架构代码提供统一接口。
- handle_irq:指向中断控制器特定的处理函数(如电平触发用handle_level_irq),负责硬件级响应(如清除中断标志、屏蔽中断)。
- action:链接所有注册到该中断号的irqaction结构体,支持多设备共享中断。
(2)关键作用
- 作为中断号的唯一标识,关联中断的硬件属性(通过irq_data)和软件处理逻辑(通过action)。
- 维护中断的生命周期状态(启用 / 禁用、活跃 / 空闲),协调中断处理的并发与同步。
2.中断处理函数的载体irqaction
(1)原型
struct irqaction {
irq_handler_t handler; // 上半部处理函数(中断上下文)
unsigned long flags; // 中断属性(如IRQF_SHARED、IRQF_TRIGGER_FALLING)
const char *name; // 处理函数名称(/proc/interrupts可见)
void *dev_id; // 设备唯一标识(共享中断时用于区分)
struct irqaction *next; // 共享中断链表节点
int irq; // 关联的中断号
irq_handler_t thread_fn; // 线程化处理函数(进程上下文)
struct task_struct *thread; // 对应的内核线程
};
- handler:上半部函数,必须快速执行,负责紧急硬件操作(如读取状态寄存器)。
- flags:定义中断行为,例如IRQF_SHARED允许多个irqaction共享同一中断号。
- thread_fn:线程化下半部函数,运行在进程上下文,可执行耗时操作(如数据解析、IO 交互)。
(2)关键作用
- 连接中断号(通过irq关联irq_desc)与具体处理逻辑(handler和thread_fn)。
- 实现中断的共享机制,通过next链表将多个设备的处理函数绑定到同一中断号。
3.内核与硬件的中间层irq_data
(1)原型
struct irq_data {
unsigned int irq; // 内核中断号(逻辑编号)
unsigned long hwirq; // 硬件中断号(物理编号,如GIC的SPI号)
const struct irq_chip *chip; // 中断控制器操作集
void *chip_data; // 中断控制器私有数据(如寄存器地址)
unsigned int node; // NUMA节点ID(用于亲和性调度)
struct irq_domain *domain; // 所属 irq_domain,管理中断号映射规则
};
- irq vs hwirq:irq是内核分配的逻辑编号,hwirq是硬件控制器使用的物理编号,两者通过irq_domain映射。
- chip:指向irq_chip结构体,为内核提供操作硬件的标准化接口。
- domain:指向所属的irq_domain结构体,管理该中断号的映射规则(如硬件中断号转换、中断控制器私有数据访问)。
(2)关键作用
- 隔离硬件差异,使内核中断核心代码无需关心具体中断控制器的实现细节。
- 传递硬件操作所需的私有数据(如寄存器地址),通过chip提供的函数操作硬件。
4.中断控制器的操作集irq_chip
(1)原型
struct irq_chip {
const char *name; // 控制器名称(如"GICv3")
void (*irq_enable)(struct irq_data *data); // 使能中断
void (*irq_disable)(struct irq_data *data); // 禁用中断
void (*irq_ack)(struct irq_data *data); // 确认中断(清除硬件标志)
int (*irq_set_type)(struct irq_data *data, unsigned int type); // 设置触发方式
};
-
irq_enable/irq_disable:操作硬件寄存器使能 / 禁用指定中断。
-
irq_ack:中断响应时清除硬件的中断挂起标志,避免重复触发。
-
irq_set_type:配置中断触发方式(边沿 / 电平、上升沿 / 下降沿)。
(2)关键作用
- 抽象中断控制器的硬件操作,为内核提供统一的编程接口。
- 隐藏硬件细节,使中断核心代码具有跨架构兼容性。
5.中断号映射的桥梁irq_domain
(1)原型
struct irq_domain {
struct list_head link; // 全局irq_domain链表
const char *name; // 域名称(如"PCI-MSI")
const struct irq_domain_ops *ops; // 映射操作集
void *host_data; // 中断控制器私有数据(如MSI控制器地址)
unsigned int flags; // 域属性(如IRQ_DOMAIN_FLAG_MSI)
struct fwnode_handle *fwnode; // 设备树节点(用于设备树解析)
};
// 映射操作集
struct irq_domain_ops {
int (*xlate)(struct irq_domain *d, struct fwnode_handle *fwnode,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type);
struct irq_data *(*alloc)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs, void *arg);
void (*free)(struct irq_domain *d, unsigned int virq, unsigned int nr_irqs);
};
- xlate:将设备树中的中断信息转换为hwirq和触发方式。
- alloc/free:分配 / 释放内核中断号(virq),建立virq与hwirq的映射关系。
(2)关键作用
- 解决硬件中断号(hwirq)与内核逻辑中断号(virq)的映射问题,支持动态分配中断号。
- 适配不同中断源(如传统 IRQ、MSI、GPIO 中断),通过设备树实现中断信息的自动解析。
6.关键数据结构的映射关系
(1)irq_domain → irq_data
- irq_domain通过alloc操作分配virq,并为每个virq创建irq_data结构体,记录virq与hwirq的映射(irq_data.irq = virq,irq_data.hwirq = hwirq)。
(2)irq_data → irq_chip
- irq_data.chip指向对应的irq_chip,使内核可通过irq_data调用硬件操作函数(如irq_data->chip->irq_enable(irq_data))。
(3)virq → irq_desc
- 内核通过irq_to_desc(virq)获取irq_desc结构体,irq_desc.irq_data直接关联上述irq_data,形成virq → irq_desc → irq_data → irq_chip的硬件控制链。
(4)irq_desc → irqaction
- irq_desc.action链表链接所有注册到该virq的irqaction,形成virq → irq_desc → irqaction的软件处理链。
IV 设备树定义的中断
1.在设备树中定义中断
设备树中中断的定义需包含两部分:中断控制器节点(描述中断管理硬件)和外设节点的中断属性(描述外设与中断控制器的连接关系)。
(1)中断控制器的定义
-
interrupt-controller:空属性,标识当前节点是中断控制器。
-
#interrupt-cells:整数,指定描述一个中断所需的 32 位cell数量(如中断号、触发方式等)
-
compatible:用于匹配中断控制器驱动。
-
reg:寄存器地址和大小,描述中断控制器的硬件地址空间。
-
示例:
intc: interrupt-controller@10140000 {
compatible = "vendor,platform-intc"; // 匹配中断控制器驱动
reg = <0x10140000 0x1000>; // 寄存器基地址0x10140000,大小0x1000
interrupt-controller; // 声明为中断控制器
#interrupt-cells = <2>; // 每个中断用2个cell描述(如<中断号 触发方式>)
};
(2)外设中断节点定义
- interrupt-parent:指向当前设备依赖的中断控制器(可继承父节点的设置)
- interrupts:数组,每个元素由#interrupt-cells指定数量的 cell 组成,描述中断的具体信息(如中断号、触发方式等)
- 示例:
key: key@12340000 {
compatible = "vendor,key"; // 匹配按键驱动
reg = <0x12340000 0x100>; // 外设寄存器地址
interrupt-parent = <&intc>; // 依赖中断控制器intc
interrupts = <5 0x1>, <6 0x2>; // 2个中断:
// 第一个:中断号5,触发方式0x1(上升沿)
// 第二个:中断号6,触发方式0x2(下降沿)
};
2.在驱动程序中获取中断
(1)platform中断节点
-
platform 设备通常直接与中断控制器相连,其中断信息通过设备树节点的interrupts属性定义,内核会自动将该属性转换为IORESOURCE_IRQ类型的资源,可以通过函数platform_get_resource获取中断资源
-
platform_get_resource:
struct resource *platform_get_resource(struct platform_device *pdev, unsigned int type, unsigned int num);
- pdev:platform 设备结构体(struct platform_device *)。
- type:资源类型,中断资源固定为IORESOURCE_IRQ。
- num:资源索引(多个中断时,0 表示第一个)。
-
示例:
static int key_probe(struct platform_device *pdev) { struct resource *irq_res; int irq, ret; // 1. 通过资源获取中断号 irq_res = platform_get_resource(pdev, IORESOURCE_IRQ, 0); if (!irq_res) { dev_err(&pdev->dev, "Failed to get irq resource\n"); return -ENODEV; } irq = irq_res->start; // 从资源中提取中断号 // 2. 申请中断(使用设备管理型接口,自动释放) ret = devm_request_irq(&pdev->dev, irq, key_irq_handler, IRQF_TRIGGER_RISING, "key-irq", NULL); if (ret) { dev_err(&pdev->dev, "Failed to request irq %d\n", irq); return ret; } dev_info(&pdev->dev, "Platform device irq %d registered\n", irq); return 0; }
(2)of_irq_get
-
对于其他总线上的设备,或者非总线设备可以通过of_irq_get函数获取中断号
-
of_irq_get:
int of_irq_get(struct device_node *dev, int index);
- dev:设备树节点指针。
- index:中断索引。
-
示例:
//I2C子设备中断获取 static int light_sensor_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct device *dev = &client->dev; int irq, ret; // 1. 用of_irq_get从设备树节点解析中断号 irq = of_irq_get(dev->of_node, 0); if (irq < 0) { dev_err(dev, "of_irq_get failed: %d\n", irq); return irq; } // 2. 申请中断 ret = devm_request_irq(dev, irq, sensor_irq_handler, IRQF_TRIGGER_FALLING, "light-sensor-irq", client); if (ret) { dev_err(dev, "Failed to request irq %d\n", irq); return ret; } return 0; }
(3)gpio获取中断
-
GPIO 引脚常被用作外设的中断源,此时中断信息通过 GPIO 控制器节点间接定义
-
gpio_to_irq:将 GPIO 编号转换为对应的中断号
int gpio_to_irq(unsigned int gpio); /*参数gpio为gpio编号*/
-
示例:
static int gpio_key_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; int gpio, irq, ret; // 1. 从设备树获取GPIO编号(假设设备树中定义了gpio属性) gpio = of_get_named_gpio(dev->of_node, "interrupt-gpios", 0); if (gpio < 0) { dev_err(dev, "Failed to get gpio from device tree\n"); return gpio; } // 2. 申请GPIO引脚 ret = devm_gpio_request(dev, gpio, "key-gpio"); if (ret) { dev_err(dev, "Failed to request gpio %d\n", gpio); return ret; }