分类: LINUX
2009-07-23 21:21:40
7.1 下半部
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。对于在上半部和下半部之间划分工作,尽管不存在某种严格的规则,但还是有一些提示可供借鉴:(1)如果一个任务对时间非常敏感,将其放在中断处理程序中执行。(2)如果一个任务和硬件相关,将其放在中断处理程序中执行。(3)如果一个任务要保证不被其它中断打断,将其放在中断处理程序中执行。(4)其它所有任务,考虑放在下半部执行。当我们开始尝试写自己的驱动程序的时候,读一下别人的中断处理程序和相应的下半部会令你受益匪浅。现在的问题是:下半部具体放到以后的什么时候去做呢?下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让他们在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序一返回就会马上执行。下半部执行的关键在于当它们运行的时候,允许响应所有中断。在2.6中,内核提供了三种不同形式的下半部实现机制:软中断、tasklet和工作队列。
7.2 软中断(kernel/softirq.c)
7.2.1 软中断的实现
软中断是在编译期间静态分配的。不像tasklet那样能被动态的注册或去除。软中断由softirq_action结构表示,它定义在
struct softirq_action {
void( *action)(struct softirq_action *); /*待执行的函数*/
void *date; /传递给函数的参数*/
} ;
在kernel/softirq.c中定义了一个包含有32个该结构体的数组。
static strcut softirq_action softirq_vec[32]; 每个注册的软中断都占据该数组中的一项。
(1)软中断处理程序:软中断处理程序action的函数原型如下:
void softirq_handler(struct softirq_action *)
当内核运行一个软中断处理程序(什么时候如何运行一个软中断程序)的时候,它就会执行这个action函数,其唯一的参数为指向相应的softirq_action结构体的指针。
(2)执行软中断:一个注册的软中断必须在被标记后才会执行。这被称作触发软中断(raising the softirq)。通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。软中断被标记后,可以用softirq_pending()检查到这个标记并按照索引号将softirq_pending()的返回值的相应位置1。软中断在do_softirq()中执行。do_softirq()经过简化后的核心部分:
u32 pending = sofeirq_pending(cpu);
if(pending) {
struct softirq_action *h = softirq_vec;
softirq_pending(cpu) = 0;
do {
if(pending&1) h->action(h); //调用action函数
h++;
pending>>=1;
}while(pending);
}
7.2.2 使用软中断
(1)分配索引:在编译期间,可以通过
raise_softirq(NET_TX_SOFTIRQ);
这会触发NET_TX_SOFTIRQ软中断。它的处理程序net_tx_action()就会在内核下一次执行软中断时投入运行。该函数在触发一个软中断前要禁止中断,触发后再恢复回原来的状态。在中断处理程序中触发软中断是最常见的形式。这样,内核在执行完中断处理程序后,马上就会调用do_softirq。
7.3 tasklet
tasklet是利用软中断实现的一种下半部机制。它和进程没有任何关系。它和软中断本质上很相似,行为表现也相近,但是,它的接口更简单,锁保护也要求较低。
7.3.1 tasklet的实现
因为tasklet是通过软中断实现的,所以它本身也是软中断。
(1)tasklet结构体:tasklet由tasklet_struct结构表示。每个结构体单独代表一个tasklet,它在
struct tasklet_struct {
struct task_struct *next; /*指向链表中的下一个tasklet */
unsigned long state; /* tasklet的状态 */
atomic_t count; /* 引用计数器 */
void (*func) (unsigned long); /* tasklet处理函数 */
unsigned long data; /*给tasklet处理函数的参数 */
};
结构体中的func成员是tasklet的处理程序,data是它唯一的参数。state成员只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet已经被调度,正准备投入运行,TASKLET_STATE_RUN表示该tasklet正在运行。只有count为0时,tasklet才被激活,否则被允许,不允许执行。
(2)调度tasklet
tasklet是由tasklet_schedule()和tasklet_hi_schedule()函数进行调度的,它们接受一个指向tasklet_struct结构的指针作为参数。已调度的tasklet存放在两个链表中:tasklet_vec和task_hi_vec中。它们都是由tasklet_struct结构体构成的链表。链表中的每个tasklet_struct代表一个不同的tasklet。
7.3.2 使用tasklet
(1)声明自己的tasklet:可以静态创建,也可以动态创建,分别对应直接引用和间接引用。静态创建,使用下面
DECLARE_TASKLET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data);
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);运行代码实际上等价于:
struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0), my_tasklet_handler, dev }; 这样就创建了一个名为my_tasklet,处理程序为tasklet_handler并且已经被激活的tasklet。
(2)编写自己的tasklet处理程序:必须符合规定的函数类型: void tasklet_handler(unsigned long data)
(3)调度自己的tasklet:通过调用task_schedule()函数并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行。tasklet_schedule(&my_tasklet);
关于tasklet还有许多地方不明白,有待进一步学习!!!!!!!
下面我们看一下软中断和tasklet的异同:在前期准备工作上,首先要给软中断分配索引,而tasklet则要用宏对处理程序声明。在给软中断分配索引后,还要通过open_softirq()函数来注册处理程序。这样看来,tasklet是一步到位,直接到了处理函数,而软中断需要做更多工作。接下来软中断要等待触发(raise_softirq()或raise_softirq_irqoff),而tasklet则是等待tasklet_schedule()和tasklet_hi_schedule()对其进行调度。两者虽然在命名上不同,但殊途同归,最终的结果都是等待do_softirq()去执行处理函数,即将下半部设置为待执行状态以便稍后执行。另外,在tasklet的tasklet_schedule()中,需要完成的动作之一便是唤起(触发)TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,说明tasklet仍然是基于软中断的。在进入do_softirq()之后,所做的工作仍然有所不同,不再论述。
7.4 工作队列
工作队列(work queue)是另外一种将工作推后执行的形式,他和我们前面讨论过的其他形式完全不同。工作队列可以把工作推后,交由一个内核线程去执行——这个下半部总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是它允许重新调度甚至睡眠。
7.4.1 工作队列的实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的其他任务。它创建的这些内核线程被称作工作者线程。工作队列可以让驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个默认的工作者线程来处理这些工作。因此,工作队列最基本的表现形式就转变成了一个把需要推后执行的任务交给特定的通用线程这样一个接口。
(1)表示线程的数据结构
工作者线程用workqueue_struct结构表示:
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
const char *name;
struct list_head list;
};该结构内是一个由cpu_workqueue_struct结构组成的数组,定义在kernel/workqueue.c中,数组的每一项对应一个系统中的处理器。每个工作者线程都对应这样的cpu_workqueue_struct结构体。cpu_workqueue_struct是kernel/workqueue.c中的核心数据结构: struct cpu_workqueue_struct {
spinlock_t lock; /* 锁定以便保护该结构体 */
long romove_sequeue; /* 最近一个被加上的(下一个要运行的) */
long insert_sequeue; /*下一个要加上的 */
wait_queue_head_t more_work;
wait_queue_head_t work_done;
struct workqueue_struct *wq; /* 有关联的workqueue_struct结构 */
task_t *thread; /* 有关联的线程 */
int run_depth; /* run_workqueue()循环深度 */
};由此可以看出,每个工作者线程类型关联一个自己的workqueue_struct。在该结构体里面,给每个线程分配一个cpu_workqueue_struct,因而也就是给每个处理器分配一个,因为每个处理器都有一个该类型的线程。
似乎有点乱说一下自己的见解:首先,工作者线程和workqueue_struct结构是针对系统而言的,它声明了一个由cpu_workqueue_struct结构组成的数组,其元素个数与系统中的处理器个数对应,对每个处理器而言,真正的工作者线程是一个cpu_workqueue_struct结构体。在后面的内容中我们会了解到,在创建新的工作队列和与之相关的工作者线程时,用到的是结构体workqueue_struct。在每个结构体cpu_workqueue_struct中有一个指向workqueue_struct的指针,它是用来与相关联的结构体相联系的。我们可以理解为结构体cpu_workquue_struct把自己的类型分离出去,交给了workqueue_struct保管,并由指针wq去指向这个“管家”。
(2)表示工作的数据结构
所有工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数(worker_thread)开始休眠。当有操作被插入到队列的时候,线程就会被唤醒,以便执行这些操作。工作用
unsigned long pending; /* 这个工作是否正在等待处理 */
struct list_head entry; /* l连接所有工作的链表 */
void (* func) (void *); /* 处理函数 */
void *wq_data; /* 内部使用 */
struct timer_list timer; /* 延迟的工作队列所用到的定时器 */
};这些结构体被连接成链表,在每个处理器的每种类型的队列都对应这样一个链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。当工作完毕时,他会将相应的work_struct对象从链表中移去。
7.4.2 使用工作队列
(1)创建推后的工作
首先要做的是实际创建一些需要推后执行的工作。可以通过DECLARE_WORK在编译时静态的创建该结构体:
DECLARE_WORK(name, void (*func) (void *), void *data);
这样就会静态的创建一个名为name,处理函数为func,参数为data的work_struct结构体。也可以在运行时通过指针创建一个工作:
INIT_WORK(struct work_struct *work, void (*func)(void *), void *data);
这样就动态的初始化了一个由work指向的工作。
(2)工作队列的处理函数
原型是:void work_handler(void *data)
(3)对工作进行调度
现在工作已经创建,我们可以调度它了,要把给定工作的处理函数提交给默认的events工作线程,只需调用: schedule_work(&work); work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。