工作队列解析

来源:互联网 发布:马云开淘宝怎么赚钱 编辑:程序博客网 时间:2024/05/16 05:45

工作项、工作队列和工作者线程

推后执行的任务叫做工作(work),描述它的数据结构为work_struct ,这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct ,而工作线程就是负责执行工作队列中的工作。系统默认的工作者线程为events

工作队列(work queue)是另外一种将工作推后执行的形式。工作队列可以把工作推后,交由一个内核线程去执行—这个下半部分总是会在进程上下文执行,但由于是内核线程,其不能访问用户空间。最重要特点的就是工作队列允许重新调度甚至是睡眠。

通常,在工作队列和软中断/tasklet中作出选择非常容易。可使用以下规则
如果推后执行的任务需要睡眠,那么只能选择工作队列;
如果推后执行的任务需要延时指定的时间再触发,那么使用工作队列,因为其可以利用timer延时;
如果推后执行的任务需要在一个tick之内处理,则使用软中断或tasklet,因为其可以抢占普通进程和内核线程;
如果推后执行的任务对延迟的时间没有任何要求,则使用工作队列,此时通常为无关紧要的任务。

实际上,工作队列的本质就是将工作交给内核线程处理,因此其可以用内核线程替换。但是内核线程的创建和销毁对编程者的要求较高,而工作队列实现了内核线程的封装,不易出错,所以我们也推荐使用工作队列。

工作队列使用

相关文件:

kernel/include/linux/workqueue.h

Kernel/kernel/workqueue.c

工作队列的创建

要使用工作队列,需要先创建工作项,有两种方式:

1)静态创建:

DECLARE_WORK(name,function); 定义正常执行的工作项

DECLARE_DELAYED_WORK(name,function); 定义延后执行的工作项

2)动态创建,运行时创建:

通常在probe()函数中执行下面的操作来初始化工作项:

INIT_WORK(&work, new_ts_work);

INIT_DELAYED_WORK(&led_work,s0340_ledtime_scanf);

工作队列待执行的函数原型是:

typedef void(*work_func_t)(structwork_struct *work);

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

创建了工作项之后,在适当的时候可以通过下面的两种方式来提交工作项给工作者线程,通常我们使用的工作队列和工作者线程都是系统初始化时候默认创建的。

工作队列的调度运行

schedule_work(&work)

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

schedule_delayed_work(&delay_work,delay);

&delay_work指向的delay_work直到delay指定的时钟节拍用完以后才会执行。

eg:

schedule_delayed_work(&kpd_backlight_work,msecs_to_jiffies(300));

默认工作队列和工作者线程创建过程

系统默认的工作队列名称是:keventd_wq,默认的工作者线程叫:events/n这里的n是处理器的编号,每个处理器对应一个线程。比如,单处理器的系统只有events/0这样一个线程。而双处理器的系统就会多一个events/1线程。

默认的工作者线程会从多个地方得到被推后的工作。许多内核驱动程序都把它们的下半部交给默认的工作者线程去做。除非一个驱动程序或者子系统必须建立一个属于它自己的内核线程,否则最好使用默认线程。不过并不存在什么东西能够阻止代码创建属于自己的工作者线程。如果你需要在工作者线程中执行大量的处理操作,这样做或许会带来好处。处理器密集型和性能要求严格的任务会因为拥有自己的工作者线程而获得好处。

默认的工作队列keventd_wq只有一个,但是其工作者线程在每一个cpu上都有。而标记为singlethread的工作者线程最存在于一个cpu上。

关于默认工作队列keventd_wq和工作者线程events/n的建立在文件Kernel/kernel/workqueue.c中实现。

Start_kernel()-->rest_init(),该函数中创建了两个内核线程kernel_initkthreadd,这两个线程都和本文描述的部分有关系,先说说kernel_init

kernel_init()-->do_basic_setup()-->init_workqueues(),该函数中创建了上面提到的默认工作队列和工作者线程。

init_workqueues()-->

-->hotcpu_notifier(workqueue_cpu_callback,0);

-->keventd_wq=create_workqueue("events");

注册的cpu通知链cpu_chain上的回调函数是workqueue_cpu_callback(),raw_notifier_call_chain()函数用来调用cpu_chain上的所有回调函数。

这里主要关注的是函数:create_workqueue("events");

@kernel/include/linux/workqueue.h

#define__create_workqueue(name,singlethread,freezeable,rt)/

__create_workqueue_key((name),(singlethread),(freezeable),(rt),/NULL,NULL)

#definecreate_workqueue(name)__create_workqueue((name),0,0,0)

#definecreate_rt_workqueue(name)__create_workqueue((name),0,0,1)

#definecreate_freezeable_workqueue(name)__create_workqueue((name),1,1,0)

#definecreate_singlethread_workqueue(name)__create_workqueue((name),1,0,0)

从宏__create_workqueue的参数可以看出,可以通过传递不同的参数:是否单cpu线程,是否可冻结,是否实时来创建不同类型的工作队列和工作者线程。

work_struct工作项结构体定义:@kernel/include/linux/workqueue.h

工作队列workqueue_struct结构体:@kernel/kernel/workqueue.c


博客分析2

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

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


  1. 工作、工作队列和工作者线程

如前所述,我们把推后执行的任务叫做工作(work ),描述它的数据结构为work_struct ,这些工作以队列结构组织成工作队列(workqueue ),其数据结构为workqueue_struct,而工作线程就是负责执行工作队列中的工作。系统默认的工作者线程为events,自己也可以创建自己的工作者线程。

  1. 表示工作的数据结构

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


structwork_struct{

        unsigned longpending; /* 这个工作正在等待处理吗?*/

        structlist_head entry; /* 连接所有工作的链表*/

        void(*func) (void*); /* 要执行的函数*/

        void*data; /* 传递给函数的参数*/

        void*wq_data; /* 内部使用*/

        struct timer_listtimer; /* 延迟的工作队列所用到的定时器*/

};


这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。

3. 创建推后的工作

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

DECLARE_WORK(name, void (*func) (void *), void *data);

这样就会静态地创建一个名为name ,待执行函数为func ,参数为data work_struct 结构。

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

INIT_WORK(struct work_struct *work, woid(*func) (void*), void *data);

这会动态地初始化一个由work 指向的工作。

4. 工作队列中待执行的函数

工作队列待执行的函数原型是:

void work_handler(void *data)

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

5. 对工作进行调度

现在工作已经被创建,我们可以调度它了。想要把给定工作的待处理函数提交给缺省的events 工作线程,只需调用

schedule_work(&work) 

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

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

schedule_delayed_work(&work, delay);

这时,&work 指向的work_struct 直到delay 指定的时钟节拍用完以后才会执行。

6. 工作队列的简单应用

#include<linux/module.h>
#include<linux/init.h>
#include<linux/workqueue.h>

staticstructworkqueue_struct *queue=NULL;
staticstructwork_struct work;

staticvoidwork_handler(structwork_struct *data)
{
        printk
(KERN_ALERT "workhandler function./n" );
}

staticint__init test_init(void)
{
        
queue=create_singlethread_workqueue("helloworld"); /*创建一个单线程的工作队列*/
        
if(!queue)
                
gotoerr;

        INIT_WORK
(&work,work_handler);
        schedule_work
(&work);

        
return0;
err
:
        
return-1;
}

staticvoid__exit test_exit(void)
{
        destroy_workqueue
(queue);
}
MODULE_LICENSE
("GPL");
module_init
(test_init);
module_exit
(test_exit);


0 0