在前面的上一节中,我们初步了解了中断的相关知识,现在看下我们常说的上半部与下半部的机制吧。。。 Linux中断分为两个半部:上半部(tophalf)和下半部(bottom half)。 上半部的功能是"登记中断",当一个中断发生时,它进行相应地硬件读写后就把中断例程的下半部挂到该设备的下半部执行队列中去。 因此,上半部 执行的速度就会很快,可以服务更多的中断请求。 但是,仅有"登记中断"是远远不够的,因为中断的事件可能很复杂。 因此,Linux引入了一个下半部,来完 成中断事件的绝大多数使命。 下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的, 下半部几乎做了中断处理程序所有的事情,而且可以被新的 中断打断!下半部则相对来说并不是非常紧急的, 通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。 Linux实现下半部的机制主要有tasklet和工作队列。 具体的使用方法和原理我们将下两节中来完成。。。 tasklet机制 tasklet机制是一种推迟执行的机制. 1.数据结构为taskletstruct /* Tasklets --- multithreaded analogue of BHs. Main feature differing them of generic softirqs: tasklet is running only on one CPU simultaneously. Main feature differing them of BHs: different tasklets may be run simultaneously on different CPUs. Properties:
- If tasklet_schedule() is called, then tasklet is guaranteed to be executed on some cpu at least once after this.
- If the tasklet is already scheduled, but its excecution is still not started, it will be executed only once.
- If this tasklet is already running on another CPU (or schedule is called from tasklet itself), it is rescheduled for later.
- Tasklet is strictly serialized wrt itself, but not
wrt another tasklets. If client needs some intertask synchronization,
he makes it with spinlocks.
*/
struct tasklet_struct
{
struct tasklet_struct *next; //指向链表中下一个结构
unsigned long state; //状态
atomic_t count; //引用计数
void (*func)(unsigned long); //要调用的函数
unsigned long data; //传递给函数的参数
};
结构中的func域就是下半部中要推迟执行的函数 ,data是它唯一的参数。
State域的取值为TASKLET_STATE_SCHED或TASKLET_STATE_RUN。
TASKLET_STATE_SCHED表示小任务已被调度,正准备投入运行,TASKLET_STATE_RUN表示小任务正在运行。
TASKLET_STATE_RUN只有在多处理器系统上才使用,单处理器系统什么时候都清楚一个小任务是不是正在运行
(它要么就是当前正在执行的代码,要么不是)。
Count域是小任务的引用计数器。如果它不为0,则小任务被禁止,不允许执行;只有当它为零,小任务才被激活,
并且在被设置为挂起时,小任务才能够执行。
2.声明和使用tasklet
大多数情况下,为了控制一个寻常的硬件设备,tasklet机制是实现下半部的最佳选择。tasklet可以动态创建,
使用方便,执行起来也比较快。
我们既可以静态地创建tasklet,也可以动态地创建它。
选择那种方式取决于到底是想要对tasklet进行直接引用还是一个间接引用。
如果准备静态地创建一个tasklet(也就是对它直接引用),使用下面两个宏中的一个:
DECLARE_TASKLET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data)
这两个宏都能根据给定的名字静态地创建一个taskletstruct结构。
当该tasklet被调度以后,给定的函数func会被执行,它的参数由data给出。
这两个宏之间的区别在于引用计数器的初始值设置不同。第一个宏把创建的tasklet的引用计数器设置为0,
因此,该tasklet处于激活状态。另一个把引用计数器设置为1,所以该tasklet处于禁止状态。例如:
DECLARE_TASKLET(mytasklet, my_tasklet_handler, dev);
这行代码其实等价于
struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0), tasklet_handler, dev};
这样就创建了一个名为mytasklet的小任务,其处理程序为tasklethandler,并且已被激活。
当处理程序被调用的时候,dev就会被传递给它。
3.编写自己的tasklet处理程序
tasklet处理程序必须符合如下的函数类型:
void tasklet_handler(unsigned long data)
由于tasklet不能睡眠,因此不能在tasklet中使用信号量或者其它产生阻塞的函数。
但是tasklet运行时可以响应中断。
4 调度自己的tasklet
通过调用taskletschedule()函数并传递给它相应的taskltstruct指针,
该tasklet就会被调度以便适当的时候执行:
tasklet_schedule(&mytasklet); /把 my_tasklet 标记为挂起 /
在tasklet被调度以后,只要有机会它就会尽可能早的运行。在它还没有得到运行机会之前,
如果一个相同的tasklet又被调度了,那么它仍然只会运行一次。
可以调用taskletdisable()函数来禁止某个指定的tasklet。如果该tasklet当前正在执行,
这个函数会等到它执行完毕再返回。调用taskletenable()函数可以激活一个tasklet,
如果希望把以DECLARE_TASKLET_DISABLED()创建的tasklet激活,也得调用这个函数,如:
tasklet_disable(&mytasklet); / tasklet现在被禁止,这个tasklet不能运行 /
tasklet_enable(&mytasklet); / tasklet现在被激活 /
也可以调用taskletkill()函数从挂起的队列中去掉一个tasklet。
该函数的参数是一个指向某个tasklet的taskletstruct的长指针。
在tasklet重新调度它自身的时候,从挂起的队列中移去已调度的tasklet会很有用。
这个函数首先等待该tasklet执行完毕,然后再将它移去。下面是一个示例:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>
static struct tasklet_struct my_tasklet;
static void tasklet_handler(unsigned long data)
{
printk(KERN_ALERT "tasklet_handler is running.\n");
}
static int __init init_tasklet(void)
{
tasklet_init(&my_tasklet,tasklet_handler,0);
tasklet_schedule(&my_tasklet);
return 0;
}
static void __exit exit_tasklet(void)
{
tasklet_kill(&my_tasklet);
printk(KERN_ALERT "tasklet_exit running.\n");
}
MODULE_LICENSE("GPL");
module_init(init_tasklet);
module_exit(exit_tasklet);
$ ./mmake tasklet.c
$ sudo insmod tasklet.ko
$ dmesg
tasklet_handler is running.
tasklet模型框架:
//tasklet使用模板
/定义tasklet和底半部函数相关联/
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet,xxx_do_tasklet,0);
/中断处理底半部/
void xxx_do_tasklet(unsigned long)
{
............
}
/中断处理顶半部/
irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
.....................
/调度xxx_do_tasklet函数在适当的时候执行。/
tasklet_schedule(&xxx_tasklet);
......................
}
/设备驱动模块加载函数/
int __init xxx_init(void)
{
.................
/申请中断/
result=request_irq(xxx_irq,xxx_interrupt,SA_INTERRUPT,"XX",NULL);
..................
}
/设备驱动模块卸载函数/
void __exit xxx_exit(void)
{
.....................
free_irq(xxx_irq,xxx_interrupt);
.....................
}
/*软中断和tasklet仍然运行于中断上下文,而工作队列则运行于进程上下文。因此,软中断和tasklet处理函数中不能睡眠,而工作队列处理函数中允许睡眠。
local_bh_disable()和local_bh_enbale()是内核中用于禁止和使能 中断和tasklet底半部机制的函数*/
工作队列 上面我们了解了tasklet,下面我们将继续了解另外一个下半部分实现的方法:工作队列 为什么还需要工作队列? 工作队列(work queue)是另外一种将中断的部分工作推后的一种方式, 它可以实现一些tasklet不能实现的工作,比如工作队列机制可以睡眠。这种差异的本质原因是, 在工作队列机制中,将推后的工作交给一个称之为工作者线程(worker thread)的内核线程去完成 (单核下一般会交给默认的线程events/0)。因此,在该机制中, 当内核在执行中断的剩余工作时就处在进程上下文(process context)中。 也就是说由工作队列所执行的中断代码会表现出进程的一些特性,最典型的就是可以重新调度甚至睡眠。 对于tasklet机制(中断处理程序也是如此),内核在执行时处于中断上下文(interrupt context)中。 而中断上下文与进程毫无瓜葛,所以在中断上下文中就不能睡眠。 因此,选择tasklet还是工作队列来完成下半部分应该不难选择。当推后的那部分中断程序需要睡眠时, 工作队列毫无疑问是你的最佳选择;否则,还是用tasklet吧。 中断上下文 在了解中断上下文时,先来回顾另一个熟悉概念:进程上下文(这个中文翻译真的不是很好理解, 用“环境”比它好很多)。一般的进程运行在用户态,如果这个进程进行了系统调用, 那么此时用户空间中的程序就进入了内核空间,并且称内核代表该进程运行于内核空间中。 由于用户空间和内核空间具有不同的地址映射,并且用户空间的进程要传递很多变量、参数给内核, 内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行。 这样就产生了进程上下文。 所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容。 当内核需要切换到另一个进程时(上下文切换),它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态继续执行。上述所说的工作队列所要做的工作都交给工作者线程来处理,因此它可以表现出进程的一些特性,比如说可以睡眠等。 对于中断而言,是硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。 过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理, 中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。 因此处于中断上下文的tasklet不会有睡眠这样的特性。 工作队列的使用 内核中通过下述结构体来表示一个具体的工作: struct work_struct { unsigned long pending;//这个工作是否正在等待处理 struct list_head entry;//链接所有工作的链表,形成工作队列 void (*func)(void *);//处理函数 void *data;//传递给处理函数的参数 void *wq_data;//内部使用数据 struct timer_list timer;//延迟的工作队列所用到的定时器 }; 而这些工作(结构体)链接成的链表就是所谓的工作队列。工作者线程会在被唤醒时执行链表上的所有工作,当一个工作被执行完毕后,相应的workstruct结构体也会被删除。当这个工作链表上没有工作时,工作线程就会休眠。 通过如下宏可以创建一个要推后的完成的工作: DECLARE_WORK(name,void(func)(void),void *data); 也可以通过下述宏动态创建一个工作: INIT_WORK(struct work_struct *work,void(func)(void),void *data); 与tasklet类似,每个工作都有具体的工作队列处理函数,原型如下: void work_handler(void *data) 将工作队列机制对应到具体的中断程序中,即那些被推后的工作将会在func所指向的那个工作队列处理函数中被执行。 实现了工作队列处理函数后,就需要schedulework函数对这个工作进行调度,就像这样: schedule_work(&work); 这样work会马上就被调度,一旦工作线程被唤醒,这个工作就会被执行(因为其所在工作队列会被执行)。 下面是一个示例: //------------------------------------------------------------------- // tryworkq.c // // This module demonstrates the use of a kernel workqueue, as a // mechanism for scheduling work to be performed after a delay. // // NOTE: Developed and tested with Linux kernel version 2.6.10. // // programmer: ALLAN CRUSE // written on: 20 MAR 2005 // revised on: 24 OCT 2007 -- for workqueue changes in 2.6.22.5 //------------------------------------------------------------------- #include <linux/module.h> // for init_module() #include <linux/workqueue.h> // for create_workqueue() char modname[] = "tryworkq"; void dowork( struct work_struct * ); DECLARE_DELAYED_WORK( mywork, dowork ); struct workqueue_struct *myqueue; void dowork( struct work_struct dummy ) { printk( "\n\n%15s: I am doing the delayed work now\n", modname ); } int init_module( void ) { int retval; printk( "<1>\nInstalling '%s' module\n", modname ); myqueue = create_singlethread_workqueue( "mywork" ); retval = queue_delayed_work( myqueue, &mywork, HZ5 ); printk( "retval=%d\n", retval ); return 0; // SUCCESS } void cleanup_module( void ) { printk( "<1>Removing '%s' module\n", modname ); destroy_workqueue( myqueue ); } MODULE_LICENSE("GPL"); $ ./mmake tryworkq.c $ sudo insmod tryworkq.ko $ dmesg
工作队列之编程模板: //定义工作队列和关联函数 struct work_struct xxx_wq; void xxx_do_work(unsigned long); //中断处理底半部 void xxx_do_work(unsigned long) { ……………… } //中断处理顶半部 irqreturn_t xxx_interrupt( int irq, void * dev_id, struct pt_regs * regs) { ………….. schedule_work(&xxx_wq); …………. } //设备驱动模块加载函数 int xxx_int(void) { ………… result = request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, “xxx”, NULL); //初始化工作队列 INIT_WORK(&xxx_wq, (void (*) (void *)) xxx_do_work, NULL); ……………. }
//设备驱动模块卸载函数 void xxx_exit(void) { ……………
free_irq(xxx_irq, xxx_interrupt);
………….
} 下面说下tasklet 与 workqueue的区别? tastlet是个软中断,workqueue放在内核线程中执行。 软中断比workqueue优先处理,在它两的入口处中断都是使能的。 另外大概2.6.32(记不得确切版本了)以后 IRQ_ONESHOT会创建单独的线程处理中断, 并且在底半部执行完后自己enable中断。 参考文章: http://edsionte.com/techblog/archives/1582 http://blog.csdn.net/wq897387/article/details/7495506