中断简介
根据中断的来源,中断可分为内部中断和外部中断。内部中断的中断源来自 CPU 的内部(软件中断、溢出、除法错误等);外部中断的中断源来自 CPU 外部,由外设提出请求。
根据中断是否可以屏蔽,中断分为可屏蔽中断和不可屏蔽中断(NMI,Not Masked Interrupt)。
根据中断入口跳转方法的不同,中断可分为向量中断和非向量中断。向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。
Linux 中断处理程序架构
Linux 将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom Half)。
顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。
小提示:在 Linux 中查看 /proc/interrupts 文件可以获得系统中中断的统计信息。
Linux 申请和释放中断
1. 申请irq
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
irq:要申请的硬件中断号
handler:向系统登记的中断处理函数(顶半部),是一个回调函数。中断发生时,系统调用这个函数,dev参数将被传递给它。
irqflags:中断处理属性,可以指定中断的触发方式及处理方式。
name:中断名字
dev:要传递给中断服务程序的私有数据,一般设置为这个设备的设备结构体或者 NULL。
返回值:0 表示成功;-EINVAL 表示中断号无效或处理函数指针为 NULL,-EBUSY 表示中断已经被占用且不能共享。
int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
此函数与 request_irq() 的区别是 devm_ 开头的 API 申请的是内核 “managed”的资源,一般不需要在出错处理和 remove() 接口里再显式的释放。
2. 释放 irq
与 request_irq() 相对应的函数为 free_irq(),free_irq() 的原型为:
void free_irq(unsigned int irq, void *dev_id)。
使能和屏蔽中断
1. 使能和屏蔽一个中断源
屏蔽一个中断源的函数如下:
void disable_irq(int irq);
void disable_irq_nosync(int irq);
disable_irq_nosync() 和 disable_irq() 的区别在于前者立即返回,而后者等待目前的中断处理完成。
由于 disable_irq() 会等待指定的中断被处理完,因此如果在 n 号中断的顶半部调用 disable_irq(n),会引起系统的死锁,这种情况下,只能调用 disable_irq_nosync(n)。
使能一个中断源的函数如下:
void enable_irq(int irq);
2. 使能和屏蔽本 CPU 内的所有中断
屏蔽本 CPU 内的所有中断的函数(或宏)如下:
#define local_irq_save(flags) ...
void local_irq_disable(void);
前者会将目前的中断状态保留在 flags 中(注意 flags 为 unsigned long 类型,被直接传递,而不是通过指针),后者直接禁止中断而不是保存状态。
与上述两个禁止中断对应的恢复中断的函数(或宏)为:
#define local_irq_restore(flags) ...
void local_irq_enable(void);
注:以 local_ 开头的方法的作用范围是本 CPU 内。
底半部机制
Linux 实现底半部的机制主要有 tasklet、工作队列、软中断和线程化 irq。
1. tasklet
tasklet 的使用较简单,它的执行上下文是软中断,执行时机通常是顶半部返回的时候。
使用步骤如下:
第1步:定义 tasklet 及其处理函数,并将两者关联
void xxx_do_tasklet(unsigned long data);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, data);
第2步:调用 tasklet
使用函数 tasklet_schedule(&xxx_tasklet) 调用即可。
使用模板如下:
/* 定义 tasklet 和底半部函数并将它们关联 */
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx tasklet, xxx_do_tasklet, 0);
/* 中断处理底半部 */
void xxx_do_tasklet(unsigned long)
{
...
}
/* 中断处理顶半部 */
irqreturn xxx_interrupt(int irq, void *dev_id)
{
...
tasklet_schedule(&xxx_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. 工作队列
工作队列的使用方法和 tasklet 非常类似,但是工作队列的执行上下文是内核线程,因此可以调度和睡眠。
使用步骤如下:
第1步:定义工作队列和处理函数
struct work_struct my_wq;
void my_wq_func(struct work_struct *work);
第2步:初始化工作并将工作队列与处理函数绑定
INIT_WORK(&my_wq, my_wq_func);
第3步:调度工作队列
与 tasklet 调度类似,直接调用函数 schecule_work(&my_wq); 即可。
使用模板如下:
/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);
/* 中断处理底半部 */
void xxx_do_work(unsigned long)
{
...
}
/* 中断处理顶半部 */
irqreturn xxx_interrupt(int irq, void *dev_id)
{
...
schedule_work(&xxx_wq);
...
}
/* 设备驱动模板加载函数 */
int __init xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
...
/* 初始化工作队列 */
INIT_WORK(&xxx_wq, &xxx_do_work);
}
/* 设备驱动模板卸载函数 */
void __exit xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
3. 软中断
软中断(Softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet是基于软中断实现的,因此也运行于软中断上下文。
在 Linux 内核中,用 softirq_action 结构体表征了一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的参数。使用 open_softirq() 函数可以注册软中断对应的处理函数,而 raise_softirq() 函数可以出发一个软中断。
4. threaded_irq
在内核中,除了可以通过 request_irq()、devm_request_irq() 申请中断以外,还可以通过 request_threaded_irq() 和 devm_request_threaded_irq() 申请,函数原型如下:
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 申请中断的时候,内核会为相应的中断分配一个对应的内核线程。
参数 handler 对应的函数执行于中断上下文,参数 thread_fn 对应的函数执行于内核线程。如果 handler 结束的时候,返回值是 IRQ_WAKE_THREAD,内核会调度对应线程执行 thread_fn 对应的函数。
中断共享
多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在,Linux 支持这种中断共享。中断共享的使用方法如下:
(1)共享中断的多个设备在申请中断时,都应该使用 IRQF_SHARED 标志,而且一个设备以 IRQF_SHARED 申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断的所有设备也都以 IRQF_SHARED 标志申请该中断。
(2)尽管内核模块可访问的全部地址都可以作为 request_irq(..., void *dev_id) 的最后一个参数 dev_id,当时设备结构体指针显然是可传入的最佳参数。
(3)在中断到来时,会遍历执行共享此中断的所有中断处理程序,知道某一个函数返回 IRQ_HANDLED。在中断处理程序顶半部中,应根据硬件寄存器中的信息比照传入的 dev_id 参数迅速判断是否为本设备的中断,若不是,应迅速返回 IRQF_NONE。
使用模板如下:
/* 中断处理顶半部 */
irqreturn xxx_interrupt(int irq, void *dev_id)
{
...
int status = read_int_status(); /* 获知中断源 */
if (!is_myint(dev_id, status)) /* 判断是否为本设备中断 */
return IRQ_NONE; /* 不是本设备中断,立即返回 */
...
/* 是本设备中断,进行处理 */
...
return IRQ_HANDLED; /* 返回 IRQ_HANDLED 表明中断已被处理 */
}
/* 设备驱动模板加载函数 */
int __init xxx_init(void)
{
...
/* 申请共享中断 */
result = request_irq(xxx_irq, xxx_interrupt, IRQF_SHARED, "xxx", NULL);
...
return IRQ_HANDLED;
}
/* 设备驱动模板卸载函数 */
void __exit xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}