一、中断

  中断分为上半部和底半部。上半部也就是硬中断,软中断只是底半部的一种实现机制
  上半部主要处理有严格时限的工作,比如读取寄存的中断状态,清除中断标志,将底半部处理程序挂到底半部的执行队列中去
底半部执行大部分耗时的工作,并且可以被其他中断打断
  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、工作队列可以把工作推后,交由一个内核线程去执行。内核线程只在内核空间运行,没有自己的用户空间,它和普通进程一样可以被调度,也可以被抢占。该工作队列总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势,最重要的就是工作队列允许重新调度甚至是睡眠。
  理论上可以用创建内核线程的方式来代替工作队列,但是由于随便创建内核线程会带来其他问题,所以实际上并不建议直接创建内核线程,而应使用工作队列的形式。
  内核中可以创建workqueue队列,将work_struct工作加入workqueue,也可以不加,会默认挂载到内核的工作队列,对应的函数有create_singlethread_workqueue(const char* name)和create_workqueue(const char* name) ,前者只是在当前CPU创建一个work_thread线程去执行工作队列里的work_struct,而后者是在每个CPU上都创建一个work_thread,使用queue_work(struct workqueue_struct *wq, struct work_struct *work)唤醒队列执行,destroy_workqueue(struct workqueue_struct *wq)释放workqueue队列

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

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