10.3 Linux中断编程
10.3.1 申请和释放中断
在Linux设备驱动中,使用中断的设备需要申请和释放对应的中断,并分别使用内核提供的request_irq()和free_irq()函数。
1.申请irq
<linux/interrupt.h >
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
irq是要申请的硬件中断号。
handler是向系统登记的中断处理函数(上半部),是一个回调函数,中断发生时,系统调用这个函数,dev参数将被传递给它。
flags是中断处理的属性,可以指定中断的触发方式以及处理方式。在触发方式方面,IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW等。在处理方式方面,若设置了IRQF_SHARED,则表示多个设备共享中断,dev是要传递给中断服务程序的私有数据,一般设置为这个设备的设备结构体或者NULL。
request_irq()返回0表示成功,返回-EINVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。
static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
{
return devm_request_threaded_irq(dev, irq, handler, NULL, irqflags,
devname, dev_id);
}
此函数申请的是内核“managed”的资源,一般不需要在出错处理和remove()接口里再显式的释放。类似于Java的垃圾回收机制。
上半部handler的类型irq_handler_t定义为:
typedef irqreturn_t (*irq_handler_t)(int, void *); <=>typedef int (*irq_handler_t)(int, void *);
typedef int irqreturn_t;
2.释放irq
与request_irq()相对应的函数为free_irq(),free_irq()的原型为:
void free_irq(unsigned int irq,void *dev_id);
free_irq()中参数的定义与request_irq()相同。
10.3.2 使能和屏蔽中断
下列函数用于屏蔽一个中断源:
void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
disable_irq_nosync()立即返回,disable_irq()等待目前的中断处理完成。由于disable_irq()会等待指定的中断被处理完,因此如果在n号中断的上半部调用disable_irq(n),会引起系统的死锁,这种情况下,只能调用disable_irq_nosync(n)。
下列两个函数(或宏,具体实现依赖于CPU的体系结构)将屏蔽本CPU内的所有中断:
#define local_irq_save(flags) ...
void local_irq_disable(void);
local_irq_save会将目前的中断状态保留在flags中(注意flags为unsigned long类型,被直接传递,而不是通过指针),local_irq_disable直接禁止中断而不保存状态。
与上述两个禁止中断对应的恢复中断的函数(或宏)是:
#define local_irq_restore(flags) ...
void local_irq_enable(void);
备注:以local_开头的方法的作用范围是本CPU内。
10.3.3 下半部机制
Linux实现下半部的机制主要有tasklet、工作队列、软中断和线程化irq。
1.tasklet(小任务)
tasklet的执行上下文是软中断,tasklet的执行时机通常是上半部返回的时候。只需要定义tasklet及其处理函数,并将两者关联则可。
<linux/interrupt.h >
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
例如:
void my_tasklet_func(unsigned long data); /*定义一个处理函数*/
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data)实现定义名称为my_tasklet的结构体为struct tasklet_struct 的变量,并将其与my_tasklet_func()这个函数绑定,传入这个函数的参数为data。
在需要调度tasklet的时候调用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行:
tasklet_schedule(&my_tasklet);
使用tasklet作为下半部处理中断的设备驱动程序模板如代码清单10.2所示(仅包含与中断相关的部分)。
/* 定义tasklet和下半部函数并将它们关联 */
void xxx_do_tasklet(unsigned long data);
// 定义tasklet
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 78);/* 中断处理下半部 */
void xxx_do_tasklet(unsigned long data)
{
...
}
/* 中断处理上半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
tasklet_schedule(&xxx_tasklet); // xxx_do_tasklet在适当的时候被执行
...
}
/* 设备驱动模块加载函数 */
int __init xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
...
return IRQ_HANDLED;
}
/* 设备驱动模块卸载函数 */
void __exit xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
2.工作队列
<linux/workqueue.h>
工作队列的执行上下文是内核线程,可以调度和睡眠。下面的代码用于定义一个工作队列和一个下半部执行函数:
struct work_struct wq; /* 定义一个工作队列 */
void wq_func(struct work_struct *work); /* 定义一个处理函数 */
通过INIT_WORK()初始化这个工作队列并将工作队列与处理函数绑定:
INIT_WORK(&wq, wq_func);
/* 初始化工作队列并将其与处理函数绑定 */
#define INIT_WORK(_work, _func) \
do { \
__INIT_WORK((_work), (_func), 0); \
} while (0)
用于调度工作队列执行的函数为schedule_work(),如:
schedule_work(&wq); /* 调度工作队列执行 */
/**
* schedule_work - put work task in global workqueue
* @work: job to be done
*
* Returns %false if @work was already on the kernel-global workqueue and
* %true otherwise.
*
* This puts a job in the kernel-global workqueue if it was not already
* queued and leaves it in the same position on the kernel-global
* workqueue otherwise.
*/
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
使用工作队列处理中断下半部的设备驱动程序模板如代码清单10.3所示(仅包含与中断相关的部分)。
代码清单10.3 工作队列使用模板
#include <linux/workqueue.h>
/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);
/* 中断处理下半部 */
void xxx_do_work(struct work_struct *work)
{
...
}
/*中断处理上半部*/
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
schedule_work(&xxx_wq);// 调度工作队列
...
return IRQ_HANDLED;
}
/* 设备驱动模块加载函数 */
int xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
...
/* 初始化工作队列 */
INIT_WORK(&xxx_wq, xxx_do_work);
...
}
/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
工作队列早期的实现是在每个CPU核上创建一个worker内核线程,所有在这个核上调度的工作都在该worker线程中执行,其并发性差强人意。在Linux 2.6.36以后,转而实现了“Concurrency-managed workqueues”,并发管理的工作队列cmwq会自动维护工作队列的线程池以提高并发性,同时保持了API的向后兼容。
3.软中断
软中断(Softirq)也是一种传统的下半部处理机制,软中断的执行时机通常是上半部返回的时候,tasklet(小任务)是基于软中断实现的,因此也运行于软中断上下文。
<linux/interrupt.h>
struct softirq_action
{
void (*action)(struct softirq_action *); //软中断处理函数指针
};
在Linux内核中,用softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的参数。
注册软中断对应的处理函数:void open_softirq(int nr, void (*action)(struct softirq_action *));
触发一个软中断对应的处理函数:void raise_softirq(unsigned int nr);
软中断和tasklet(小任务)运行于软中断上下文,属于原子上下文的一种,而工作队列则运行于进程上下文。因此,在软中断和tasklet(小任务)处理函数中不允许睡眠,而在工作队列处理函数中允许睡眠。
local_bh_disable()和local_bh_enable()是内核中用于禁止和使能软中断及tasklet下半部机制的函数。
一般来说,驱动的编写者不会也不宜直接使用softirq。
硬中断、软中断和信号的区别:
硬中断是外部设备对CPU的中断,软中断是中断下半部的一种处理机制,信号则是由内核(或其他进程)对某个进程的中断。
在涉及系统调用的场合,常说通过软中断(例如ARM为swi)陷入内核,此时软中断的概念是指由软件指令引发的中断。
特别说明的是,软中断以及基于软中断的tasklet如果在某段时间内大量出现的话,内核会把后续软中断放入ksoftirqd内核线程中执行。中断优先级高于软中断,软中断又高于任何一个线程。软中断适度线程化,可以缓解高负载情况下系统的响应。
4.xxx_threaded_irq
在内核中,除通过request_irq()、devm_request_irq()申请中断外,还可以通过request_threaded_irq()和devm_request_threaded_irq()申请中断。
#include <linux/interrupt.h>
int
request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev);
int
devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,void *dev_id);
这两个函数比request_irq()、devm_request_irq()多了一个参数thread_fn。用这两个API申请中断的时候,内核会为相应的中断号分配一个对应的内核线程。注意这个线程只针对这个中断号,如果其他中断也通过request_threaded_irq()申请,自然会得到新的内核线程。
参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数则执行于内核线程。如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。
request_threaded_irq()和devm_request_threaded_irq()支持在irqflags中设置IRQF_ONESHOT标记,内核会自动在中断上下文中屏蔽对应的中断号,而在内核调度thread_fn执行后,重新使能该中断号。
对于无法在上半部清除中断的情况,IRQF_ONESHOT特别有用,避免了中断服务程序一退出,中断就洪泛的情况。
handler参数可以设置为NULL,这种情况下,内核会用默认的irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT标记。
kernel/irq/manage.c
/*
* Default primary interrupt handler for threaded interrupts. Is
* assigned as primary handler when request_threaded_irq is called
* with handler == NULL. Useful for oneshot interrupts.
*/
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}