中断是一种处理异步事件的机制。中断源触发中断信号,内核检测到当前的中断事件需要处理,则暂停当前的运行进程,转而去处理中断事件。用于处理中断事件的函数称为中断处理程序,中断处理程序的入口地址一般存放在内存中的固定位置,这样方便内核查找各个中断所对应的中断处理程序。
中断处理程序有一个限制条件,中断处理程序不能进入睡眠状态,即它不能调用任何有可能会导致睡眠的函数。原因是:中断处理程序运行在中断上下文中,中断上下文与进程无关,它并不是一个调度实体,如果将其睡眠,将无法唤醒(根本原因:设计如此)。
为了快速响应中断,中断处理程序应该尽可能快速处理,否则将有可能影响到下一个中断的处理。但在某些情况下,中断处理程序需要处理一些费时的任务甚至可能会导致睡眠,因此内核开发者引入了中断上下部的概念:在中断上部处理紧急的任务,然后调用中断下部;而一些费时的或需要睡眠的部分交由中断下部进行。
在中断上半部一般处理一些硬件操作,在结束前调用中断的下半部,在上半部是不允许进入睡眠状态的,因此任何的可能导致睡眠的函数在上半部是不能被使用的。对于中断的下半部,在2.6版本内核常用的两种实现机制为:tasklet和workqueue。
tasklet是借助软中断的方式来实现的,tasklet的调度函数tasklet_schedule()或tasklet_hi_schedule(),通过触发软中断TASKLET_SOFTIRQ或HI_SOFTIRQ,从而唤起相应的软中断来处理下半部分。这样在处理下半部时,上半部可以处理下一次的中断事件。同样的在软中断中也是不能调用可能引起睡眠的函数,它可以延缓处理下半部,也可以与下一次上半部并发处理。
/*
** 按键驱动程序:通过中断(下部通过tasklet)实现的部分源码。
*/
#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <mach/irqs.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
static struct input_dev *button_dev;
static void bottom_interrupt(unsigned long t)
{
printk(KERN_INFO "This bottom part by tasklet!\n");
}
DECLARE_TASKLET(button_tasklet, bottom_interrupt, 0);
static irqreturn_t button_interrupt(int irq, void *dev)
{
printk(KERN_INFO "This top part!\n");
tasklet_schedule(&button_tasklet);
return IRQ_HANDLED;
}
static int __init button_init(void)
{
int ret;
ret = gpio_request(S5PV210_GPH0(2), "GPH0_2");
if (ret) {
printk(KERN_ERR "gpio request failed!\n");
return -EBUSY;
}
s3c_gpio_cfgpin(S5PV210_GPH0(2), S3C_GPIO_SFN(0x0f));
if (request_irq(IRQ_EINT2, button_interrupt,
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, "button-x210", NULL)) {
printk(KERN_ERR "irq request failed!\n");
ret = -EBUSY;
goto request_irq_error;
}
button_dev = input_allocate_device();
if (!button_dev) {
printk(KERN_ERR "allocate input device failed!\n");
ret = -ENOMEM;
goto allocate_error;
}
set_bit(EV_KEY, button_dev->evbit);
set_bit(KEY_LEFT, button_dev->keybit);
ret = input_register_device(button_dev);
if (ret) {
printk(KERN_ERR "register input device failed\n");
goto register_error;
}
return 0;
register_error:
input_free_device(button_dev);
allocate_error:
free_irq(IRQ_EINT2, NULL);
request_irq_error:
gpio_free(S5PV210_GPH0(2));
return ret;
}
static void __exit button_exit(void)
{
input_free_device(button_dev);
free_irq(IRQ_EINT2, NULL);
gpio_free(S5PV210_GPH0(2));
}
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alants");
workqueue则是通过将下半部交由一个内核线程去处理,这样中断的下半部就是在进程的上下文中执行。因此使用workqueue的方式实现的下半部是可以进入睡眠的。workqueue的调度是通过接口schedule_work()来操作的,schedule_work()所进行的实际操作就是将当前的下半部的相关结构体work_struct添加到当前的work_list中,等待内核线程调用。
/*
** 按键驱动程序:通过中断(下部通过workqueue)实现的部分源码。
*/
#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <mach/irqs.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
static struct input_dev *button_dev;
static void bottom_interrupt(struct work_struct *work)
{
printk(KERN_INFO "This bottom part by workqueue!\n");
}
DECLARE_WORK(button_work, bottom_interrupt);
static irqreturn_t button_interrupt(int irq, void *dev)
{
printk(KERN_INFO "This top part!\n");
schedule_work(&button_work);
return IRQ_HANDLED;
}
static int __init button_init(void)
{
int ret;
ret = gpio_request(S5PV210_GPH0(2), "GPH0_2");
if (ret) {
printk(KERN_ERR "gpio request failed!\n");
return -EBUSY;
}
s3c_gpio_cfgpin(S5PV210_GPH0(2), S3C_GPIO_SFN(0x0f));
if (request_irq(IRQ_EINT2, button_interrupt,
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, "button-x210", NULL)) {
printk(KERN_ERR "irq request failed!\n");
ret = -EBUSY;
goto irq_request_error;
}
button_dev = input_allocate_device();
if (!button_dev) {
printk(KERN_ERR "allocate input device failed!\n");
ret = -ENOMEM;
goto allocate_error;
}
set_bit(EV_KEY, button_dev->evbit);
set_bit(KEY_LEFT, button_dev->keybit);
ret = input_register_device(button_dev);
if (ret) {
printk(KERN_ERR "register input device failed\n");
goto register_error;
}
return 0;
register_error:
input_free_device(button_dev);
allocate_error:
free_irq(IRQ_EINT2, NULL);
irq_request_error:
gpio_free(S5PV210_GPH0(2));
return ret;
}
static void __exit button_exit(void)
{
input_unregister_device(button_dev);
input_free_device(button_dev);
free_irq(IRQ_EINT2, NULL);
gpio_free(S5PV210_GPH0(2));
}
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alants");
从上述两种按键驱动中断实现方式来看,workqueue和tasklet的使用似乎没有很大的区别,但是在实际使用中要根据中断下半部分具体的处理内容(例如,是否会进入睡眠)来考虑下半部采用哪种机制。