第8章 下半部和推后执行的工作

8.4 工作队列

工作队列是另外一种将工作推后执行的形式,工作队列可以把工作推后,交由一个内核线程去执行——这个下半部分总是会在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许重新调度甚至是睡眠。

通常,在工作队列和软中断/tasklet中做出选择非常容易。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择软中断或tasklet。实际上,工作队列通常可以用内核线程替换。但由于内核开发者非常反对创建新的内核线程,所以推荐使用工作队列。

如果需要用一个可以重新调度的实体来执行下半部处理,应该使用工作队列。它是唯一能在进程上下文中运行的下半部实现机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时,在需要获取信号量时,在需要执行阻塞式的IO操作时,都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。

8.4.1 工作队列的实现

工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程称作工作者线程。工作队列可以让驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个缺省的工作者线程来处理这些工作。因此,工作队列最基本的表现形式,就转变成了一个把需要推后执行的任务交给特定的通用线程的一种接口。

缺省的工作者线程叫做events/n,n是处理器的编号;每个处理器对应一个线程。例如,单处理器的系统只有events/0一个线程,而双处理器的系统就会多一个events/1线程。缺省的工作者线程会从多个地方得到被推后的工作。许多内核驱动程序都把它们的下半部交给缺省的工作者线程来做。除非一个驱动程序或者子系统必须建立一个属于它自己的内核线程,否则最好使用缺省线程。

不过并不存在什么东西能够阻止代码创建属于自己的工作者线程。如果需要在工作者线程中执行大量的处理操作,这样做或许会带来好处。处理器密集型和性能要求严格的任务会因为拥有自己的工作者线程而获得好处。此时这么做有助于减轻缺省线程的负担,避免工作队列中其他要完成的工作处于饥饿状态。

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;
        struct list_head list;
        const char *name;
        int singlethread;
        int freezeable;         /* Freeze threads during suspend */
        int rt;
#ifdef CONFIG_LOCKDEP
        struct lockdep_map lockdep_map;
#endif
};

cpu_workqueue_struct 是kernel/workqueue.c中的核心数据结构:

/*
 * The per-CPU workqueue (if single thread, we always use the first
 * possible cpu).
 */
struct cpu_workqueue_struct {

        spinlock_t lock;

        struct list_head worklist;
        wait_queue_head_t more_work;
        struct work_struct *current_work;

        struct workqueue_struct *wq;
        struct task_struct *thread;
} ____cacheline_aligned;

2、表示工作的数据结构

所有的工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数执行一个死循环并开始休眠。当有操作被插入到队列里时,线程会被唤醒,以便执行这些操作。当没有剩余的操作时,它又会继续休眠。

工作用<linux/workqueue.h>中定义的work_struct结构体表示:

