目录
[一. Linux 的中断系统](#一. Linux 的中断系统)
[1.1 中断概念](#1.1 中断概念)
[1.2 回顾裸机中中断处理方法](#1.2 回顾裸机中中断处理方法)
[1.3 Linux 中断相关API函数](#1.3 Linux 中断相关API函数)
[1.3.1 request_irq 函数](#1.3.1 request_irq 函数)
[1.3.2 free_irq 函数](#1.3.2 free_irq 函数)
[1.3.3 中断处理函数](#1.3.3 中断处理函数)
[1.3.4 中断使能与禁止函数](#1.3.4 中断使能与禁止函数)
[二. 中断的上半部与下半部](#二. 中断的上半部与下半部)
[2.1 简介](#2.1 简介)
[2.2 下半部的实现方式](#2.2 下半部的实现方式)
[2.2.1 软中断](#2.2.1 软中断)
[2.2.2 tasklet](#2.2.2 tasklet)
[2.2.3 工作队列 Workqueue](#2.2.3 工作队列 Workqueue)
[三. 设备树中的中断信息](#三. 设备树中的中断信息)
[3.1 .dtsi 文件](#3.1 .dtsi 文件)
[3.2 设备树中断函数](#3.2 设备树中断函数)
[3.2.1 获取中断号](#3.2.1 获取中断号)
[四. 按键中断示例代码](#四. 按键中断示例代码)
[4.1 修改设备树](#4.1 修改设备树)
[4.2 驱动代码](#4.2 驱动代码)
[4.3 应用层代码](#4.3 应用层代码)
[4.4 结果](#4.4 结果)
[五. 阻塞IO 与非阻塞 IO](#五. 阻塞IO 与非阻塞 IO)
[5.1 区别](#5.1 区别)
[5.2 等待队列](#5.2 等待队列)
[5.3 相关函数](#5.3 相关函数)
[5.3.1 init_waitqueue_head](#5.3.1 init_waitqueue_head)
[5.3.2 等待唤醒](#5.3.2 等待唤醒)
[5.3.3 等待事件](#5.3.3 等待事件)
[5.4 修改示例代码](#5.4 修改示例代码)
一. Linux 的中断系统
1.1 中断概念
中断是指CPU在处理事件的过程中,遇到了突发事情需要去处理,CPU暂停当前程序转去处理突发事件,处理完之后又返回原程序被打断的地方继续执行,这个过程就叫做中断。
中断分为了内部中断和外部中断
内部中断来源于CPU内部,外部中断来源于CPU外部
1.2 回顾裸机中中断处理方法
1、使能中断,初始化相应的中断
2、注册中断处理函数,也就是向中断向量表的指定下标处写入中断处理函数
3、中断发生后执行相应的中断处理函数
1.3 Linux 中断相关API函数
1.3.1 request_irq 函数
在 Linux 内核中要想使用某个中断是需要申请的,request_irq 函数用于申请中断,request_ irq 函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。request_irq 函数会激活(使能)中断,所以不需要手动去使能中断

**作用:**注册中断处理函数,绑定中断号与处理函数
参数:
**irq:**要申请中断的中断号。
**handler:**中断处理函数,当中断发生以后就会执行此中断处理函数,类型如下:
cpp
typedef irqreturn_t (*irq_handler_t)(int, void *);
flags: 中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志,这里我们 介绍几个常用的中断标志,如图所示:

name: 中断名字,设置以后可以在 /proc/interrupts 文件中看到对应的中断名字,proc下都是与进程相关的
dev: 如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体,**dev 会传递给中断处理函数 irq_handler_t 的第二个参数,**也可设置为 NULL
返回值: 0 表示中断申请成功,其他负值中断申请失败,如果返回**-EBUSY** 的话表示中断已经被申请了
1.3.2 free_irq 函数
使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断

**作用:**释放已经注册的中断资源
参数:
**irq:**要释放的中断。
**dev:**一般为 NULL
**返回值:**无
1.3.3 中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下:
cpp
irqreturn_t (*irq_handler_t) (int, void *)
参数:
第一个参数是要中断处理函数要相应的中断号 。第二个参数是一个指向 void 的指针 ,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。
返回值:
中断处理函数的返回值为 irqreturn_t 类型,irqreturn_t 类型定义如下所示
cpp
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
代表的意义:
**IRQ_NONE:**中断触发了但是不是由本函数处理,因为中断可以共
**IRQ_NANDLED:**与上面相反,处理中断
**IRQ_WAKE_THREAD:**处理中断放在另一线程中,且唤醒线程,在中断函数中唤醒
等待的队列,并且是条件变量置一
1.3.4 中断使能与禁止函数

**作用:**enable_irq 和 disable_irq 用于使能和禁止指定的中断
参数:
**irq:**就是要禁止的中断号。disable_irq
**返回值:**无
注意:
disable_irq 函数要等到当前正在执行的中断处理函数执行完才返回

**作用:**禁止中断,但是与 disable_irq 不同的是:不会等待中断处理程序结束后返回,会立即返回

**作用:**local_irq_enable 用于使能当前处理器中断系统,local_irq_disable 用于禁止当前处理器中断系统。
上面两个函数不足之处: 假如 A 任务调用 local_irq_disable 关闭全局中断 10S,当关闭了 2S 的时候 B 任务开始运行,B 任务也调用local_irq_disable 关闭全局中断 3S,3 秒以后 B 任务调用 local_irq_enable 函数将全局中断打开了。此时才过去 2+3=5 秒的时间,然后全局中断就被打开了,但是 A 任务要关闭 10S 全局中断,可能系统会崩溃。

作用:local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。
local_irq_restore 用于恢复中断,将中断到 flags 状态
二. 中断的上半部与下半部
2.1 简介
在使用 request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。中断处理函数一定要快点执行完毕,越短越好,但是现实中有些中断处理过程就是比较费时间,必须要对其进行处理,缩小中断处理函数的执行时间,这个时候中断处理过程就分为 了两部分:
**上半部:**上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成,比如置一个标志位,置一个硬件寄存器的标志位等等,通常设置为不可被新中断打断
上半部使用的场景:
要处理的任务不希望被其他中断打断;要处理的任务对时间敏感;处理与硬件相关的事物
**下半部:**如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出
下半部使用的场景:
处理并不是紧急的耗时任务;可以被打断

2.2 下半部的实现方式
2.2.1 软中断
软中断系统资源有限,常用于系统级的任务
cpp
系统中,使用软中断的
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ, /普通定时器
NET_TX_SOFTIRQ, /网络发送软中断
NET_RX_SOFTIRQ, /网络接收软中断
BLOCK_SOFTIRQ, /块设备
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /tasklet
SCHED_SOFTIRQ, /调度器
HRTIMER_SOFTIRQ, /高精度定时器
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
2.2.2 tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,软中断调度tasklet
执行时机是通常是上半部返回的时候
软中断和tasklet运行于中断上下文,不允许休眠,非则系统会崩溃
相关的函数或者宏:
1、定义+初始化宏

参数:
name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的时候变量
cpp
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; //处理函数
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
typedef void (*work_func_t)(struct work_struct *work);
func就是 tasklet 的处理函数,类型如下:
cpp
void (*func)(unsigned long);
data 是传递给 func 函数的参数
2、调用

**作用:**在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适
的时间运行
参数:
**t:**要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name
2.2.3 工作队列 Workqueue
工作队列运行于进程上下文,允许休眠
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的 工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠
Linux 内核使用 work_struct 结构体表示一个工作
相关的宏或者函数:
1、初始化宏

参数:
_work 表示要初始化的工作**(struct work_struct *类型)**
cpp
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long); //对应的处理函数
unsigned long data;
};
_func 是工作对应的处理函数,类型如下:
cpp
void workque_handler(struct work_struct *work); //工作队列处理函数
2、调度函数

参数:
**work:**要调度的工作。
**返回值:**0:成功,其他值:失败。
三. 设备树中的中断信息
如果使用设备树的话就需要在设备树中设置好中断属性信息,Linux 内核通过读取设备树中的中断属性信息来配置中断
3.1 .dtsi 文件
打开 imx6ull.dtsi 文件,其中的 intc 节点就是 I.MX6ULL 的中断控制器节点,节点内容如下所示:
cpp
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
1、compatible 属性值 为**"arm,cortex-a7-gic"**在 Linux 内核源码中搜索"arm,cortex-a7 -gic"即可找到 GIC 中断控制器驱动文件
2、#interrupt-cells和#address-cells、#size-cells 一样。表示此中断控制器下设备的 cells 大小,对于设备而言,会使用 interrupts 属性描述中断信息,#interrupt-cells 描述了 interrupts 属性的 cells 大小,也就是一条信息有几个 cells。每个 cells 都是 32 位整形值,对于 ARM 处理的 GIC 来说,一共有 3 个 cells,这三个 cells 的含义如下:
第一个 cells:中断类型,0 表示 SPI 中断,1 表示 PPI 中断。
第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断 号的范围为 0~15。
第三个 cells:中断触发类型
为 1 的时候表示上升沿触发
为 2 的时候 表示下降沿触发
为 4 的时候表示高电平触发
为 8 的时候表示低电平触发。
3、interrupt-controller 节点为空,表示当前节点是中断控制器
对于 gpio 来说,gpio 节点也可以作为中断控制器,比如 imx6ull.dtsi 文件中的 gpio5 节点内 容如下所示:
cpp
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>;
};
1、interrupts 描述中断源信息 ,对于 gpio5 来说一共有两条信息,中断类型都是 SPI, 触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75
2、interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO 的中断
3、将#interrupt-cells 修改为 2
3.2 设备树中断函数
3.2.1 获取中断号

参数:
**dev:**设备节点。
**index:**索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信
息。
**返回值:**中断号。
如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,如下:

参数:
**gpio:**要获取的 GPIO 编号。
**返回值:**GPIO 对应的中断号
四. 按键中断示例代码
4.1 修改设备树
cpp
putekey
{
#address-cells = <1>;
#size-cells = <1>;
compatible = "pt,pute-key";
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ptkey>;
gpio-key = <&gpio1 18 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_FALLING>;
};
4.2 驱动代码
中断处理函数:
cpp
//中断处理函数
static irqreturn_t key_irq_handler(int irq_num, void * arg)
{
if(led_stauts == 0)
{
led_stauts = 1;
}
else
{
led_stauts = 0;
}
gpio_set_value(led_gpio, led_stauts);
return IRQ_HANDLED;
}
cpp
//杂项设备结构体
static struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR, //这个宏为255,但是次设备号不一定是255;自动获取次设备号
.name = DEV_NAME, //设备节点名 和 /sys/class/misc/xxx 两者名称相同
.fops = &fops //杂项设备文件操作函数集合
};
static int __init mydrv_init(void)
{
int ret = 0;
/注册杂项设备
ret = misc_register(&misc_dev);
if(ret)
{
pr_err("misc_register error, errno:%d\n", ret);
return ret;
}
pnode_key = of_find_node_by_path("/putekey");
if(!pnode_key)
{
pr_err("of_find_node_by_path error\n");
ret = -ENODEV;
goto del_misc;
}
//参数三是0:在设备树中<>代表数组,0就代表下标是0对应的GPIO
//返回值:成功:正数 失败:负数错误号
key_gpio = of_get_named_gpio(pnode_key, "gpio-key", 0);
if(key_gpio < 0)
{
ret = key_gpio;
pr_err("of_get_named_gpio error, errno:%d\n", ret);
goto del_misc;
}
//参数二:标签,随便赋值
ret = gpio_request(key_gpio, "gpio_for_key");
if(ret)
{
pr_err("gpio_request error, errno:%d\n", ret);
goto del_misc;
}
ret = gpio_direction_input(key_gpio);
if(ret)
{
pr_err("gpio_direction_input error, errno:%d\n", ret);
goto del_gpio;
}
/获取中断号
/key_irq_num = of_irq_get(pnode_key, 0);
key_irq_num = gpio_to_irq(key_gpio);
if(key_irq_num < 0)
{
ret = key_irq_num;
pr_err("of_irq_get error, errno:%d\n", ret);
goto del_gpio;
}
pr_info("key_irq_num is %d\n", key_irq_num);
//申请中断
ret = request_irq(key_irq_num, key_irq_handler, IRQF_TRIGGER_FALLING, "key_interrupt", NULL);
if(ret < 0)
{
pr_err("request_irq error, errno:%d\n", ret);
goto del_gpio;
}
//要实现按键控制 LED 就得初始化LED,以下是LED部分
pnode_led = of_find_node_by_path("/puteled");
if(!pnode_led)
{
ret = -ENODEV;
pr_err("of_find_node_by_path error, errno:%d\n", ret);
goto del_gpio;
}
led_gpio = of_get_named_gpio(pnode_led, "gpio-led", 0);
if(led_gpio < 0)
{
ret = led_gpio;
pr_err("of_get_named_gpio error, errno:%d\n", ret);
goto del_gpio;
}
ret = gpio_request(led_gpio, "led-gpio");
if(ret)
{
pr_err("gpio_request error, errno:%d\n", ret);
goto del_gpio;
}
ret = gpio_direction_output(led_gpio, 1);
if(ret)
{
pr_err("gpio_direction_output error, errno:%d\n", ret);
goto del_led_gpio;
}
pr_info("key_irq drv init finish!\n");
return 0;
del_led_gpio:
gpio_free(led_gpio);
del_gpio:
gpio_free(key_gpio);
del_misc:
misc_deregister(&misc_dev);
return ret;
}
static void __exit mydrv_exit(void)
{
gpio_free(led_gpio);
free_irq(key_irq_num, NULL);
gpio_free(key_gpio);
//卸载杂项设备
misc_deregister(&misc_dev);
pr_info("hello, bbb\n");
return;
}
module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("this drv is key test\n");
MODULE_AUTHOR("@dl");
4.3 应用层代码
cpp
int main(int argc, char **argv)
{
int fd = 0;
fd = open("/dev/mykey0", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
char buffer[10] = {0};
int sw = 0;
int key_status = 0;
while (1)
{
read(fd, &key_status, sizeof(key_status));
if(key_status == 1)
{
sw = 1;
printf("key status is on\n");
}
else if(key_status == 0)
{
sw = 0;
printf("key status is off\n");
}
}
close(fd);
return 0;
}
4.4 结果
可以看到应用层一直在轮询按键的状态,这样一直占用 CPU 资源很不好,故可将其修改为阻塞 IO


五. 阻塞IO 与非阻塞 IO
5.1 区别
阻塞IO:

非阻塞IO:

5.2 等待队列
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将 CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。Linux 内核提供了等待队列(wait queue) 来实现阻塞进程的唤醒工作,如果要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体 wait_queue _head_t 表示
5.3 相关函数
5.3.1 init_waitqueue_head

**作用:**初始化等待队列头
**参数:**参数 q 就是要初始化的等待队列头
**返回值:**无
5.3.2 等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程:

参数 q 就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。 wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进 程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程
5.3.3 等待事件

5.4 修改示例代码
读函数
cpp
ssize_t myread(struct file *f, char __user *userdata, size_t size, loff_t *off)
{
unsigned long ret = 0;
wait_condition = 0;
wait_event_interruptible(wq, wait_condition);
ret = gpio_get_value(key_gpio);
if( ret == 0 )
{
key_status = KEY_ON;
}
else
{
key_status = KEY_OFF;
}
ret = copy_to_user(userdata, &key_status, sizeof(key_status));
if(ret)
{
pr_err("copy_to_user error\n");
return ret;
}
pr_info("read caller\n");
return sizeof(key_status);
}
中断处理函数:
cpp
//中断处理函数
static irqreturn_t key_irq_handler(int irq_num, void * arg)
{
wait_condition = 1;
//不是自动唤醒,必须加上 wake up
wake_up(&wq);
if(led_stauts == 0)
{
led_stauts = 1;
}
else
{
led_stauts = 0;
}
gpio_set_value(led_gpio, led_stauts);
return IRQ_HANDLED;
}
在 drv_init 中新增:
cpp
//等待队列头
static wait_queue_head_t wq;
//等待条件
static int wait_condition = 0;
//初始化等待队列头
init_waitqueue_head(&wq);
结果:
