LV10-07-中断-01-中断处理基础
本文主要是内核中中断处理的基础知识的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
Linux开发板 | 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2 |
u-boot | 2013.01 |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
点击查看相关文件下载
文件 | 下载链接 |
--- | --- |
一、中断相关API
1. 中断号获取
每个中断都有一个中断号,通过中断号即可区分不同的中断,有些地方也会叫中断线。我们之前裸机开发的时候,是直接看芯片手册,在这里也一样,我们需要将中断号写入设备树,然后在驱动中通过相关函数读取。
1.1 irq_of_parse_and_map()
我们使用以下命令查询一下函数所在头文件:
1 | grep irq_of_parse_and_map -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数获得设备树中的中断号并进行映射。
【函数参数】
- node : struct device_node * 类型,设备节点指针。
- index : int 类型, GPIO 索引,因为一个属性里面可能包含多个 GPIO ,此参数指定要获取哪个 GPIO 的编号,如果只有一个 GPIO 信息的话此参数为 0 。
【返回值】 int 类型,成功返回中断号,失败返回一个负数,绝对值表示错误码。
【使用格式】 none
【注意事项】 none
1.2 使用实例
- 设备树
1 | fs4412-key2{ |
- 获取中断号
1 | struct device_node *pnode = NULL; |
2. 申请中断
在 Linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断。
2.1 request_irq()
我们使用以下命令查询一下函数所在头文件:
1 | grep request_irq -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于申请中断。
【函数参数】
- irq : unsigned int irq 类型,要申请的中断号。
- handler : irq_handler_t 类型, 这是一个函数指针,用于指向中断处理函数,中断发生的时候会执行该函数,中断处理函数格式见使用实例。
点击查看 irq_handler_t
1 | /* include/linux/interrupt.h */ |
- flags :unsigned long 类型,中断标志,表示触发方式或者处理方式。
点击查看常见的 flags 取值
中断标志定义在linux内核源码的这个文件中:
1 | include/linux/interrupt.h |
- 常用flag取值
1 | 触发方式: |
- name :char * 类型,表示中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
- dev: void * 类型,如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体地址, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
【返回值】 int 类型,中断申请成功返回0 ,中断申请失败返回其他负值 ,如果返回 -EBUSY 的话表示中断已经被申请了。
【使用格式】 none
【注意事项】 none
2.2 使用实例
- 中断处理函数格式
1 | /** |
- 申请中断实例
1 | ret = request_irq(p_gmykey_dev->irqno, key2_irq_handle, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "fs4412key2", p_gmykey_dev); |
3. 释放中断
3.1 free_irq()
我们使用以下命令查询一下函数所在头文件:
1 | grep free_irq -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于释放中断。
【函数参数】
irq : unsigned int irq 类型,要释放的中断号。
dev: void * 类型,如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
【返回值】 int 类型,中断申请成功返回0 ,中断申请失败返回其他负值 ,如果返回 -EBUSY 的话表示中断已经被申请了。
【使用格式】 none
【注意事项】 none
3.2 使用实例
暂无
4. 中断处理函数
4.1 irq_handler_t
在linux内核源码的这个文件中,定义了中断处理函数的类型:
1 | include/linux/interrupt.h |
我们打开这个文件,可以看到该指针变量为:
1 | typedef irqreturn_t (*irq_handler_t)(int, void *); |
【参数说明】
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。 dev 用于区分共享中断的不同设备, 也可以指向设备数据结构。
【返回值】中断处理函数的返回值为 irqreturn_t 类型, irqreturn_t 类型定义如下:
1 | /* include/linux/irqreturn.h */ |
4.2 使用实例
1 | irqreturn_t xxx_irq_handle(int irq_no, void *arg) |
5. 中断使能与禁止
5.1 enable_irq ()
我们使用以下命令查询一下函数所在头文件:
1 | grep enable_irq -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于使能指定的中断。
【函数参数】
- irq : unsigned int irq 类型,要使能的中断号。
【返回值】 none
【使用格式】none
【注意事项】 none
5.2 disable_irq ()
我们使用以下命令查询一下函数所在头文件:
1 | grep disable_irq -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于禁止指定中断。
【函数参数】
- irq : unsigned int irq 类型,要禁止的中断号。
【返回值】 none
【使用格式】 none
【注意事项】 该函数在当前正在执行的中断处理函数执行完才返回,因此需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。
5.3 disable_irq_nosync ()
我们使用以下命令查询一下函数所在头文件:
1 | grep disable_irq_nosync -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于禁止指定中断。
【函数参数】
- irq : unsigned int irq 类型,要禁止的中断号。
【返回值】 none
【使用格式】 none
【注意事项】 disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。
二、上半部与下半部
前边我们在使用request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。
比如电容触摸屏通过中断通知 SOC 有触摸事件发生, SOC 响应中断,然后通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们们都知道 IIC 的速度最高也只有400Kbit/S,所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。
按上边做法,这个时候中断处理过程就分为了两部分:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
因此, Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断。
以下情况可以作为参考:
(1)如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
(2)如果要处理的任务对时间敏感,可以放到上半部。
(3)如果要处理的任务与硬件有关,可以放到上半部 。
三、下半部处理机制
下半部处理机制大概有三种,软中断,tasklet和工作队列,其实tasklet也是基于软中断的,相对而言更推荐使用tasklet,所以常见的下半部处理机制其实有两种,一种就是tasklet,另一种是工作队列。
1. 软中断
一开始 Linux 内核提供了 bottom half 机制来实现下半部,简称“BH”。后面引入了软中断和 tasklet 来替代“BH”机制,完全可以使用软中断和 tasklet 来替代 BH,从 2.5 版本的 Linux内核开始 BH 已经被抛弃了。
1.1 相关结构体
Linux 内核使用结构体 softirq_action 表示软中断,它定义在linux内核源码的这个文件中:
1 | include/linux/interrupt.h |
我们打开这个文件,可以看到结构体定义如下:
1 | struct softirq_action |
- action :函数指针变量,指向软中断服务函数。
1.2 软中断的定义
在 kernel/softirq.c 文件中一共定义了 10 个软中断 :
1 | static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp; |
其中 NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中 :
1 | enum |
可以看出,一共有 10 个软中断,因此 NR_SOFTIRQS 为 10,因此数组 softirq_vec 有 10 个元素。 softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。
1.3 相关函数
1.3.1 open_softirq()
我们使用以下命令查询一下函数所在头文件:
1 | grep open_softirq -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于注册软中断函数。
【函数参数】
- nr:int 类型,表示要开启的软中断,在10个软中断中选择一个。
- action:函数指针,指向软中断对应的处理函数。
【返回值】none
【使用格式】none
【注意事项】 要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数。
1.3.2 raise_softirq()
我们使用以下命令查询一下函数所在头文件:
1 | grep raise_softirq -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于触发已经注册的软中断,注册好软中断以后需要通过 raise_softirq 函数触发 。
【函数参数】
- nr:int 类型,表示要触发的软中断,在10个软中断中选择一个。
【返回值】none
【使用格式】none
【注意事项】 none
1.3.3 softirq_init()
我们使用以下命令查询一下函数所在头文件:
1 | grep softirq_init -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】软中断必须在编译的时候静态注册, Linux 内核使用 softirq_init 函数初始化软中断,这个函数好像不需要我们调用,内核会帮我们调用。
点击查看 softirq_init 函数详情
这个函数定义在linux内核源码的这个文件:
1 | kernel/softirq.c |
函数定义如下:
1 | void __init softirq_init(void) |
可以看出, softirq_init 函数默认会打开 TASKLET_SOFTIRQ 和 HI_SOFTIRQ。
【函数参数】
- nr:int 类型,表示要触发的软中断,在10个软中断中选择一个。
【返回值】none
【使用格式】none
【注意事项】softirq_init 函数默认会打开 TASKLET_SOFTIRQ 和 HI_SOFTIRQ。
2. tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议使用 tasklet。
2.1 相关结构体
Linux 内核使用 tasklet_struct 结构体来表示 tasklet,该结构体定义在linux内核源码的这个文件中:
1 | include/linux/interrupt.h |
我们打开这个文件,可以看到这个结构体定义如下:
1 | struct tasklet_struct |
2.2 相关函数
2.2.1 tasklet_init ()
我们使用以下命令查询一下函数所在头文件:
1 | grep tasklet_init -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数初始化一个 tasklet 。
【函数参数】
- t :struct tasklet_struct * 类型,要初始化的 tasklet。
- func: 函数指针,tasklet 的处理函数。
tasklet 处理函数一般格式
1 | void tasklet_func(unsigned long data) |
- data: unsigned long 类型,要传递给 tasklet 处理函数 func 的参数 。
【返回值】none
【使用格式】none
【注意事项】none
2.2.2 DECLARE_TASKLET()
我们使用以下命令查询一下函数所在头文件:
1 | grep DECLARE_TASKLET -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】这是一个宏,可以直接完成 tasklet 的定义和初始化。
【函数参数】
- name :要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的指针变量 。
- func: 函数指针,tasklet 的处理函数名。
tasklet 处理函数一般格式
1 | void tasklet_func(unsigned long data) |
- data: 要传递给 tasklet 处理函数 func 的参数 。
【返回值】none
【使用格式】none
【注意事项】none
2.2.3 tasklet_schedule()
我们使用以下命令查询一下函数所在头文件:
1 | grep tasklet_schedule -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数完成 tasklet 调度,用在中断处理函数中,tasklet 对应的处理函数将会在合适的时间运行 。
【函数参数】
- t :struct tasklet_struct * 类型,要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
【返回值】none
【使用格式】none
【注意事项】none
2.3 使用实例
下边是一个使用的框架
1 | /* 定义 taselet */ |
2.4 使用步骤
- (1)定义一个 taselet ;
- (2)初始化 tasklet;
- (3)编写 tasklet 处理函数;
- (4)在需要tasklet作为下半部处理的中断处理函数中进行 tasklet 调度。
3. 工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。
如果我们要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
3.1 相关结构体
3.1.1 工作结构体
Linux 内核使用 work_struct 结构体表示一个工作,它定义在linux内核源码的这个文件中:
1 | include/linux/workqueue.h |
我们打开这个文件,可以看到这个结构体定义如下:
1 | struct work_struct { |
3.1.2 工作队列结构体
工作组成工作队列,Linux 内核使用 workqueue_struct 结构体表示一个工作队列,它定义在linux内核源码的这个文件中:
1 | kernel/workqueue.c |
我们打开这个文件,可以看到这个结构体定义如下:
1 | struct workqueue_struct |
3.1.3 工作者线程
Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作, Linux 内核使用 worker 结构体表示工作者线程,它定义在linux内核源码的这个文件中:
1 | kernel/workqueue_internal.h |
我们打开这个文件,可以看到这个结构体定义如下:
1 | struct worker |
可以看到每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。
3.2 相关函数
3.2.1 INIT_WORK()
我们使用以下命令查询一下函数所在头文件:
1 | grep INIT_WORK -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数初始化一个工作 。
【函数参数】
- _work :struct work_struct 类型,要初始化的工作。
- _func : 函数指针,工作对应处理函数。
工作处理函数一般格式
工作处理函数指针是 work_func_t 类型的,该函数指针类型为:
1 | /* include/linux/workqueue.h */ |
所以工作的处理函数格式为:
1 | void xxx_work_func(struct work_struct *pwk) |
【返回值】none
【使用格式】none
【注意事项】none
3.2.2 DECLARE_WORK()
我们使用以下命令查询一下函数所在头文件:
1 | grep DECLARE_WORK -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】这是一个宏,可以直接完成工作的定义和初始化。
【函数参数】
- n :表示定义的工作,work_struct类型的。
- f :表示工作对应的处理函数 。
工作处理函数一般格式
工作处理函数指针是 work_func_t 类型的,该函数指针类型为:
1 | /* include/linux/workqueue.h */ |
所以工作的处理函数格式为:
1 | void xxx_work_func(struct work_struct *pwk) |
【返回值】none
【使用格式】none
【注意事项】none
3.2.3 schedule_work()
我们使用以下命令查询一下函数所在头文件:
1 | grep schedule_work -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数完成队列调度,用在中断处理函数中 。
【函数参数】
- work :struct work_struct * 类型,要调度的工作结构体变量。
【返回值】none
【使用格式】none
【注意事项】none
3.3 使用实例
下边是一个使用的框架:
1 | /* 定义工作(work) */ |
3.4 使用步骤
- (1)定义一个工作;
- (2)初始化工作;
- (3)编写工作处理函数;
- (4)在需要工作队列作为下半部处理的中断处理函数中调度工作队列。
4. 下半部机制比较
- 任务机制
工作队列(workqueue)——内核线程,可以睡眠,运行时间无限制。
- 异常机制——不能睡眠 下半部执行时间不宜太长( < 1s)
(1)软中断,它的接口不方便;
(2)tasklet,无具体延后时间要求时可以采用。
(3)定时器,有具体延后时间要求时可以采用。