7.6 工作队列
从表面上看,工作队列类似于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
在初始化结构体更彻底,应该结构体第一次声明时使用它。PREPARE_WORK
做相同的工作,但是不会初始化链接到工作队列的work_struct
结构的指针。它的用处就是,有时候可能需要改变这个结构体,那么就使用PREPARE_WORK
,而不是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);
任何人都可以添加工作队列到给定的工作队列中。但是,使用queue_delayed_work
函数的话,至少要延时delay
个嘀嗒时间(jiffies)后才会执行真实的工作。返回0,则说明成功地添加工作到工作队列了;非0,说明该work_struct
结构体已经在工作队列中,不需要第二次添加。
如果需要取消挂起的工作队列项可以调用下面的函数:
int cancel_delayed_work(struct work_struct *work);
如果某项工作在开始执行前被取消,返回值是非0。由内核保证在调用cancel_delayed_work
之后不会启动给定work的执行。但是,即使返回0,该工作也有可能已经在不同的处理器上运行了。为了完全保证work函数没有在系统的任何地方运行,在调用cancel_delayed_work
之后,紧接着调用下面这个函数。
void flush_workqueue(struct workqueue_struct *queue);
调用flush_workqueue
函数之后,work已经不会在系统的任何地方运行了。接下来,可以调用下面的函数从工作队列中消除该work:
void destroy_workqueue(struct workqueue_struct *queue);
7.6.1 共享队列
在许多情况下,驱动程序不需要它自己的工作队列。如果只是偶尔提交任务到队列中,使用共享队列更有效率,内核默认使用共享队列。但是,使用共享队列,意味着你不能长时间占有工作队列。jiq
模块(just in queue)生成了两个文件,阐释工作队列的使用。代码如下:
static struct work_struct jiq_work;
/* this line is in jiq_init( ) */
INIT_WORK(&jiq_work, jiq_print_wq, &jiq_data);
当有进程读取/proc/jiqwq
文件时,模块就会没有延时地启动共享队列的跳转。使用的函数是:
int schedule_work(struct work_struct *work);
注意:使用共享队列时,使用的函数不同。它只有work_struct
类型的一个参数。jiq
模块的代码如下:
prepare_to_wait(&jiq_wait, &wait, TASK_INTERRUPTIBLE);
schedule_work(&jiq_work);
schedule( );
finish_wait(&jiq_wait, &wait);
同jit
模块一样,先打印一行;如果有需要,重新提交work_struct
结构体到工作队列中。jiq_print_wq
的完整代码如下:
static void jiq_print_wq(void *ptr)
{
struct clientdata *data = (struct clientdata *) ptr;
if (! jiq_print (ptr))
return;
if (data->delay)
schedule_delayed_work(&jiq_work, data->delay);
else
schedule_work(&jiq_work);
}
如果用户正在读取有延时的设备(/proc/jiqwqdelay
),工作函数使用schedule_delayed_work
重新提交自己。
int schedule_delayed_work(struct work_struct *work, unsigned long delay);
输出结果:
% cat /proc/jiqwq
time delta preempt pid cpu command
1113043 0 0 7 1 events/1
1113043 0 0 7 1 events/1
1113043 0 0 7 1 events/1
1113043 0 0 7 1 events/1
1113043 0 0 7 1 events/1
% cat /proc/jiqwqdelay
time delta preempt pid cpu command
1122066 1 0 6 0 events/0
1122067 1 0 6 0 events/0
1122068 1 0 6 0 events/0
1122069 1 0 6 0 events/0
1122070 1 0 6 0 events/0
从上面的结果来看,读取/proc/jiqwq
文件时没有明显的延时。但是读取/proc/jiqwqdelay
时,每行数据会有一个嘀嗒(jiffy
)的延时。但是打印的进程名称是相同的;它就是内核实施共享队列的线程。斜杠后面打印的是CPU号;刚开始读取时,不知道在哪个CPU上运行,但是之后,就会一直在一个CPU上运行。
取消提交到共享队列中的工作,可以使用cancel_delayed_work
函数。但是,刷新共享工作队列需要一个不同的函数,如下:
void flush_scheduled_work(void);
因为不知道是否还有其它地方使用,所以不会知道flush_scheduled_work
函数多长时间返回。