中断处理介绍
在驱动程序中我们不乏使用中断机制,中断属于异常的一种,一般用于打断当前线程,进而去执行比较紧急的事件,当紧急事件处理完成再回来继续执行前面线程工作的一种机制。中断的应用在驱动程序中比较常见,比如按键驱动、触摸屏驱动都用到中断机制。中断还有底半部机制,用来处理耗时较长的任务,可以提高驱动执行效率。中断的底半部有多种实现方式比如tasklet机制与工作队列机制(work queue)以及中断线程化。linux内核中不支持中断嵌套操作。
中断处理API
request_irq
request_irq:该函数用于申请中断
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)
函数参数和返回值含义如下:
irq:要申请中断的中断号。
handler:中断处理函数,当中断发生以后就会执行此中断处理函数。使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
flags:中断标志,中断标志如下,这些标志可以通过“|”来实现多种组合。
标志 | 描述 |
IRQF_SHARED | 多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话,request_irq函数的dev参数,就是唯一区分它们的标志。 |
IRQF_ONESHOT | 单次中断,中断执行一次就结束。 |
IRQF_TRIGGER_NONE | 无触发。 |
IRQF_TRIGGER_RISING | 上升沿触发。 |
IRQF_TRIGGER_FALLING | 下降沿触发。 |
IRQF_TRIGGER_HIGH | 高电平触发。 |
IRQF_TRIGGER_LOW | 低电平触发。 |
name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经被申请了。
free_irq
在中断使用完成request_irq申请的中断资源后,需要使用free_irq释放中断资源。
void free_irq(unsigned int irq, void *dev)
函数参数和返回值含义如下:
irq:要释放的中断。
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。
中断使能与禁止函数
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。
void disable_irq_nosync(unsigned int irq)
disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。该函数一般在中断处理函数中使用。由于它不会阻塞当前函数的执行,因此适合在中断处理函数中使用,以防止中断嵌套。如果在中断处理函数中使用disable_irq
,则会导致死锁,因为disable_irq
会阻塞当前函数,导致无法返回,从而无法重新使能中断。
local_irq_enable()
local_irq_disable()
local_irq_disable函数可以用来禁止本地中断。没有加local可以用来禁止全局中断。
local_irq_save(flags)
local_irq_restore(flags)
local_irq_save函数在Linux内核中用于保存当前CPU的中断状态,并关闭本地中断,以确保在某段代码执行期间不会受到中断的干扰。在需要保护临界区资源时使用,并且配对使用。
中断底半部
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
上半部,下半部使用方法:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部。
④、除了上述三点以外的其他任务,优先考虑放到下半部。
tasklet机制
tasklet主要API
taskled_init 函数原型如下:
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
函数参数和返回值含义如下:
t:要初始化的 tasklet
func:tasklet 的处理函数。
data:要传递给 func 函数的参数
返回值:没有返回值。
也可以使用宏DECLARE_TASKLET来一次性完成tasklet的定义和初始化 :
DECLARE_TASKLET(name, func, data)
其中,name是tasklet的名称,func是tasklet绑定的处理函数,data是传递给处理函数的参数。这个宏帮助在编译时静态初始化tasklet,使其可使用。
在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行,tasklet_schedule 函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
函数参数和返回值含义如下:
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
返回值:没有返回值。
销毁tasklet函数tasklet_kill,函数原型如下:
void tasklet_kill(struct tasklet_struct *t)
它一般在设备关闭或者驱动卸载的时候被执行,如果当前task正在执行,它会等待执行完后销毁。如果tasklet有调用它本身,那就要注意在执行tasklet_kill时必须先阻止它调用自己。
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);......
}
/*驱动出口函数*/
static void __exit xxxx_exit(void)
{....../* 释放中断资源 */free_irq(xxx_irq, &xxx_dev);/*销毁tasklet*/
tasklet_kill((&testtasklet);......
}
Tasklet适用于那些不能在中断服务程序中直接处理,但又需要在中断后尽快执行的任务。例如,处理硬件设备的状态变化、发送网络数据包等。使用Tasklet时需要注意以下几点:
Tasklet绑定的函数中不能调用可能导致休眠的函数,否则可能引起内核异常;
Tasklet绑定的函数在同一时间只能在一个CPU上运行,避免了并发冲突。
工作队列机制
工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
工作队列主要API
工作队列的使用首先是定义一个 work_struct 结构体变量,然后使用 INIT_WORK 宏来初始化工作,INIT_WORK 宏定义如下:
#define INIT_WORK(_work, _func)
_work 表示要初始化的工作,_func 是工作对应的处理函数。也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)
n 表示定义的工作(work_struct),f 表示工作对应的处理函数。和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原型如下所示:
bool schedule_work(struct work_struct *work)
schedule_work函数是异步的,意味着它只是将工作项添加到队列,然后立即返回,并不会等待工作项的执行完成。该函数参数和返回值含义如下:
work:要调度的工作。
返回值:0 成功,其他值 失败。
void destroy_workqueue(struct workqueue_struct *wq);
该函数用于安全地销毁一个工作队列(workqueue),确保其中的所有工作项都被执行完毕后再释放资源。该函数首先会同步排空队列中的所有工作项,然后异步释放工作队列所占用的资源。
工作队列参考使用示例
/* 定义工作(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);......
}
/* 驱动出口函数 */
static void __exit xxxx_exit(void)
{....../* 销毁工作队列 */destroy_workqueue(&testwork);/* 释放中断资源 */free_irq(xxx_irq, &xxx_dev);......
}
使用工作队列的一个主要优点是避免在中断上下文中执行长时间运行的操作。中断上下文应该尽可能短暂,以免影响系统的响应性和稳定性。使用工作队列可以将长时间运行的操作延迟到稍后的时间执行,从而避免在中断上下文中执行。 另一个优点是工作队列可以在多个CPU上并行执行任务。这可以提高系统的吞吐量和响应性总之,工作队列是Linux内核中一种非常有用的异步处理机制。它可以帮助内核模块和驱动程序避免在中断上下文中执行长时间运行的操作,并提高系统的吞吐量和响应性。
中断线程化
工作队列后就可以把耗时的任务放入工作队列中进行处理了,但是当有多个work时前面没有执行完的work就会影响到后面work的执行,这样的效率还是不高,那么这个时候就需要使用中断的线程化处理了。使用这个机制需要使用到下面这个函数:
int request_threaded_irq(unsigned int irq, irq_handler_t handler,irq_handler_t thread_fn, unsigned long irqflags,const char *devname, void *dev_id);
这个函数具有以下参数:
irq:要请求的中断号。
handler:中断处理函数,即中断发生时要调用的函数。它通常是一个标准的中断处理函数,接收中断相关的参数。
thread_fn:中断线程函数,当中断处理函数返回IRQ_WAKE_THREAD时会调用。它通常用于执行耗时操作,以便避免中断处理函数的执行时间过长。
irqflags:中断标志,用于指定中断的行为,例如共享中断、触发类型等。
devname:设备名称,用于标识请求中断的设备。
dev_id:设备ID,用于传递设备相关的数据给中断处理函数。
中断底半部机制对比
机制 | 执行上下文 | 可抢占性 | 适用场景 |
软中断 | 中断上下文 | 不可抢占 | 高频、低延迟任务(如网络) |
tasklet | 中断上下文 | 不可抢占 | 串行化任务(如设备驱动) |
工作队列 | 进程上下文 | 可抢占 | 复杂任务(需睡眠或阻塞) |
中断线程化 | 进程上下文 | 可抢占 | 复杂任务(需睡眠或阻塞) |