一、中断
中断分为上半部和底半部。上半部也就是硬中断,软中断只是底半部的一种实现机制
上半部主要处理有严格时限的工作,比如读取寄存的中断状态,清除中断标志,将底半部处理程序挂到底半部的执行队列中去
底半部执行大部分耗时的工作,并且可以被其他中断打断
1、硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。每个设备或设备集都有它自己的IRQ(中断请求)。基于IRQ,CPU可以将相应的请求分发到对应的硬件驱动上(注:硬件驱动通常是内核中的一个子程序,而不是一个独立的进程)
2、软中断是一组静态定义的下半部分接口,可以在所有的处理器上同时执行,即使两个类型相同也可以。但是一个软中断不会抢占另外的一个软中断,唯一可以抢占软中断的硬中断。可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保其数据结构。
二、中断函数
1.开关中断
/************关闭指定中断*************************/
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq) //要等到当前正在执行的中断处理函数执行完才返回
void disable_irq_nosync(unsigned int irq) //函数调用以后立即返回
/**************硬中断****************************/
local_irq_disable():关闭中断
local_irq_enable():开启中断
local_irq_save(flags):关闭中断并保存中断标志位
local_irq_restore(flags):开启中断并清除中断标志位
/*************底半部开关************************/
local_bh_disable();
local_bh_enable();
/*************判断中断状态*********************/
#define in_interrupt() (irq_count()) // 是否处于中断状态(硬中断或软中断)
#define in_irq() (hardirq_count()) // 是否处于硬中断
#define in_softirq() (softirq_count()) // 是否处于软中断
disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。
当任务A调用local_irq_disable企图关闭中断1s,然后再开启中断,但在中断中出现了另一个优先级更高的任务B也调用了local_irq_disable关闭中断,然后100ms后执行local_irq_enable开启中断,导致任务A的中断没有持续1s就被打开了,这显然是有问题的,所以需要用到后两个中断函数,当B任务关闭中断的时候保存中断标志位,开启中断的时候清除中断标志位,还原之前的中断状态,才能使任务A继续处于中断状态中正确执行。
2、申请中断
int request_irq(unsigned int irq, irq_handle_t handle, unsigned int flags,
const char *name, void* dev_id);
int devm_request_irq(struct device *dev, unsigned int irq, irq_handle_t handle,
unsigned int flags, const char *name, void* dev_id);
其中flags代表中断标志,可以取值如下:
上升沿触发:IRQF_TRIGGER_RISING
下降沿触发:IRQF_TRIGGER_FALLING
高电平触发:IRQF_TRIGGER_HIGH
低电平触发:IRQF_TRIGGER_LOW
共享中断:IRQF_SHARED
…
dev_id代表要传给中断处理程序的私有数据,一般是这个设备的结构体或者NULL
devm_request_irq和request_irq区别在于前者是申请的内核“managed”资源,不需要自己手动释放,会自动回收资源,而后者需要手动调用free_irq来释放中断资源
3、释放中断
void free_irq(unsigned int irq, void *dev_id);
二、底半部实现机制:软中断、tasklet、工作队列、线程化irq
1、软中断一般不使用,由于必须要设计为可重入函数,导致复杂度高,并且无法睡眠、不可阻塞,无法重新调度。
软中断应该保留给系统中对时间要求最严格的下半部使用。目前只有两个子系统直接使用软中断:网络和SCSI。此外,内核定时器和tasklet也是基于软中断实现的。由于同一个软中断可以在不同处理器上同时执行,所以软中断要求对共享资源的处理要非常严格,锁保护的要求很高。
2、tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择。也就是说tasklet是软中断的一种特殊用法,即延迟情况下的串行执行。
tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。这两者唯一的区别在于,HI_SOFTIRQ类型的软中断先于TASKLET_SOFTIRQ类型的软中断执行。
tasklet_struct my_tasklet;
void my_tasklet_func(unsigned long);
//将my_tasklet和my_tasklet_func绑定关联
DECLARE_TASKLET(my_tasklet, may_tasklet_func, data);
void my_tasklet_func(unsigned long)
{
...
}
irqreturn_t my_tasklet_demo_interrupt(int irq, void *dev_id)
{
...
//执行tasklet_shedule调度,会在适当的时候执行my_tasklet_func
tasklet_shedule(&my_tasklet);
...
return IRQ_HANDLED;
}
int __init my_tasklet_demo_init(void)
{
...
int result = request_irq(irq_num, my_tasklet_demo_interrupt, IRQF_SHARED, "my_tasklet_demo", dev_id);
...
return IRQ_HANDLED;
}
void __exit my_tasklet_demo_exit(void)
{
...
free_irq(irq_num, my_tasklet_demo_interrupt);
...
}
3、工作队列可以把工作推后,交由一个内核线程去执行。内核线程只在内核空间运行,没有自己的用户空间,它和普通进程一样可以被调度,也可以被抢占。该工作队列总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势,最重要的就是工作队列允许重新调度甚至是睡眠。
理论上可以用创建内核线程的方式来代替工作队列,但是由于随便创建内核线程会带来其他问题,所以实际上并不建议直接创建内核线程,而应使用工作队列的形式。
struct work_struct my_workqueue;
void my_wq_func(struct work_struct* work);
/*中断底半部工作队列的处理*/
void my_wq_func(struct work_struct* work)
{
...
}
irqreturn_t my_wq_interrupt(int irq, void *dev_id)
{
...
schedule_work(&my_workqueue);
...
return IRQ_HANDLED;
}
int __init my_wq_init(void)
{
...
int result = request_irq(irq_num, my_wq_interrupt, IRQF_TRIGGER_RISING, "my_wq", dev_id);
//初始化工作队列,并将my_workqueue和my_wq_func处理函数关联
INIT_WORK(&my_workqueue, my_wq_func);
...
return 0;
}
void my_wq_exit(void)
{
...
free_irq(irq_num, my_wq_interrupt);
...
}
4、threaded_irq:内核提供request_threaded_irq和devm_request_threaded_irq为中断分配内核线程,原型如下:
int request_threaded_irq(unsigned int irq,
irq_handle_t* handle,
irq_handle_t* thread_func,
unsigned long flags, const char* name, void* dev_id);
int devm_request_threaded_irq(struct device* dev, unsigned int irq,
irq_handle_t* handle,
irq_handle_t* thread_func,
unsigned long flags, const char* name, void* dev_id);
其中handle对应的函数执行于中断上下文,thread_func对应的函数执行于内核线程,如果handle的返回值是IRQ_WAKE_THREAD,内核会自动调度对应的线程执行thread_func函数
若flags中设置IRQF_ONESHOT标志,内核会自动在中断上下文中屏蔽该中断号,防止上半部无法清除中断,导致上半部一退出,瞬间大量中断同时发生导致异常。
三、中断里不能睡眠
睡眠会导致进程切换,中断上下文没有进程的概念,中断有自己的内核栈,进程使用的task_struct结构体信息,schedule调度就是去访问这些task_struct,抢占一个进程运行另一个进程,所以中断里无法进行系统调度,如果在中断里睡眠,进程将无法被调度,从而引发内核崩溃
假设中断里可以睡眠,也就是可以进行进程调度,如果此时内核正执行到临界区,处于加锁状态(一般是自旋锁),切换另一个进程执行到此处无法加锁,就会等待下去,造成死锁。
No pains, no gains