struct work_struct {
    atomic_long_t data;
#define WORK_STRUCT_PENDING 0        /* T if work item pending execution */
#define WORK_STRUCT_STATIC  1        /* static initializer (debugobjects) */
#define WORK_STRUCT_FLAG_MASK (3UL)
#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
    struct list_head entry;
    work_func_t func;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

worker_thread()函数的核心流程(kernel/workqueue.c),如下:

static int worker_thread(void *__cwq)
{
    struct cpu_workqueue_struct *cwq = __cwq;
    DEFINE_WAIT(wait);

    if (cwq->wq->freezeable)
        set_freezable();

    for (;;) {
        prepare_to_wait(&cwq->more_work, &wait, TASK_INTERRUPTIBLE);
        if (!freezing(current) &&
            !kthread_should_stop() &&
            list_empty(&cwq->worklist))
            schedule();
        finish_wait(&cwq->more_work, &wait);

        try_to_freeze();

        if (kthread_should_stop())
            break;

        run_workqueue(cwq);
    }

    return 0;
}

static void run_workqueue(struct cpu_workqueue_struct *cwq)
{
    spin_lock_irq(&cwq->lock);
    while (!list_empty(&cwq->worklist)) {
        struct work_struct *work = list_entry(cwq->worklist.next,
                        struct work_struct, entry);
        work_func_t f = work->func;
#ifdef CONFIG_LOCKDEP
        /*
         * It is permissible to free the struct work_struct
         * from inside the function that is called from it,
         * this we need to take into account for lockdep too.
         * To avoid bogus "held lock freed" warnings as well
         * as problems when looking into work->lockdep_map,
         * make a copy and use that here.
         */
        struct lockdep_map lockdep_map = work->lockdep_map;
#endif
        trace_workqueue_execution(cwq->thread, work);
        debug_work_deactivate(work);
        cwq->current_work = work;
        list_del_init(cwq->worklist.next);
        spin_unlock_irq(&cwq->lock);

        BUG_ON(get_wq_data(work) != cwq);
        work_clear_pending(work);
        lock_map_acquire(&cwq->wq->lockdep_map);
        lock_map_acquire(&lockdep_map);
        f(work);
        lock_map_release(&lockdep_map);
        lock_map_release(&cwq->wq->lockdep_map);

        if (unlikely(in_atomic() || lockdep_depth(current) > 0)) {
            printk(KERN_ERR "BUG: workqueue leaked lock or atomic: "
                    "%s/0x%08x/%d\n",
                    current->comm, preempt_count(),
                           task_pid_nr(current));
            printk(KERN_ERR "    last function: ");
            print_symbol("%s\n", (unsigned long)f);
            debug_show_held_locks(current);
            dump_stack();
        }

        spin_lock_irq(&cwq->lock);
        cwq->current_work = NULL;
    }
    spin_unlock_irq(&cwq->lock);
}

3、工作队列实现机制的总结

第8章 下半部和推后执行的工作

8.4.2 使用工作队列

1、创建推后的工作

首先要做的是实际创建一些需要推后完成的工作。可以通过DECLARE_WORK在编译时静态地创建该结构体:

#define DECLARE_WORK(n, f)                    \
    struct work_struct n = __WORK_INITIALIZER(n, f)

也可以在运行时通过指针创建一个工作:

#define INIT_WORK(_work, _func)                    \
    do {                            \
        __INIT_WORK((_work), (_func), 0);        \
    } while (0)

2、工作队列处理函数

工作队列处理函数的原型是:

void work_handler(void *data);

这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中。默认情况下,允许响应中断,并且不持有任何锁。如果需要,函数可以睡眠。注意:尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。通常在发生系统调用时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。

3、对工作进行调度

想要把给定工作的处理函数提交给缺省的events工作线程,只需调用:

schedule_work(&work);

work就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。

有时并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,可以调度它在指定的时间执行:

schedule_delayed_work(&work, delay);

4、刷新操作

插入队列的工作会在工作者线程下一次被唤醒时执行。有时,在继续下一步工作之前,必须保证一些操作已经执行完毕了。这一点对模块来说就很重要,在卸载之前,它就有可能需要调用下面的函数。而在内核的其他部分,为了防止竞争的出现,也可能需要确保不再有待处理的工作。

出于以上目的,内核准备一个用于刷新指定工作队列的函数:

kernel/workqueue.c

void flush_scheduled_work(void);

函数会一直等到,直到队列中所有对象都被执行以后才返回。在等到所有待处理的工作执行时 ,该函数会进入休眠状态,所以只能在进程上下文中使用它。

注意:该函数并不取消任何延迟执行的工作。任何通过schedule_delayed_work()调度的工作,如果其延迟时间未结束,它并不会因为调用flush_scheduled_work()而被刷新掉。取消延迟执行的工作调用:

int cancel_delayed_work(struct work_struct *work);

这个函数可以取消任何与work_struct相关的挂起工作。

5、创建新的工作队列

如果缺省的队列不能满足需要,应该创建一个新的工作队列和与之相应的工作者线程。由于这么做会在每个处理器上都创建一个工作者线程,所以只有在明确了必须要靠自己的一套线程来提高性能的情况下,再创建自己的工作队列。

创建一个新的任务队列和与之相关的工作者线程,只需调用一个函数:

struct workqueue_struct *create_workqueue(const char *name);

name参数用于该内核线程的名字。比如,缺省的events队列的创建就调用的是:

static struct workqueue_struct *keventd_wq;

keventd_wq = create_workqueue("events");

这样就会创建所有的工作者线程,并且做好所有开始处理工作之前的准备工作。

创建一个工作时无须考虑工作队列的类型。在创建之后,可以调用下面列举的函数。这些函数与schedule_work()以及schedule_delayed_work()相近,唯一的区别在于它们针对给定的工作队列而不是缺省的events队列进行操作。

int queue_work(struct workqueue_struct *wq, struct work_struct *work);

int queue_delayed_work(struct workqueue_struct *wq,
            struct delayed_work *dwork, unsigned long delay);

最后,可以调用下面的函数刷新指定的工作队列:

void flush_workqueue(struct workqueue_struct *wq);

这个函数只是它在返回前等待清空的是给定的队列。