中断处理程序是内核必不可少的一部分,但由于一些局限,它只能完成整个中断处理流程的前半部分.
这些局限包括:
*中断处理程序以异步方式执行,可能会打断其他重要代码(甚至其他中断程序的执行)。因此,中断处理程序应该执行的越快越好
*中断处理程序会引起其他中断的屏蔽(同级别的或所有中断),这个屏蔽时间必须短
*中断处理程序需要对硬件操作,通常有很高的时间要求,需要尽快响应
*中断处理程序不在进程上下文中运行,所以不能阻塞,也就不能调用那些可能引起阻塞的函数
因此,中断处理程序必须快速,异步,简单。
那些中断处理流程中对时间要求不严格的部分,应该推后到中断被激活后运行。
这样,整个中断处理流程就被分成两部分,或叫两半。第一个部分是中断处理程序(上半部),
(2)下半部的特点
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。
理想情况下,我们希望把尽可能多的工作都交给下半部去做。
这样中断处理程序就能尽可能短,从而尽快响应硬件的中断并解除对其他中断的屏蔽。
如何划分上下半部:
*如果一个任务对时间非常敏感,放在中断处理程序中执行
*如果一个任务和硬件相关,放在中断处理程序中执行
*如果一个任务要保证不被其他中断(特别是相同的中断打断),放在中断处理程序中执行
*其他所有任务,考虑放在下半部执行
(3)为什么要用下半部:
中断处理程序运行时当前的中断会被屏蔽,如果处理程序是IRQF_DISABLED类型的,还会禁止所有本地中断。
如果这个中断处理程序运行10秒钟,那么系统看上去就死机了10秒。所以,缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。
下半部仅仅是强调不是马上执行,通常下半部在中断处理程序一返回就会马上运行,下半部的关键在于当他们运行的时候,允许响应所有的中断。
实现中断处理程序的方法只有一种,而下半部有多种实现方法。
在linux的发展过程中曾经出现过多种下半部的实现,极容易引起混淆。主要介绍2.6支持的下半部实现方法。
(4)2.6中支持的下半部:
a.软中断(softirq)
不能睡眠
b.tasklet
基于软中断实现,不能睡眠
c.工作队列(work queue)
基于内核线程,可以睡眠,可以调度,但不能访问用户空间
(5)tasklet的实现
tasklet是通过软中断实现的,所以它们本身也是软中断。 。
a.其结构体在<linux/interrupt.h>中定义:
struct tasklet_struct{
struct tasklet_struct *next; /*链表中的下一个tasklet*/
unsigned long state; /*tasklet的状态 */
atomic_t count; /* 引用计数器 */
void (*func) (unsigned long); /* tasklet处理函数 */
unsigned long data; /* 给tasklet处理函数的参数 */
}
state: 0或TASKLET_STATE_SCHED
count: 不为0则tasklet 被禁止, 为0时tasklet 激活
b.调度tasklet
已调度(schdule)的tasklet(等同于被触发raise的软中断)存放在两个单处理器数据结构中:
tasklet_vec和tasklet_hi_vec
这两个是由tasklet构成的链表。
通过tasklet_schedule()和tasklet_hi_schedule()进行调度
如何创建我们自己的tasklet
a.声明
静态声明(见<linux/interrupt.h>)
DECLARE_TASKLET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data)
动态声明
struct tasklet_struct *t = kmalloc(sizeof(tasklet_struct), GFP_KENEL);
tasklet_init(t, tasklet_handler, dev);
b.编写自己的tasklet处理程序
void tasklet_handler(unsigned long data){ ; }
tasklet不能睡眠,且运行在开中断时。要注意对共享数据的保护。
c.调度自己的tasklet
通常在中断处理函数结束前进行调度:
tasklet_schedule(&my_tasklet);
如果一个tasklet在调度后但还没有运行前又被调度了,第二次调度作废,tasklet只运行一次。
为了更好地利用cache,一个tasklet总在调度它的cpu上运行。
d.其他的tasklet处理函数
tasklet_disable()
tasklet_disable_nosync()
tasklet_enable()
tasklet_kill()
tasklet例子:
/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <mach/gpio.h>
#include <mach/regs-gpio.h>
void do_task(unsigned long data)
{
printk("[KERN] %s %d\n", __func__, __LINE__);
printk("[KERN] in_interupt() = %lu, data = %lu\n", in_interrupt(), data);
printk("[KERN] %s %d\n", __func__, __LINE__);
}
DECLARE_TASKLET(mytask, do_task, 10);
static irqreturn_t key_handler(int irqno, void *dev_id)
{
printk("[KERN] %s %d\n", __func__, __LINE__);
// 1. 访问硬件 读取数据 清中断
printk("[KERN] %s %d\n", __func__, __LINE__);
// 2. 调度中断的下半部处理 数据
mytask.data = 0x10;
tasklet_schedule(&mytask);
printk("[KERN] %s %d\n", __func__, __LINE__);
return IRQ_HANDLED;
}
static int __init test_init(void)
{
int ret;
/**如果是友善提供的内核,则需要把arch/arm/mach-s5pv210/mach-mini210.c文件的gpio_buttons定义的相关按键去掉,不然板子上的按键中断就已经被占用,不能注册中断*/
ret = request_irq(gpio_to_irq(S5PV210_GPH2(0)), key_handler, IRQF_TRIGGER_FALLING, "mykey_irq", NULL);
return 0;
}
static void __exit test_exit(void)
{
free_irq(gpio_to_irq(S5PV210_GPH2(0)), NULL);
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Micky Liu");
MODULE_DESCRIPTION("Test for tasklet");
//Makefile
obj-m = test.o
KERN = /home/administrator/soft/kernel/linux-3.0.8/
all:
make -C $(KERN) M=`pwd` modules
cp *.ko /nfsroot/ko/
.PHONY:clean
clean:
make -C $(KERN) M=`pwd` modules clean
(6)工作队列work_queue的实现
work_queue是另一种下半部实现方式,它可以把工作推后,交给一个内核线程执行。
这样,work_queue将在进程上下文中运行,从而允许重新调度甚至睡眠。
如果推后执行的任务需要睡眠,那么就选择work_queue,如果不需要睡眠,那么就选择软中断或tasklet。
工作队列可以用内核线程替换,但不提倡这样做。
内核中为每个cpu建立了一个默认的工作者线程(worker thread)enents/n.
单cpu只有enents/0这样一个线程,而双cpu会多一个enents/1。
许多驱动都把自己的下半部work_queue交给这个默认线程处理。
如果下半部要完成的工作对性能要求比较严格,也可以创建自己的工作队列。
工作队列在kernel/workqueue.c中实现
(7)使用默认的工作队列
<linux/workqueue.h>
a.创建我们要完成的工作(work_struct)
静态
DECLARE_WORK(name, void(*func) (void *));
动态
struct work_struct *mywork = kmalloc(...);
INIT_WORK(struct work_struct *mywork, void(*func) (void *));
b.提供工作队列的处理函数
void work_handler(struct work_struct *data)
该函数会由一个工作者线程执行,运行在进程上下文。
默认情况下,可以响应中断,并且不持有任何锁。
如果需要,函数可以睡眠。
注意!尽管允许在进程上下文,但不能访问用户空间。
d.对工作进行调度
我们的工作将被提交给默认的events工作线程,调用:
schedule_work(&mywork);
work马上会被调度,一旦其所在的cpu上的enents线程被唤醒,它就会被执行。
schedule_delayed_work(&work, delay);
延迟delay个时钟节拍后执行工作
e.刷新工作队列
void flush_scheduled_work(void);
函数会一直等待,直到队列中所有的对象(除了那些需要延迟的)都被执行以后才返回。
可能在模块卸载时调用。
f.取消延迟执行的操作
int cancel_delayed_work(struct work_struct *work);
(8)创建我们自己的工作队列
<linux/workqueue.h>
a.创建新的工作队列
如果默认的队列不能满足性能要求,可以创建一个新的工作队列和相应的工作者线程。
每个cpu上都会创建一个工作者线程。
struct workqueue_struct *create_workqueue(const char *name);
在每个cpu上创建一个名为name/n的线程
struct workqueue_struct *create_singlethread_workqueue(const char *name);
只在当前cpu上创建线程
比如默认的events队列的创建就调用:
struct workqueue_struct *keventd_wq;
keventd_wq = create_workqueue("events");
b.创建要完成的工作和工作队列的处理函数和使用events队列是一样的
c.对工作进行调度
int queue_work(struct workqueue_struct *wq, struct work_struct *work);
或:
int queue_delayed_work(struct workqueue_struct *wq, struct work_struct *work, unsigned long delay);
d.刷新指定的工作队列
flush_work_queue(struct workqueue_struct *wq);
(9)如何根据具体情况选择相应的下半部实现机制
*软中断的执行速度最快,但必须小心处理数据共享.
*由于两个同种类型的tasklet不能同时执行(并发),所以实现起来比软中断要简单。
*使用最简单的是task queue,但它的开销也最大,因为涉及到内核线程和上下文切换
workqueue例子1:
//test.c (可传递参数到下半部处理函数中,并使用默认的工作队列)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/workqueue.h>
#include <linux/sched.h>
#include <mach/gpio.h>
#include <mach/regs-gpio.h>
struct my_work_struct_t {
struct work_struct work;
int data;
};
static void work_handler(struct work_struct *work)
{
struct my_work_struct_t *my_work_tmp = container_of(work, struct my_work_struct_t, work);
printk("[KERN] %s %d\n", __func__, __LINE__);
printk("[KERN] in_interrupt() = %lu, comm = %s, data = %d\n",in_interrupt(), current->comm, my_work_tmp->data);
printk("[KERN] %s %d\n", __func__, __LINE__);
}
static struct my_work_struct_t mywork;
static irqreturn_t key_handler(int irqno, void *dev_id)
{
printk("[KERN] %s %d\n", __func__, __LINE__);
// 1. 访问硬件 读取数据 清中断
printk("[KERN] %s %d\n", __func__, __LINE__);
mywork.data = 10;
// 2. 调度中断的下半部处理 数据
schedule_work(&mywork.work);
printk("[KERN] %s %d\n", __func__, __LINE__);
return IRQ_HANDLED;
}
static int __init test_init(void)
{
int ret;
INIT_WORK(&mywork.work, work_handler);
ret = request_irq(gpio_to_irq(S5PV210_GPH2(0)), key_handler, IRQF_TRIGGER_FALLING, "mykey_irq", NULL);
return 0;
}
static void __exit test_exit(void)
{
free_irq(gpio_to_irq(S5PV210_GPH2(0)), NULL);
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Micky Liu");
MODULE_DESCRIPTION("Test for workqueue");
workqueue例子2:
//test.c (可传递参数到下半部处理函数中,并使用自己创建的工作队列)
#include <linux/module.h>
#include <linux/init.h>
#include <linux/irq.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>
#include <mach/gpio.h>
#include <mach/regs-gpio.h>
struct my_work_struct_t {
struct work_struct work;
int data;
};
static struct my_work_struct_t mywork;
static struct workqueue_struct *wq;
static void work_handler(struct work_struct *work)
{
struct my_work_struct_t *my_work_struct = container_of(work, struct my_work_struct_t, work);
printk(KERN_INFO "%s %d data=%d\n", __func__, __LINE__, my_work_struct->data);
}
static irqreturn_t key_handler(int irqno, void *dev_id)
{
mywork.data = (int)dev_id;
queue_work(wq, &mywork.work);
return IRQ_HANDLED;
}
static int __init test_init(void)
{
int ret;
INIT_WORK(&mywork.work, work_handler);
wq = create_workqueue("my_workqueue");
ret = request_irq(gpio_to_irq(S5PV210_GPH2(0)), key_handler, IRQF_TRIGGER_FALLING, "KEY_0", (void *)10);
return ret;
}
static void __exit test_exit(void)
{
free_irq(gpio_to_irq(S5PV210_GPH2(0)), NULL);
flush_workqueue(wq);
destroy_workqueue(wq);
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Micky Liu");
MODULE_DESCRIPTION("Test for workqueue");