11 Linux内核中断
Linux内核中断
总览
Linux驱动开发中,对于中断,同样遵循着“驱动分层”和“主机驱动和设备驱动分离”的思想,可以分为以下几层:
1.硬件/架构相关层(最底层)
这是与具体CPU架构相关的实现层,负责:
- 处理器特定的中断控制器操作(如x86的APIC、ARM的GIC)
- 中断向量表的设置
- 底层中断启用/禁用
- 中断上下文保存与恢复
文件位置:arch/xxx/kernel/irq.c
(如arch/arm/kernel/irq.c
)
2.中断控制器驱动层(irqchip driver)
这是针对具体SoC的中断控制器的驱动层,例如:
- ARM GIC驱动(
drivers/irqchip/irq-gic.c
) - x86 IOAPIC驱动
- 其他SoC专用中断控制器
这些驱动需要:
- 初始化硬件中断控制器
- 实现
irq_chip
操作集(如mask/unmask中断) - 处理硬件级中断路由
3.中断核心层(irq core)
这是Linux内核提供的通用中断子系统核心,负责:
- 中断描述符管理(
struct irq_desc
) - 中断流控处理(边沿/电平触发)
- 中断共享机制
- 中断统计信息(/proc/interrupts)
- 提供统一的API供驱动使用(如
request_irq()
)
文件位置:kernel/irq/
目录下
4.设备驱动层
这是具体设备驱动使用中断的层面:
- 通过
request_irq()
注册中断处理函数 - 在中断处理函数中响应设备事件
- 可能使用中断线程化(threaded IRQ)
- 处理中断共享和设备特定逻辑
普通驱动开发者一般只涉及设备驱动层,其开发包括设备树和驱动程序2部分
主机驱动层
中断控制器的驱动和设备树一般不需要用户自己写,厂家基本上都写好了。同一个SoC可能有多个中断控制器,且多为==嵌套==关系
对于I.MX6ULL SoC,其有2类中断控制器,定义在imx6ull.dsti
中,由interrupt-controller;
标识:
1.首先是Arm内核Soc中的GIC中断控制器(相当于STM32中的NVIC)
1 | intc: interrupt-controller@00a01000 { |
#interrupt-cells
表示此中断控制器下子节点的interrupt
属性长度,对于设备节点而言,会使用 interrupts
属性描述中断信息。
每个 cell 都是 32 位整形值,GIC的子节点,一共有3个cells,这三个 cells 的含义如下:
- 第一个 cells:中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断
- 第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0
987,对于 PPI 中断来说中断号的范围为 015 - 第三个 cells:标志, bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候表示下降沿触发,为 4 的时表示高电平触发,为 8 的时候表示低电平触发。 bit[15:8]为 PPI 中断的 CPU 掩码
例如:
1 | interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>; |
2.各个GPIO同样也可以作为中断控制器
1 | gpio5: gpio@020ac000 { |
设备驱动层
对于使用了中断的某个设备,需要在其设备树中描述和中断相关的信息,和中断相关的属性:
interrupt-parent
:建立设备与中断控制器之间的层级关系,指定中断的路由方式interrupts
:设置:中断号、触发方式等属性
==单级中断控制器==:
1 | // 中断控制器定义 |
==多级中断控制器==:
1 | // 主中断控制器(如ARM GIC) |
触发类型定义如下:
1 |
核心层
常用API
1.申请中断上半部:该函数会自动使能中断,不需要再手动使能
1 | int request_irq(unsigned int irq, |
- irq:要申请中断的中断号
- handler:中断处理函数,当中断发生以后就会执行此中断处理函数
- flags:中断标志,可以在文件
include/linux/interrupt.h
里面查看所有的中断标志,这里我们介绍几个常用的中断标志,这些标志可以用|
组合

- name:中断名字,设置以后可以在
/proc/interrupts
文件中看到对应的中断名字 - dev: 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分共享中断的不同设备,一般情况下将dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
- 返回值: 0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经被申请了。
2.释放中断上半部
1 | void free_irq(unsigned int irq, id *dev); |
- irq: 要释放的中断的中断号
- dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
3.中断处理服务函数的函数指针
1 | irqreturn_t (*irq_handler_t) (int, void *) |
第一个参数是中断号
第二个参数是一个指向 void 的指针,也就是个通用指针,需要与
request_irq
函数的 dev 参数保持一致。用于区分==共享中断的不同设备==返回值类型
irqreturn_t
的详细定义:
1 | enum irqreturn { |
在返回时一般需要用IRQ_RETVAL
包裹一下再返回,比如:
1 | return IRQ_RETVAL(IRQ_HANDLED); |
中断上半部与下半部
在操作系统内核中,处理中断的过程通常分为两部分:上半部和下半部。这种分层处理的方式有助于确保及时响应中断的同时,避免在中断处理程序中执行过多耗时操作,从而提高系统的响应性和可靠性。
上半部
上半部是中断处理程序的第一部分,通常是在中断上下文中执行的,负责紧急任务(如清除中断标志、读取关键数据)。它的主要特点包括:
- 中断处理:检测和识别中断的发生,确定中断的来源和类型
- 快速响应:执行必要的、紧急的中断处理任务,通常是一些简短的操作,例如更新状态、唤醒进程或者触发更复杂的处理流程
- 禁用中断:为了避免竞态条件和保护关键资源,上半部在处理过程中可能会临时禁用本地 CPU 的中断,确保当前中断处理不会被其他中断打断,并在执行完中断服务函数时恢复中断,相当于进入了==临界区==
用
irq_request()
注册的中断服务函数就属于上半部
下半部
下半部是中断处理程序的第二部分,在上半部稍后时间点延迟执行非紧急任务(如数据处理、唤醒进程)。它的主要特点包括:
- 延迟处理:处理那些不能立即在上半部完成的任务,例如复杂的数据结构操作、磁盘和网络 I/O 操作,或者需要与用户空间交互的任务。
- 解锁资源:在上半部临时禁用中断后,下半部可以==重新启用中断==,允许其他中断和进程继续执行,从而避免长时间阻塞整个系统。
- 异步执行:下半部通常以一种异步的方式执行,例如使用work queue、tasklet、定时器…以便在合适的时间点处理。
区别与应用
- 区别:上半部处理程序在中断上下文中执行,必须快速响应并保护关键资源;下半部则允许更多时间处理复杂任务,并且可以在稍后时间点执行。
- 应用:上半部处理程序通常直接与硬件交互、更新数据结构或触发通知;下半部处理程序则处理复杂的异步任务、与用户空间的交互或长时间操作。
如何一个函数到底放在上半部还是下半部?
- 上半部:
- 不希望被打断
- 对时间敏感
- 与硬件相关
- 下半部:
- 除了上述3种例子以外的情况
下半部机制
Linux内核提供了3种机制让我们实现中断下半部:
soft_irq
软中断是效率最高的一种方式,在中断上下文运行,不可睡眠,可并发执行,函数要求可重入,在do_softirq
中执行
tasklet
tasklet
在软中断上下文中==原子地==执行异步任务,而无需担心竞态条件或长时间占用 CPU
1 | struct tasklet_struct |
tasklet
有以下特点:
tasklet
是一种轻量级的结构,适合于执行简单但频繁的任务- 它们的开销比工作队列要小,因为它们在内核中直接以链表形式管理,而不涉及线程调度和内核线程的开销
tasklet
可以在软中断上下文中执行,且不会被其他线程抢占,是原子性的,但是会被其他中断抢占- 不允许在下半部中睡眠线程,因为它不在线程上下文中,看不到调度器
- 个人感觉用tasklet下半部和不用下半部差不多,都是在中断的上下文中,唯一的区别就是前者又开启了中断
使用tasklet
的步骤:
相关函数:
1.tasklet的初始化和禁用
1 | // 初始化一个tasklet实例 |
name
:tasklet
实例的名称。func
:tasklet
执行的函数。data
:传递给func
的参数
2.tasklet的调度
1 | // 启动一个tasklet实例的调度,操作系统会在合适时调用该下半部 |
- ==此函数需要在中断上半部中被调用==
workqueue
工作队列是在进程上下文中进行的,工作队列将要延迟执行的一个工作(下半部)交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度
在使用工作队列定义下半部时,只需要初始化一个
work_struct
就行了,不需要管工作队列,它有操作系统本身维护
Linux内核使用以下的结构体表示一个工作:
1 | struct work_struct { |
使用工作队列的步骤:
相关函数:
1 | // 初始化一个已经创建的工作(work_struct) |
调试
1.通过cat /proc/interrupts
查看当前中断信息,举个例子:
1 | CPU0 |
各字段含义:
- 第一列:中断号:内核分配给该中断的唯一编号
- 第二列:中断触发次数:自系统启动以来,该中断在
CPU0
上触发的累计次数(0
表示未触发) - 第三列:中断控制器名称:处理该中断的硬件控制器
- 第四列:硬件中断线号:中断控制器内部的物理中断线编号(与芯片手册对应)
- 第五列:触发类型:
Level
:电平触发(高/低电平持续触发)Edge
:边沿触发(上升沿/下降沿触发)- 其他可能值:
RISING
(上升沿)、FALLING
(下降沿)、BOTH
(双边沿)
- 最后一列:关联设备或驱动的名称
2.如何确定某引脚的硬件中断线号?
- 如果该引脚被配置为GPIO,则硬件中断线号就是==GPIO的序号==,比如:
1 | gt9147:gt9147@14 { |
配置GPIO1的pin9为中断