tasklet
和定时器相关的另一个内核设施是tasklet(小任务)机制。中断管理中大量使用了这种机制。
tasklet在很多方面类似内核定时器:它们始终在中断期间运行,始终会在调度它们的同一CPU上运行,而且都接收一个unsigned long参数,tasklet也会在“软件中断”上下文以原子模式执行。和内核定时器不同的是,我们不能要求tasklet在某个给定时间执行。
软件中断是指打硬件中断的同时执行某些异步任务的一种内核机制。
tasklet的数据结构如下,使用前必须初始化,调用特定的函数或使用特定的宏来声明该结构,可以完成tasklet的初始化:
#include <linux/interrupt.h>struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data);
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data);tasklet为我们提供了许多有意思的特性:
- 一个tasklet可以稍后被禁止或者重新启用;只有启用的次数和禁止的次数相同时,tasklet才会被执行
- 和定时器类似,tasklet可以注册自身
- tasklet可被调度以在通常的优先级或者高优先级执行。高优先级的tasklet总会首先执行。
- 如果系统负荷不重,则tasklet会立即得到执行,但始终不会晚于下一个定时器滴答。
- 一个tasklet可以和其他tasklet并发,但对自身来讲是严格串行处理的,也就是说,同一tasklet永远不会在多个处理器上同时运行。当然我们已经指出,tasklet始终会在调度自己的同一CPU上运行。
下面描述了tasklet相关的内核接口,可在tasklet结构被初始化后使用:
void tasklet_disable(struct tasklet_struct *t);禁用指定的tasklet。该tasklet仍然可以用tasklet_schedule调度,但其执行被推迟,直到该tasklet被重新启用。如果tasklet当前正在运行,该函数会进入忙等待直到tasklet退出为止;因此在调用tasklet_disable之后,我们可以确信该tasklet不会在系统任何地方运行。
void tasklet_disable_nosync(struct tasklet_struct *t);禁用指定的tasklet,但不会等待任何正在运行的tasklet退出。该函数返回后,tasklet是禁用的,而且在重新启用前,不会再次被调度。但是,当该函数返回时,指定的tasklet可能仍在其他CPU上运行。
void tasklet_enable(struct tasklet_struct *t);启用一个先前被禁止的tasklet。如果该tasklet已经被调度,它很快就会运行。对tasklet_enable的调用必须和每个对tasklet_disable的调用匹配,因为内核对每个tasklet保存有一个“禁用计数”。
void tasklet_schedule(struct tasklet_struct *t);调度执行指定的tasklet。如果在获得运行机会之前,某个tasklet被再次调度,则该tasklet只会运行一次。但是如果在该tasklet运行时被调度,就会在完成后再次运行。这样,可确保正在处理事件时发生的其他事件也会被接收并注意到。这种行为也允许tasklet重新调度自身。
void tasklet_hi_schedule(struct tasklet_struct *t); 调度指定的tasklet以高优先级执行。当软件中断处理例程运行时,它会在处理其他软件中断任务(包括”通常“的tasklet)之前处理高优先级的tasklet。理想状态下,只有具备低延迟需求的任务(比如填充音频缓冲区)才能使用这个函数,这样可避免由其他软件中断处理例程引入 的额外延迟。
void tasklet_kill(struct tasklet_struct *t);该函数确保指定的tasklet不会被再次调度运行;当设备要被关闭或者模块要被移除时,我们通常调用这个函数。如果tasklet正被调度执行,该函数会等待其退出。如果tasklet重新调度自己,则应该避免在调用tasklet_kill之前完成重新调度,这和del_timer_sync的处理类似。
tasklet在实现在kernel/softirq.c中。其中有两个(通常优先级和高优先级)tasklet链表,它们作为per-CPU数据结构而声明,并且使用了类似内核定时器那样的CPU相关联机制。tasklet管理中使用的数据结构是个简单链表,因为tasklet不必像内核定时器那样来处理时间问题。
工作队列
从表面上来看,工作队列(workqueue)类似于tasklet,它们都允许内核代码请求某个函数在将来的时间被调用。但是,两者之间存在一些非常重要的区别,其中包括:
- tasklet在软件中断上下文中运行,因此,所有的tasklet代码都必须是原子的。相反,工作队列函数在一个特殊内核进程的上下文中运行,因此它们具有更好的灵活性。尤其是工作队列函数可以休眠。
- tasklet始终运行在被初始提交的同一处理器上,但这只是工作队列的默认方式。
- 内核代码可以请求工作队列函数的执行延迟给定的时间间隔。
两者的关键区别在于:tasklet会在很短的时间段内很快的执行,并且以原子模式执行,而工作队列函数可具有更长的延迟并且不必原子化。两种机制有各自适合的情形。
工作队列的数据结构为struct workqueue_struct类型,定义在<linux/workqueue.h>中。在使用前之前,我们必须显式地创建一个工作队列,可使用下面两个函数之一:
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);每个工作队列有一个或多个专用的进程(“内核线程”),这些进程运行提交到该队列的函数。create_workqueue内核会在系统中的每个处理器上为工作队列创建专用线程。在许多情况下,众多的线程可能对性能具有某种程度的杀伤力;因此如果单个工作线程足够使用,那么应该使用create_singlethread_workqueue创建工作队列。
要向一个工作队列提交一个任务,需要填充一个work_struct结构,这可通过下面的宏在编译时完成:
DECLARE_WORK(name, void(*function)(void *), void *data);
name是要声明的结构名称,function是要从工作队列中调用的函数,而data是要传递给该函数的值。如果要在运行时构造work_struct结构,可使用下面两个宏:
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);在首次构造该结构时应该使用这个宏。
PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data);它不会初始化将该结构链接到工作队列的指针。如果结构已经提交到工作队列,而只是需要修改该结构则应该使用这个宏而不是INIT_WORK。如果要将工作提交到工作队列,则可使用下面的函数之一:
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, unsigned long delay);
它们都是将work提交到给定的queue。但是如果使用queue_delayed_work,则实际的工作至少会在经过指定的jiffies(由delayed决定)之后才会被执行。如果工作被成功添加到队列,则上述函数返回值为1。返回值为非零时意味着给定的work_struct结构已要等待在该队列中,从而不能两次加入该队列。
在将业的某个时间,工作函数会被调用,并传入给定的data值。该函数会在工作线程的上下文运行,因此如果有必要,它可以休眠,当然我们应该仔细考虑休眠会不会影响提交到同一工作队列的其他任务。但是函数不能访问用户空间,这是因为它运行在内核线程,而该线程没有对应的用户空间可以访问。
如果要取消某个挂起的工作队列入口项,可调用:
int cancel_delayed_work(struct work_struct *work);
如果该入口项在开始执行前被取消,则上述函数返回非零值。在调用它后内核会确保不会执行给定的初始化入口项,但是如果该入口项已经在其他处理器上运行,则它返回0,并且返回后该入口项可能仍在运行。为了绝对确保在cancel_delayed_work返回0之后,工作函数不会在系统中的任何地方运行,则应该随后调用下面的函数:
void flush_workqueue(struct workqueue_struct *queue);
在该函数返回后,任何在该调用前被提交的工作函数都不会在系统任何地方运行。
在结束工作队列的使用后,可调用下面的函数释放相关资源:
void destroy_workqueue(struct workqueue_struct *queue);
共享队列
在许多情况下,设备驱动程序不需要有自己的工作队列。如果我们只是偶尔需要向队列中提交任务,则一种更简单、更有效的办法是使用内核提供的共享的默认工作队列。但是如果使用它则应该记住我们正在和其他人共享该工作队列。这意味着,我们不应该长期独占该队列,即不能长时间休眠,而且我们的任务可能需要更长的时间才能获得处理器时间。
使用共享队列,工作的声明和初始化和前面介绍的一样,提交工作使用的是下面函数:
int schedule_work(struct work_struct *work);
int schedule_delayed_work(struct work_struct *work, unsigned long delay);
取消已提交到共享队列的工作和之前介绍过的一样是cancel_delayed_work函数。但是刷新共享工作队列需要另一个函数:
void flush_scheduled_work(void);
因为我们无法知道其他人是否在使用该队列,因此我们也无法知道在flush_scheduled_work返回前到底要花费多少时间。