下半部和推后执行的工作
- 1 下半部
- 为什么要用下半部
- 下半部的环境
- 内核定时器
- 2 软中断
- 软中断的实现
- 软中断处理程序
- 执行软中断
- 使用软中断
- 3 tasklet
- tasklet的实现
- 使用tasklet
- ksoftirqd
- 4 工作队列
- 工作队列的实现
- 工作、工作队列和工作者线程之间的关系
- 使用工作队列
- 5 下半部机制的选择
- 6 在下半部之间加锁
中断处理程序本身存在一些局限,所以它只能完成整个中断处理流程的上半部分。这些局限包括:
- 中断处理程序以异步方式执行并且它有可能会打断其他重要代码(甚至包括其他处理程序)的执行。因此,为了避免被打断的代码停止时间过长,中断处理程序应该执行地越快越好。
- 如果当前有一个中断处理程序正在执行,在最好的情况下(如果设置了SA_INTERRUPT),与该中断同级的其他中断会被屏蔽,在最坏的情况下,当前处理器上所有其他中断都会被屏蔽。因此,仍应该让它们执行的越快越好。
- 由于中断处理程序往往需要对硬件进行操作,所以它们通常有很高的时限要求。
- 中断处理程序不在进程上下文中运行,所以它们不能阻塞,这限制了它们所做的事情。
现在,为什么中断处理程序只能作为整个硬件中断处理流程一部分的原因就很明显了。我们必须有一个快速、异步、简单的处理程序负责对硬件做出迅速响应并完成那些时间要求很严格的额操作。中断处理程序很适合于实现这些功能,可是,对于那些对时间要求相对宽松的任务,就应该推后到中断被激活以后再去运行。
这样,整个中断流程就被分为了两个部分,第一个部分是中断处理程序(上半部),还有我们接下来所讨论的下半部。
1 下半部
下半部的任务是执行与中断处理密切相关但中断处理程序(上半部)本身不执行的工作。
为什么要用下半部
我们希望尽量减少中断处理程序中需要完成的工作量,因为在它运行的时候当前的中断线在所有处理器上都会被屏蔽。更糟糕的是如果一个处理程序是SA_INTERRUPT类型,它执行的时候会禁止所有的本地中断。而缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。再加上中断处理程序要与其他程序异步执行,我们必须尽力缩短中断处理程序的执行。解决的方法就是把一些工作放到以后去做。
下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行就可以了。下半部执行的关键在于当它们运行的时候,允许响应所有的中断。
下半部的环境
和上半部只能通过中断处理程序实现不同,下半部可以通过多种机制实现,这些用来实现下半部的机制分别由不同的接口和子系统组成。
最早的Linux只提供bottom half这种机制用于实现下半部,这种机制也被称为BH,我们现在也这么叫它,以避免和下半部这个通用词汇混淆。BH接口也非常简单,它提供了一个静态创建、由32个bottom half组成的链表。上半部通过一个32位整数中的一位来标识出哪个bottom half可以执行。每个BH都在全局范围内进行同步,即使分属于不同的处理器,也不允许任何;两个bottom half同时执行。
不久,内核开发者引入了任务队列机制来实现工作的推后执行,并用它来替代BH机制。内核为此定义了一组队列。其中每个队列都包含一个由等待调用的函数组成链表。根据其所处队列的位置,这些函数会在某个时刻被执行。驱动程序可以把它们自己的下半部注册到合适的队列上去。这种机制表现得还不错,但不够灵活,对于一些性能要求较高的子系统,像网络部分,它不能胜任。
在2.3版本中,内核开发者引入了软中断(softirqs)和tasklet。如果无需考虑过去的驱动程序兼容的话,软中断和tasklet可以完全替代BH接口。软中断是一组静态定义的下半部接口,有32个,可以在所有处理器上同时执行,即使两个类型相同也可以。tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制。两个不同类型的tasklet可以在不同的处理器上同时执行,但类型相同的tasklet不能同时执行。使用软中断需要特别小心,因为两个相同的软中断有可能同时被执行。此外,软中断必须在编译期间就进行静态注册,与此相反,tasklet可以通过代码进行动态注册。
在2.5版本,BH接口最终被弃置了。所有的BH使用者必须转而使用其他下半部接口。此外,任务队列也被工作队列接口取代了。工作队列是一种简单但很有用的方法,它们先对要推后执行的工作排队,稍后在进程上下文中执行它们。
在2.6版本中,内核提供了三种不同形式的下半部实现机制:软中断、tasklet和工作队列。
内核定时器
另外一个可以用于将工作推后执行的机制是内核定时器。内核定时器把操作推迟到某个确定的时间段之后执行。内核定时器是以软中断为基础建立的。
2 软中断
软中断使用得比较少,而tasklet是下半部更常用的一种形式。软中断的代码位于kernel/softirq.c文件中。
软中断的实现
软中断是在编译期间静态分配的。它不像tasklet那样能被动态地注册或去除。软中断由softirq_action结构表示,定义在include/linux/interrupt.h中
/* softirq mask and active fields moved to irq_cpustat_t in* asm/hardirq.h to get better cache usage. KAO*/struct softirq_action
{void (*action)(struct softirq_action *);void *data;
};
kernel/softirq.c中定义了一个包含有32个该结构体的数组
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
每个被注册的软中断都占据该数组的一项。因此最多可能有32个软中断。注意这是个定制,注册的软中断数目的最大值没法动态改变。
软中断处理程序
软中断处理程序action的函数原型如下:
void softirq_handler(struct softirq_action *);
当内核运行一个软中断处理程序的时候,它就会执行这个函数,其唯一的参数为指向相应softirq_action结构体的指针。例如,如果my_softirq指向softirq_vec数组的某项,那么内核会用如下的方式调用软中断处理程序中的函数:
my_softirq->action(my_softirq);
内核把整个结构体都传递给软中断处理程序。
一个软中断不会抢占另一个软中断。唯一可以抢占软中断的是中断处理程序。不过,其他软中断(可以是相同类型的软中断),可以在其他处理器上同时执行
执行软中断
一个注册的软中断必须在被标记后才会执行。这被称作触发软中断。通常,中断处理程序会在返回前标记它的软中断,使其在稍后执行。在下列地方,待处理的软中断会被检查和执行:
- 从一个硬件中断代码处返回时
- 在ksoftirqd内核线程中
- 在那些显式检查和执行待处理的软中断的代码中,如网络子系统。
不管用什么方法唤起,软中断都要在do_softirq()中执行。如果有待处理的软中断,do_softirq()会循环遍历每一个,调用它们的处理程序。它是先去获取软中断32位位图,如果第n位被设置为1,那么第n为对应类型的软中断等待处理,do_softirq()会去调用softirq_vec数组的第n个软中断处理程序。
使用软中断
软中断保留给系统中对时间要求最严格以及最重要的下半部使用,在2.6.10版本中,只有两个子系统:网络和SCSI,直接使用软中断。此外,内核定时器和tasklet都是建立在软中断上的。对于时间要求严格并能自己高效完成加锁工作的应用,软中断是正确的选择。当你想加入一个新的软中断时,首先应该问问自己为什么tasklet实现不了?
-
分配索引
在编译期间,可以通过include/linux/interrupt.h中定义的一个枚举类型来静态地声明软中断。内核用这些从0开始的索引来表示一种相对优先级。索引号小的软中断在索引号大的软中断之前执行。
建立一个新的软中断必须在此枚举类型中加入新的项。而加入时,不能简单地加入到末尾。应该根据希望赋予它的优先级来决定加入的位置。 -
注册你的处理程序
接着,在运行时通过调用open_softirq()注册软中断处理程序,该函数有三个参数:软中断的索引号、处理函数和data域存放的数值。例如网络子系统,通过以下方式注册自己的软中断:
open_softirq(NET_TX_SOFTIRQ,net_tx_action,NULL);
软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。在一个处理程序运行的时候,当前处理器上的软中断被禁止。但其他的处理器仍可以执行别的软中断。实际上,如果同一个软中断在它被执行的同时再次被触发了,那么另外一个处理器可以同时运行其处理程序。这意味着任何共享数据,都需要严格的锁保护。
- 触发你的软中断
通过在枚举类型的列表中添加新项以及调用open_softirq()进行注册以后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下次调用do_softirq()函数时投入运行。比如,网络子系统可能会调用:
raise_softirq(NET_TX_SOFTIRQ);
这会触发NET_TX_SOFTIRQ软中断,它的处理程序net_tx_action()就会在内核下一次执行软中断时投入运行。该函数在触发一个软中断之前先要禁止中断,触发后再 恢复回原来的状态。如果中断本来就已经被禁止了,那么可以调用另一个函数raise_softirq_irqoff()。
3 tasklet
tasklet是利用软中断实现的一种下半部机制。它和进程没有任何关系。
tasklet的实现
因为tasklet是通过软中断实现的,所以它们本身也是软中断。tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。这两者唯一的区别就是HI_SOFTIRQ类型的软中断优先与TASKLET_SOFTIRQ类型的软中断执行。
- tasklet结构体
tasklet是由tasklet_struct结构表示。每个结构体单独代表一个tasklet,它在include/linux/interrupt.h中定义。
/* Tasklets --- multithreaded analogue of BHs.Main feature differing them of generic softirqs: taskletis running only on one CPU simultaneously.Main feature differing them of BHs: different taskletsmay be run simultaneously on different CPUs.Properties:* If tasklet_schedule() is called, then tasklet is guaranteedto be executed on some cpu at least once after this.* If the tasklet is already scheduled, but its excecution is still notstarted, it will be executed only once.* If this tasklet is already running on another CPU (or schedule is calledfrom tasklet itself), it is rescheduled for later.* Tasklet is strictly serialized wrt itself, but notwrt another tasklets. If client needs some intertask synchronization,he makes it with spinlocks.*/struct tasklet_struct
{struct tasklet_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正在运行。TASKLET_STATE_RUN只有在多处理器的系统上才会作为一种优化来使用,单处理器系统任何时候都清楚单个tasklet是不是正在运行。
count成员是tasklet的引用计数器。如果它不为0,则tasklet被禁止,不允许执行;只有当它为0时,tasklet才被激活,并且在被设备为挂起状态,该tasklet才能够执行。
- 调度tasklet
已调度的tasklet存放在两个单处理器数据结构:task_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)中。这两个数据结构都是由tasklet_struct结构体构成的链表。链表中的每个tasklet_struct代表一个不同的tasklet。
tasklet由tasklet_schedule()和tasklet_hi_schedule()函数进行调度,它们接受一个指向tasklet_struct结构的指针作为参数。两个函数非常类似(区别在于一个使用TASKLET_SOFTIRQ而另一个用HI_SOFTIRQ)
所有的tasklet都通过重复运用HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断实现的。当一个tasklet被调度时,内核就会唤起这两个软中断的一个。随后,该软中断会被特定的函数处理,执行所有已调度的tasklet。同一时间只有一个给定类型的tasklet会被执行(但其他不同类型的tasklet可以被同时执行)。
使用tasklet
- 声明你自己的tasklet
既可以静态创建tasklet,也可以动态创建它。选择那种方式取决于你到时是对tasklet的直接引用还是间接引用。如果你真准备静态创建一个tasklet(直接引用),使用include/linux/interrupt.h中定义的两个宏中一个:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
这两个宏都能根据给定的名称静态创建一个tasklet_struct结构。当该tasklet被调度以后,给定的函数func会执行,它的参数由data给出。这两个宏之间的区别在于引用计数器的初始值设置不同,DECLARE_TASKLET设置为0,该tasklet处于激活状态;DECLARE_TASKLET_DISABLED的引用计数器为1,该tasklet处于禁止状态。
还可以通过将一个间接引用(一个指针)赋给一个动态创建的tasklet_struct结构的方式来初始化一个tasklet。
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
- 编写你自己的tasklet处理程序
tasklet的处理程序必须符合规定的函数类型:
void tasklet_handler(unsigned long data);
因为是靠软中断实现,所以tasklet不能睡眠。这意味着你不能在tasklet中使用信号量或者其他什么阻塞式的函数。
- 调度你自己的tasklet
通过tasklet_schedule()函数并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行。
tasklet_schedule(&my_tasklet);
tasklet被调度以后,只要有机会它就会尽可能早地运行。
可以调用task_disable()函数来禁止某个指定的tasklet。如果该tasklet正在运行,这个函数会等到它执行完毕再返回。也可以调用tasklet_diable_nosync()函数,它也可用来禁止指定的tasklet,不过无须在返回前等待tasklet执行完毕。这么做往往不太安全,因为无法估计该tasklet是否仍在执行。调用tasklet_enable()函数可以激活一个tasklet。
你可以通过调用tasklet_kill()函数从挂起的队列中去掉一个tasklet。
void tasklet_kill(struct tasklet_struct *t);
该函数的参数是一个指向某个tasklet的tasklet_struct的长指针。这个函数首先等待该tasklet执行完毕,然后再将它移去。
ksoftirqd
每个处理器都有一组辅助软中断的内核线程。当内核中出现大量软中断的时候,这些内核进程就会辅助处理它们。
软中断被触发的频率有时会很高,处理函数还会自行重复触发。当一个软中断执行的时候,它可以重新触发自己以便再次得到执行。如果软中断本身出现的频率就高,再加上它们又有将自己重新设置为可执行状态的能力,那么就会导致用户空间进程无法获得足够的处理器时间,因而处于饥饿状态。
在设计软中断时,开发者需要一些折中。最终在内核中实现的方案是不会立即处理重新触发的软中断。而作为改进,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负责,这些线程在最低的优先级上运行(nice值为19),这能避免它们跟其他重要的任务抢夺资源。但它们最终肯定会被执行,所以,这个折中方案能够保证软中断负担很重的时候用户程序不会因为得不到处理时间而处于饥饿状态。相应的,也能保证“过量”的软中断 终究会得到处理。
每个处理器都有一个这样的线程。所有线程的名字都叫做ksoftirqd/n,区别在于n,它是对于的处理器的编号。在一个双CPU的机器上就有两个这样的线程,分别叫做ksoftirqd/0和ksoftirqd/1。为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程。一旦线程被初始化,就会执行类似下面的死循环。
只要有待处理的软中断,ksoftirqd就会调用do_softirq()去处理它们。通过重复执行这样的操作,重新触发的软中断也会被执行。每次迭代后都会调用schedule()以便让更重要的进程得到处理机会。当所有需要执行的操作都完成以后,该内核线程将自己设置为TASK_INTERRUPTIBLE状态,唤起调度程序选择其他可执行进程投入运行。
只要do_softirq()函数发现已经执行过的内核线程重新触发了它自己,软中断内核线程就会被唤醒。
4 工作队列
工作队列是另外一种将工作推后执行的形式,工作队列把工作推后,交由一个内核线程去执行,然后在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许重新调度甚至是睡眠。
工作队列的实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程被称作工作者线程。工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。工作队列子系统提供了一个默认的工作者线程来处理这些工作。
默认的工作者线程叫做events/n,这里的n是处理器编号。每个处理器对应一个线程。比如,单处理器的系统只有events/0这样一个线程。而双处理器的系统就会多一个events/1线程。
- 表示线程的数据结构
工作者线程用workqueue_struct结构表示,定义在kernel/workqueue.c:
/** The externally visible workqueue abstraction is an array of* per-CPU workqueues:*/
struct workqueue_struct {struct cpu_workqueue_struct cpu_wq[NR_CPUS];const char *name;struct list_head list; /* Empty if single thread */
};
该结构内的cpu_workqueue_struct ,定义在kernel/workqueue.c中,cpu_workqueue_struct的每一项对应系统中的一个处理器。由于系统中每个处理器对应一个工作者线程,对于计算机来说,就是每个处理器。每个工作者线程对应一个这样的cpu_workqueue_struct结构体。cpu_workqueue_struct是kernel/workqueue.c中的数据结构:
/** The per-CPU workqueue (if single thread, we always use cpu 0's).** The sequence counters are for flush_scheduled_work(). It wants to wait* until until all currently-scheduled works are completed, but it doesn't* want to be livelocked by new, incoming ones. So it waits until* remove_sequence is >= the insert_sequence which pertained when* flush_scheduled_work() was called.*/
struct cpu_workqueue_struct {spinlock_t lock; /* 锁,来保护该结构体 */long remove_sequence; /* 最近一个 */long insert_sequence; /* Next to add */struct list_head worklist; /* 工作列表 */wait_queue_head_t more_work;wait_queue_head_t work_done;struct workqueue_struct *wq;task_t *thread;int run_depth; /* Detect run_workqueue() recursion depth */
} ____cacheline_aligned;
每个工作者线程关联一个自己的workqueue_struct。在该结构体里面,给每个线程分配一个cpu_workqueue_struct,因而也就是给每个处理器分配一个。
- 表示工作的数据结构
所有的工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化以后,这个函数执行一个死循环并开始休眠。当有操作被插入到队列里的时候,线程就会被唤醒,当没有剩余的操作时,它又会继续休眠。
工作用include/linux/workqueue.h中定义的work_struct结构体表示:
struct work_struct {unsigned long pending; /* 这个工作是否正在等待处理 */struct list_head entry; /* 连接所有工作的链表 */void (*func)(void *); /* 处理函数 */void *data; /* 处理函数的参数 */void *wq_data; /* 内部使用 */struct timer_list timer; /* 延迟的工作队列所用到的处理器 */
};
这样结构体被连接成链表,在每个处理器上的每种类型的队列都对应这样一个链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表中删除。当链表不再有对象的时候,它就会继续休眠。
- run_workqueue()
下一步,由run_workqueue()函数来实际完成推后到此的工作:
static inline void run_workqueue(struct cpu_workqueue_struct *cwq)
{unsigned long flags;/** Keep taking off work from the queue until* done.*/spin_lock_irqsave(&cwq->lock, flags);cwq->run_depth++;if (cwq->run_depth > 3) {/* morton gets to eat his hat */printk("%s: recursion depth exceeded: %d\n",__FUNCTION__, cwq->run_depth);dump_stack();}while (!list_empty(&cwq->worklist)) {struct work_struct *work = list_entry(cwq->worklist.next,struct work_struct, entry);void (*f) (void *) = work->func;void *data = work->data;list_del_init(cwq->worklist.next);spin_unlock_irqrestore(&cwq->lock, flags);BUG_ON(work->wq_data != cwq);clear_bit(0, &work->pending);f(data);spin_lock_irqsave(&cwq->lock, flags);cwq->remove_sequence++;wake_up(&cwq->work_done);}cwq->run_depth--;spin_unlock_irqrestore(&cwq->lock, flags);
}
该函数循环遍历链表上每个待处理的工作,执行链表每个节点上的work_struct中的func成员函数:
- 当链表不为空时,选取下一个节点对象
- 获取我们希望执行的函数func以及参数data
- 把该节点从链表上解下来,将待处理标志位pending清0
- 调用函数
- 重复执行
工作、工作队列和工作者线程之间的关系
我们先看一张图,该图阐述了这几个数据结构的逻辑关系
位于最高一层的是工作者线程。系统允许有多种类型的工作者线程存在。对于指定的一个类型,系统的每个CPU上都有一个该类的工作者线程。在默认情况下内核只有events这一种类型的工作者线程。每个工作者线程都由一个cpu_workqueue_struct结构体表示。而workqueue_struct结构体则表示给定类型的所有工作者线程。比如,在系统默认的通用events工作者类型之外,我们自己加入了一种falcon工作者类型,并且使用的是一个拥有四个处理器的计算久,那么,系统中现在有四个events类型的线程(也就有四个cpu_workqueue_struct结构体)和四个falcon类型的线程(会有另外四个cpu_workqueue_struct结构体)。同时,又一个对应events类型的workqueue_struct和一个对应falcon类型的workqueue_struct。
工作处于最低一层,你的驱动程序创建这些需要推后执行的工作。它们用work_struct结构表示。这个结构体中最重要的部分是一个指针,它指向一个函数,而正是该函数负责处理需要推后执行的具体任务。工作会被提交给某个具体的工作者线程,然后工作者线程会被唤醒并执行这些排好的工作。
使用工作队列
- 创建推后的工作
首先要做的是实际创建一些需要推后完成的工作。可以通过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);
- 工作队列处理函数
工作队列处理函数的原型是:
void work_handler(void *data);
这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中。
- 对工作进行调度
现在工作已经被创建,我们可以调度它了。想要把给定工作的处理函数提交给默认的events工作线程,只需调用:
schedule_work(&work);
work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。有时候我们不希望工作立马执行,而是希望它经过一段延迟后在执行。这种情况下,我们可以调用schedule_delayed_work函数将工作在指定的时间执行:
schedule_delayed_work(&work,delay);
work指向的work_struct知道delay指定的时钟节拍用完后才会执行。
- 刷新操作
插入队列的工作会在工作者线程下一次被唤醒的时候执行。有时,在继续下一步工作之前,你必须保证一些操作已经执行完毕了。处于以上目的,内核准备了一个用于刷新指定工作队列的函数:
void flush_scheduled_work(void);
函数会一直等待,知道队列中所有对象都被执行以后才返回,该函数并不取消任何延迟执行的工作。取消延迟执行的工作应该调用:
int cancel_delayed_work(struct work_struct *work);
这个函数可以取消任何与work_struct相关的挂起工作。
- 创建新的工作队列
如果默认的队列不能满足你的需求,你应该创建一个新的工作队列和与之相应的工作者线程。由于这么做会在每个处理器上都创建一个工作者线程,所以只有在你明确了必须要靠自己的一套线程来提高性能的情况下,再创建自己的工作队列。
创建一个新的任务队列和与之相关的工作者线程,只需调用一个简单的函数:
struct workqueue_struct *create_workqueue(const char *name);
name用于该内核线程的命名。比如,默认events队列的创建就调用的是:
struct workqueue_struct *keventd_wq;
keventd_wq=create_workqueue("events");
这个函数会创建所有的工作者线程(系统中每个处理器都有一个)并且做好所有开始处理工作之前的准备工作。
创建一个工作的时候无需考虑工作队列的类型。在创建之后,我们可以调用下面的函数,将工作加到指定的工作队列上。
你可以调用下面的函数刷新指定的工作队列,确保所有指定工作队列上的任务都已经完成:
flush_workqueue(struct workqueue_struct *wq);
5 下半部机制的选择
在内核2.6版本中,有三种选择:软中断、tasklet和工作队列。tasklet基于软中断实现,所以二者很相近。工作队列机制与它们完全不同,它靠内核线程实现。
从设计角度考虑,软中断提供的执行序列化的保障最少。这就要求软中断处理函数必须采取一些步骤确保共享数据的安全,因为两个甚至更多相同类别的软中断有可能在不同的处理器上同时运行。
如果你需要把任务推后到进程上下文中完成,那么在这三者就只能选择工作队列了。如果明确的说,并不需要睡眠,那么软中断和tasklet可能更合适。
如果说到易于使用,工作队列当仁不让。使用默认的events队列非常简单。接下来是tasklet,它的接口也很简单。最后才是软中断,它必须静态创建,并且需要慎重考虑其实现。
下表是对三种下半部接口的比较:
简单地说,一般地驱动程序的编写者需要做出两个选择。首先,你是不是需要一个休眠的功能?要是有,工作队列就是唯一选择。否则最好用tasklet。要是必须专注于性能的提高,那么就考虑软中断吧。
6 在下半部之间加锁
如果需要禁止所有的下半部处理(就是所有的软中断和所有的tasklet),可以调用local_bh_disable()函数。允许下半部进行处理,可以调用local_bh_enbale()函数。
这些函数有可能被嵌套使用,最后被调用的local_bh_enable()最终激活下半部。比如,第一次调用local_bh_disable函数,则本地软中断处理被禁止。如果local_bh_disable被调用三次,则本地处理仍然被禁止,只有当第四次调用local_bh_enbale函数时,软中断才被最新激活。
这两个函数并不能禁止工作队列的执行,因为工作队列是在进程上下文运行。