第10章 中断与时钟之Linux中断编程

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;
  }

猜你喜欢

转载自blog.csdn.net/xiezhi123456/article/details/80414804