本文介绍中断机制,中断作为需要频繁使用的功能,本文将详细介绍linux内核中的中断机制。
嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!
当你在刷手机的时候,此时突然npy打电话来了,于是你退出刷手机状态,接听npy电话,此过程即为中断。
简单来说,中断会让CPU停止正在执行的程序,转而让CPU执行中断处理函数,执行完再返回原程序。
另外,整个操作系统就是一个中断驱动的死循环,即裸机开发中常写的while(true) {}
。其他所有的事情都是由操作系统提前注册的中断机制和其对应的中断处理函数完成。
中断主要有4个用途:外设异步通知CPU,CPU间发送消息,处理CPU异常,实现系统调用
中断信号的产生有以下4个来源:
1. 外设 :外设产生的中断信号是异步的,一般也叫硬件中断,硬件中断按照是否可以屏蔽分为可屏蔽中断和不可屏蔽中断,例如:网卡、磁盘、定时器都可以产生硬件中断。
2. CPU:一个CPU向另一个CPU发送中断,叫做IPI
(处理器间中断),是一种特殊的硬件中断,也是异步的。
3. CPU异常:CPU在执行指令的过程中发现异常会向自己发送中断信号,这种中断是同步的,一般也叫做软件中断。
4. 中断指令:直接用CPU指令来产生中断信号,这种中断和CPU异常一样是同步的,也可以叫做软件中断。例如,中断指令int 0x80可以用来实现系统调用。
在单片机或裸机开发中,中断的处理方法是:
①、使能中断,初始化相应的寄存器
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
③、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。
中断的执行时间不可以过长,否则会影响对新的中断信号的响应性,所以要尽量缩短中断执行场景的时间,为此对异步中断的处理方法有两种:
1、立即完全处理:
对于简单好处理的异步中断可以立即进行完全处理。
2、立即预处理(上半部)+稍后完全处理(下半部):
对于处理起来耗时的可以采取立即预处理加稍后完全处理的方式来实现中断。 立即预处理只能用直接处理来实现,而稍后完全处理的方法分两类:直接中断后处理有 softirq
(软中断)、tasklet
(微任务)、线程化中断后处理有workqueue
(工作队列)、threaded_irq
(中断线程)。
此处有一个概念:硬件中断、软件中断、硬中断、软中断是不同的概念,前两个是中断来源,后两个是中断处理方式。
我们每个人都有各自的身份证,代表每个人的唯一id,这样通过身份证就可以指定唯一的人。中断也是这样的,不同的中断信号有不同的处理方式,那么系统如何区分呢,即通过中断向量号。中断向量号是一个整数,CPU收到一个中断信号会根据这个信号的中断的向量号去查询中断向量表,根据中断向量表调用相应的处理函数。
中断向量表是一个表,表里面存放的是中断向量。中断服务程序的入口地址或存放中端服务程序的首地址成为中断向量,因此中断向量表是一系列中断服务程序入口地址组成的表。
软件中断主要是两类:CPU异常和指令中断。
CPU在执行过程中遇到异常就会给自己发送异常信号,但是异常信号不一都是错误。
指令中断是因为执行指令而产生了中断,指令中断是执行特定指令而发生的中断,设计这些指令的目的就是为了产生中断。其中INT n可以产生任意中断,Linux用int ix80来作为系统调用的指令。
硬件中断分为外设中断和处理器间中断(IPI)。
外设中断和软件中断有一个很大的不同,软件中断是CPU自己给自己发送中断,而外设中断是需要外设发送中断给CPU。显然不可能将所有外设都直接连到CPU上,因此需要一个中间设备,替CPU连接到所有外设接受中断信号,这个设备叫中断控制器
不同的架构有不同的中断控制器,比如STM32这种Cortex-M内核的单片机叫NVIC,Cortex-A中叫GIC,x86上Intel开发的叫APIC。
上半部希望执行时间快,不会占用很长时间的处理;下半部多用于处理耗时的代码,保证中断函数的快进快出。
哪部分属于上半部,哪部分属于下半部没有明确规定,可以有一下一些原则:
①、要处理的内容不希望被其他中断打断,可以放入上半部
②、如果要处理的任务对时间敏感,可以放上半部
③、如果要处理的任务与硬件有关,可以放上半部
上半部的实现直接编写中断处理函数,下半部有多种实现机制,具体下文介绍。
Linux内核中使用结构体softirq_action
表示软中断。
/*
* @description : 注册软中断处理函数
* @param-nr : 要开启的软中断
* @param-action: 软中断对应的处理函数
* @return : 无
*/
void open_softirq(int nr, void (*action)(struct softirq_action *))
软中断类型枚举如下:
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, /* RCU 软中断 */
NR_SOFTIRQS
};
注册好软中断后需要通过raise_softirq()
函数触发
/*
* @description: 出发软中断
* @param-nr : 要触发的中断
* @return : 无
*/
void raise_softirq(unsigned int nr)
tasklet使用方法简单、灵活,自带有锁机制可以防止多个CPU同时运行,是中断处理下半部分最常用的一种方法,通过执行中断处理程序来快速完成上半部分的工作,接着通过调用tasklet使得下半部分的工作得以完成。tasklet执行过程中是可以被硬件中断所中止的,这样不会影响系统实时性。是一种将任务推后执行的一种机制。
软中断和tasklet
之间,建议使用tasklet
,用tasklet_struct
结构体表示tasklet
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_init
函数:
/*
* @description: 初始化tasklet
* @param-t : 要初始化的tasklet
* @param-func : tasklet的处理函数
* @param-data : 要传递给func的参数
* @return : 无
*/
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
也可以使用宏定义来初始化:
#include
/*
* @description: 初始化tasklet
* @param-name : 要初始化的tasklet的名字
* @param-func : tasklet的处理函数
* @param-data : 要传递给func的参数
*/
DECLARE_TASKLET(name, func, data)
如果中途不想使用tasklet,则可以调用该函数释放它,不能在tasklet回调函数调用。和初始化函数作用相反。这个函数首先等待该tasklet执行完毕,然后再将它释放。
/*
* @description: 释放tasklet
* @param-t : 要释放的tasklet
* @return : 无
*/
void tasklet_kill(struct tasklet_struct *t);
在上半部中,使用tasklet_schedule
函数使tasklet
在合适的时间运行
/*
* @description: 上半部中调用使tasklet运行
* @param-t : 要调度的tasklet
* @return : 无
*/
void tasklet_schedule(struct tasklet_struct *t)
tasklet
的使用模板:
/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
总结:tasklet被调用之后,其绑定的处理函数不会被马上运行,需要在合适的时机去运行!在tasklet被调度以后,只要有机会它就会尽可能早的运行,在它还没有得到运行机会之前,如果一个相同的tasklet又被调度了,那么它仍然只会运行一次。本质上tasklet链表不能存在相同的tasklet对象。
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
tasklet机制是在中断上下文执行,所以在tasklet中不可以执行休眠动作。
关于workqueue与tasklet这两个机制的选择,看具体的工作过程
有没有休眠动作
!
Linux内核使用work_struct
结构体表示一个工作
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程基本不用管。我们需要做的是定义一个work_struct
结构体然后使用宏定义来初始化工作:
// _work表示要初始化的工作,_func是工作对应的处理函数
#define INIT_WORK(_work, _func)
也可以使用 DECLARE_WORK
宏一次性完成工作的创建和初始化
// n表示定义的工作(work_struct), f表示要处理的函数
#define DECLARE_WORK(n, f)
和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work
/*
* @description: 在上半部中对工作的调度
* @param-work : 要调度的工作
* @return : 0,成功;其他值,失败
*/
bool schedule_work(struct work_struct *work)
工作队列
的使用模板:
/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{
/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 work */
schedule_work(&testwork);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 work */
INIT_WORK(&testwork, testwork_func_t);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
总结:当调用了schedule_work后很快会执行工作函数,工作函数执行完毕后work对象便自动从工作队列中移除,所以就不需要用户开发中在驱动卸载函数中手动移除work对象了
在Linux内核中想使用某个中断需要申请,可以用request_irq
函数,其注册的中断服务函数属于中断处理的上半部,只要中断触发,就会立即执行。
/*
* @description : 申请中断向量
* @param-irq : 要申请中断的中断号
* @param-handler: 中断处理函数
* @param-flags : 中断标志
* @param-name : 中断名字
* @param-dev : 如果flags是IRQF_SHARED的话,dev用来区分不同的中断,一般情况下将dev设置为设备结构体,dev会传递给中断处理函数的第二个参数
* @return : 0,中断申请成功,其他负值表示中断申请失败,如果返回-EBUSY的话表示中断已经被申请了。
*/
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
中断标志有如下所示几个:
标志 | 描述 |
---|---|
IRQF_SHARED | 多个设备共享一个中断线,共享的所有中断都必须指定此标志。request_irq 函数的 dev 参数就是唯一区分他们的标志 |
IRQF_ONESHOT | 单词触发,中断执行一次就结束 |
IRQF_TRIGGER_NONE | 无触发 |
IRQF_TRIGGER_RISING | 上升沿触发 |
IRQF_TRIGGER_FALLING | 下降沿触发 |
IRQF_TRIGGER_HIGH | 高电平触发 |
IRQF_TRIGGER_LOW | 低电平触发 |
表中的标志可以通过 "|"
来实现各种组合。
/*
* @description : 释放相应的中断
* @param-irq : 要释放中断的中断号
* @param-dev : 如果flags是IRQF_SHARED的话,dev用来区分不同的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
* @return : 无
*/
void free_irq(unsigned int irq, void *dev);
/*
* @description : 中断处理函数
* @param-first : 中断处理函数要响应的中断号
* @param-second: 一个void指针,需要与request_irq函数的dev参数保持一致,用于区分共享中断的不同设备
* @return : 返回irq_handler_t,是一个枚举类型
*/
irqreturn_t(*irq_handler_t)(int, void*);
enable_irq()
和disable_irq()
用于使能和禁止中断,其中disable_irq()
要等当前正在执行的中断函数处理函数执行完才返回,因此使用者必须保证不会产生新的中断,并且确保所有已经开始执行的中断处理函数已经全部退出。
/*
* @description: 中断使能与禁止
* @param-irq : 要禁止的中断号
* @return : 无
*/
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
当要立即返回时,可以使用以下函数:
/*
* @description: 调用后立即返回,不会等待当前中断处理程序执行完毕
* @param-irq : 要禁止的中断号
* @return : 无
*/
void disable_irq_nosync(unsigned int irq)
local_irq_enable()
用于使能当前处理器中断系统,local_irq_disable()
用于禁止当前处理器中断系统。
/*
* @description: 打开和关闭全局中断
* @param : 无
* @return : 无
*/
local_irq_enable()
local_irq_disable()
对于关闭全局中断但是途中又要打开一会,执行完成后又要保持关闭状态的话,可以使用如下函数,执行完会将中断状态恢复到以前的状态。
local_irq_save
函数用于禁止中断,并且将中断状态保存在 flags
中。local_irq_restore
用于恢复中断,将中断到 flags
状态。
/*
* @description: 打开和关闭全局中断
* @param-flags: 保存中断状态的变量
* @return : 无
*/
local_irq_save(flags)
local_irq_restore(flags)
[1] 【正点原子】I.MX6U嵌入式Linux驱区动开发指南 第五十一章、第十七章
[4] Linux_中断下半部