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部分

image-20240707152257135

主机驱动层

中断控制器的驱动设备树一般不需要用户自己写,厂家基本上都写好了。同一个SoC可能有多个中断控制器,且多为==嵌套==关系

对于I.MX6ULL SoC,其有2类中断控制器,定义在imx6ull.dsti中,由interrupt-controller;标识:

1.首先是Arm内核Soc中的GIC中断控制器(相当于STM32中的NVIC)

1
2
3
4
5
6
7
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};

#interrupt-cells 表示此中断控制器下子节点interrupt属性长度,对于设备节点而言,会使用 interrupts 属性描述中断信息。

每个 cell 都是 32 位整形值,GIC的子节点,一共有3个cells,这三个 cells 的含义如下:

  • 第一个 cells:中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断
  • 第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0987,对于 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
2
3
4
5
6
7
8
9
10
gpio5: gpio@020ac000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x020ac000 0x4000>;
interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>; //子节点的interrupt属性长度为2
};

设备驱动层

对于使用了中断的某个设备,需要在其设备树中描述和中断相关的信息,和中断相关的属性:

  • interrupt-parent:建立设备与中断控制器之间的层级关系,指定中断的路由方式
  • interrupts:设置:中断号、触发方式等属性

==单级中断控制器==:

1
2
3
4
5
6
7
8
9
10
11
12
// 中断控制器定义
intc: interrupt-controller {
compatible = "arm,gic";
#interrupt-cells = <3>;
};

// 设备节点,没有显示指定interrupt-parent,其实继承了其父节点soc{}的该属性
uart0: serial@101f0000 {
compatible = "ns16550";
interrupt-parent = <&intc>; // 指定中断控制器
interrupts = <0 24 4>; // 中断类型-中断号-触发类型
};

==多级中断控制器==:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 主中断控制器(如ARM GIC)
gic: interrupt-controller@fee00000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
};

// 次级中断控制器(如GPIO控制器)
gpio_intc: interrupt-controller@48000000 {
compatible = "ti,gpio-intc";
interrupt-parent = <&gic>; // 本级控制器本身的中断向上级GIC提交
#interrupt-cells = <2>;
interrupts = <0 32 4>; // 中断类型-中断号-触发类型
};

// 实际设备(连接在GPIO控制器上)
buttons {
compatible = "gpio-keys";
interrupt-parent = <&gpio_intc>; // 指定次级控制器
interrupts = <14 IRQ_TYPE_EDGE_FALLING>; // 中断号-触发类型
};

触发类型定义如下:

1
2
3
4
5
6
#define IRQ_TYPE_NONE        0   // 无触发
#define IRQ_TYPE_EDGE_RISING 1 // 上升沿
#define IRQ_TYPE_EDGE_FALLING 2 // 下降沿
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING) // 双边沿
#define IRQ_TYPE_LEVEL_HIGH 4 // 高电平
#define IRQ_TYPE_LEVEL_LOW 8 // 低电平

核心层

常用API

1.申请中断上半部:该函数会自动使能中断,不需要再手动使能

1
2
3
4
5
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
  • irq:要申请中断的中断号
  • handler:中断处理函数,当中断发生以后就会执行此中断处理函数
  • flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志,这里我们介绍几个常用的中断标志,这些标志可以用|组合
image-20240707152550065
  • 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
2
3
4
5
6
enum irqreturn {
IRQ_NONE = (0 << 0), //通知内核当前中断处理程序没有处理该中断,内核可能会尝试其他可能的处理程序.
IRQ_HANDLED = (1 << 0), //通知内核当前中断已经被处理,内核可以继续其他的处理或恢复正常操作.
IRQ_WAKE_THREAD = (1 << 1), //通知内核需要唤醒一个线程来进一步处理中断。常见的应用场景是中断处理程序先执行快速处理,然后唤醒一个线程来执行较长时间的处理
};
typedef enum irqreturn irqreturn_t;

在返回时一般需要用IRQ_RETVAL包裹一下再返回,比如:

1
return IRQ_RETVAL(IRQ_HANDLED);

中断上半部与下半部

在操作系统内核中,处理中断的过程通常分为两部分:上半部和下半部。这种分层处理的方式有助于确保及时响应中断的同时,避免在中断处理程序中执行过多耗时操作,从而提高系统的响应性和可靠性。

上半部

上半部是中断处理程序的第一部分,通常是在中断上下文中执行的,负责紧急任务(如清除中断标志、读取关键数据)。它的主要特点包括:

  1. 中断处理:检测和识别中断的发生,确定中断的来源和类型
  2. 快速响应:执行必要的、紧急的中断处理任务,通常是一些简短的操作,例如更新状态、唤醒进程或者触发更复杂的处理流程
  3. 禁用中断:为了避免竞态条件和保护关键资源,上半部在处理过程中可能会临时禁用本地 CPU 的中断,确保当前中断处理不会被其他中断打断,并在执行完中断服务函数时恢复中断,相当于进入了==临界区==

irq_request()注册的中断服务函数就属于上半部

下半部

下半部是中断处理程序的第二部分,在上半部稍后时间点延迟执行非紧急任务(如数据处理、唤醒进程)。它的主要特点包括:

  1. 延迟处理:处理那些不能立即在上半部完成的任务,例如复杂的数据结构操作、磁盘和网络 I/O 操作,或者需要与用户空间交互的任务。
  2. 解锁资源:在上半部临时禁用中断后,下半部可以==重新启用中断==,允许其他中断和进程继续执行,从而避免长时间阻塞整个系统。
  3. 异步执行:下半部通常以一种异步的方式执行,例如使用work queue、tasklet、定时器…以便在合适的时间点处理。

区别与应用

  • 区别:上半部处理程序在中断上下文中执行,必须快速响应并保护关键资源;下半部则允许更多时间处理复杂任务,并且可以在稍后时间点执行。
  • 应用:上半部处理程序通常直接与硬件交互、更新数据结构或触发通知;下半部处理程序则处理复杂的异步任务、与用户空间的交互或长时间操作。

如何一个函数到底放在上半部还是下半部?

  • 上半部:
    • 不希望被打断
    • 对时间敏感
    • 与硬件相关
  • 下半部:
    • 除了上述3种例子以外的情况

下半部机制

Linux内核提供了3种机制让我们实现中断下半部:

soft_irq

软中断是效率最高的一种方式,在中断上下文运行,不可睡眠,可并发执行,函数要求可重入,在do_softirq中执行

tasklet

tasklet软中断上下文中==原子地==执行异步任务,而无需担心竞态条件或长时间占用 CPU

1
2
3
4
5
6
7
8
struct tasklet_struct
{
struct tasklet_struct *next; /* 下一个 tasklet */
unsigned long state; /* tasklet 状态 */
atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
void (*func)(unsigned long); /* tasklet 执行的函数 */
unsigned long data; /* 函数 func 的参数 */
};

tasklet有以下特点:

  • tasklet 是一种轻量级的结构,适合于执行简单但频繁的任务
  • 它们的开销比工作队列要小,因为它们在内核中直接以链表形式管理,而不涉及线程调度和内核线程的开销
  • tasklet可以在软中断上下文中执行,且不会被其他线程抢占,是原子性的,但是会被其他中断抢占
  • 不允许在下半部中睡眠线程,因为它不在线程上下文中,看不到调度器
  • 个人感觉用tasklet下半部和不用下半部差不多,都是在中断的上下文中,唯一的区别就是前者又开启了中断

使用tasklet的步骤:

image-20240707150119026

相关函数:

1.tasklet的初始化和禁用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 初始化一个tasklet实例
DECLARE_TASKLET(name, func, data);
// 初始化一个tasklet实例
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

// 禁用一个tasklet实例
DECLARE_TASKLET_DISABLED(name, func, data);

// example1
void my_tasklet_function(unsigned long data); //下半部回调函数
DECLARE_TASKLET(my_tasklet, my_tasklet_function, 0);

// example2
struct tasklet_struct my_tasklet;
tasklet_init(&my_tasklet, my_tasklet_function, 0);
  • nametasklet 实例的名称。
  • functasklet 执行的函数。
  • data:传递给 func 的参数

2.tasklet的调度

1
2
// 启动一个tasklet实例的调度,操作系统会在合适时调用该下半部
void tasklet_schedule(struct tasklet_struct *t);
  • ==此函数需要在中断上半部中被调用==

workqueue

工作队列是在进程上下文中进行的,工作队列将要延迟执行的一个工作(下半部)交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠重新调度


在使用工作队列定义下半部时,只需要初始化一个work_struct就行了,不需要管工作队列,它有操作系统本身维护

Linux内核使用以下的结构体表示一个工作:

1
2
3
4
5
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};

使用工作队列的步骤:

相关函数:

1
2
3
4
5
6
7
8
// 初始化一个已经创建的工作(work_struct)
#define INIT_WORK(_work, _func)

// 创建 + 初始化一个工作
#define DECLARE_WORK(n, f)//n 表示定义的工作(work_struct),f 表示工作对应的处理函数

// 启动下半部的调度(需要在上半部调用)
bool schedule_work(struct work_struct *work)

调试

1.通过cat /proc/interrupts查看当前中断信息,举个例子:

1
2
3
4
5
6
7
8
9
10
11
           CPU0
16: 23132 GPC 55 Level i.MX Timer Tick
18: 0 GPC 33 Level 2010000.ecspi
19: 308 GPC 26 Level 2020000.serial
20: 0 GPC 50 Level 2034000.asrc
36: 7 gpio-mxc 9 Edge gt9147
45: 0 gpio-mxc 18 Edge gpio_key_platform
46: 0 gpio-mxc 19 Edge 2190000.usdhc cd
195: 0 GPC 4 Level 20cc000.snvs:snvs-powerkey
196: 16826 GPC 120 Level 20b4000.ethernet
...

各字段含义:

  1. 第一列:中断号​​:内核分配给该中断的唯一编号
  2. 第二列:​​中断触发次数​:自系统启动以来,该中断在CPU0上触发的累计次数(0表示未触发)
  3. 第三列:中断控制器名称​​:处理该中断的硬件控制器
  4. 第四列​:硬件中断线号​:中断控制器内部的物理中断线编号(与芯片手册对应)
  5. 第五列:​触发类型​​:
    • Level:电平触发(高/低电平持续触发)
    • Edge:边沿触发(上升沿/下降沿触发)
    • 其他可能值:RISING(上升沿)、FALLING(下降沿)、BOTH(双边沿)
  6. 最后一列:​关联设备或驱动的名称​

2.如何确定某引脚的硬件中断线号

  • 如果该引脚被配置为GPIO,则硬件中断线号就是==GPIO的序号==,比如:
1
2
3
4
5
6
7
gt9147:gt9147@14 {
...
interrupt-parent = <&gpio1>;
interrupts = <9 0>;
interrupt-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
...
};

配置GPIO1的pin9为中断