下半部和推后执行的工作-工作队列机制

来源:互联网 发布:mysql 基本命令 编辑:程序博客网 时间:2024/05/22 00:39

  • 应用场景

         触摸屏驱动和 sensor驱动

  • 实现原理
什么是workqueue?

Linux中的Workqueue机制就是为了简化内核线程的创建。通过调用workqueue的接口就能创建内核线程。并且可以根据当前系统CPU的个数创建线程的数量,使得线程处理的事务能够并行化。
workqueue是内核中实现简单而有效的机制,他显然简化了内核daemon的创建,方便了用户的编程。 一下介绍时基于kernel3.0之前的版本,到了3.o之后有些变化。

Workqueue机制的实现

Workqueue机制中定义的重要数据结构,分析如下:

1、workqueue_struct结构
struct workqueue_struct
{
       struct cpu_workqueue_struct cpu_wq[NR_CPUS];
       const char * name;
       struct list_head list;

2、 cpu_workqueue_struct结构,该结构将CPU和内核线程进行了绑定。在创建workqueue的过程中,Linux根据当前系统CPU的个数创建cpu_workqueue_struct。在该结构主要维护了一个任务队列,以及内核线程需要睡眠的等待队列,另外还维护了一个任务上下文,即task_struct。
struct cpu_workqueue_struct     
{
    spinlock_t               lock;                     /* 锁定以便保护该结构体             */
    long                     romove_sequeue;   /* 最近一个被加上的(下一个要运行的)      */
    long                     insert_sequeue;      /* 下一个要加上的                */
    struct list_head worklist;                      /* 工作队列 */
    wait_queue_head_t        more_work;
    wait_queue_head_t        work_done;
    struct workqueue_struct *wq;               /* 有关联的workqueue_struct结构 */
    task_t                  *thread;                    /* 有关联的线程                 */
    int                      run_depth;                /* run_workqueue()循环深度    */
}

3、 work_struct结构,该结构是对任务的抽象。在该结构中包括需要维护具体的任务方法,需要处理的数据,以及任务处理的时间。该结构定义如下:

struct work_struct {

              unsigned long pending;             /* pending是用来记录工作是否已经挂在队列上*/

               struct list_head entry;                  /* 将任务挂载到queue的挂载点 */

               void (*func)(void *);                   /* 任务方法 */主要就是实现这个回调函数的实体

               void *data;                                  /* 任务处理的数据*/

               void *wq_data;                           /* work的属主 */

               strut timer_list timer;                   /* 任务延时处理定时器 */

};     
注意:以上struct定义并非完整定义,只是摘取了主要的成员。

一个工作队列必须明确的在使用前创建,宏为:
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
 若使用 create_workqueue, 就得到一个工作队列,它在系统的每个处理器上有一个专用的线程。在很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用 create_singlethread_workqueue 来创建工作队列。
        当用户调用workqueue的初始化接口create_workqueue或者create_singlethread_workqueue对workqueue队列进行初始化时,内核就开始为用户分配一个workqueue对象,并且将其链到一个全局的workqueue队列中。然后Linux根据当前CPU的情况,为workqueue对象分配与CPU个数相同的cpu_workqueue_struct对象,每个cpu_workqueue_struct对象都会存在一条任务队列。紧接着,Linux为每个cpu_workqueue_struct对象分配一个内核thread,即内核daemon去处理每个队列中的任务。至此,用户调用初始化接口将workqueue初始化完毕,返回workqueue的指针。 

       在初始化workqueue过程中,内核需要初始化内核线程,注册的内核线程工作比较简单,就是不断的扫描对应cpu_workqueue_struct中的任务队列,从中获取一个有效任务,然后执行该任务。所以如果任务队列为空,那么内核daemon就在cpu_workqueue_struct中的等待队列上睡眠,直到有人唤醒daemon去处理任务队列。 

       Workqueue初始化完毕之后,将任务运行的上下文环境构建起来了,但是具体还没有可执行的任务,所以,需要定义具体的work_struct对象。然后将work_struct加入到任务队列中,Linux会唤醒daemon去处理任务。 

       上述描述的workqueue内核实现原理可以描述如下:


    在Workqueue机制中,提供了一个系统默认的workqueue队列——keventd_wq,也称为共享队列,这个队列是Linux系统在初始化的时候就创建的。用户可以直接初始化一个work_struct对象,然后在该队列中进行调度,使用更加方便。 

Workqueue编程接口

序号

接口函数

说明

1

create_workqueue

用于创建一个workqueue队列,为系统中的每个CPU都创建一个内核线程。输入参数:

@name:workqueue的名称

2

create_singlethread_workqueue

用于创建workqueue,只创建一个内核线程。输入参数:

@name:workqueue名称

3

destroy_workqueue

释放workqueue队列。输入参数:

@ workqueue_struct:需要释放的workqueue队列指针

4

schedule_work

调度执行一个具体的任务,执行的任务将会被挂入Linux系统提供的workqueue——keventd_wq输入参数:

@ work_struct:具体任务对象指针

5

schedule_delayed_work

延迟一定时间去执行一个具体的任务,功能与schedule_work类似,多了一个延迟时间,输入参数:

@work_struct:具体任务对象指针

@delay:延迟时间

6

queue_work

调度执行一个指定workqueue中的任务。输入参数:

@ workqueue_struct:指定的workqueue指针

@work_struct:具体任务对象指针

7

queue_delayed_work

延迟调度执行一个指定workqueue中的任务,功能与queue_work类似,输入参数多了一个delay。


注意:4和5是对共享队列keventd_wq的操作,6和7是对自己创建的工作队列的操作

  • 应用实例

以touchpanel的驱动为例,摘取主要代码如下,代码路径
/kernel/drivers/input/touchscreen/ft5306.c

Probe主要的三个部分是:

1.//动态创建一个工作
INIT_WORK(&ft5x0x_ts->pen_event_work,ft5x0x_ts_pen_irq_work);


2.//创建一个单线程的工作队列
ft5x0x_ts->ts_workqueue =create_singlethread_workqueue(dev_name(&client->dev));


3.//请求中断
err =request_irq(ft5x0x_ts->pdata->irq, ft5x0x_ts_interrupt,IRQF_TRIGGER_FALLING, "ft5x0x_ts", ft5x0x_ts);


相应的回调函数如下:
当中断触发了首先执行这个,这就是所谓的Top half,即在中断上下文中把work和work_queue关联起来(probe中仅仅初始化了)
从而中断退出后去调度内核线程,执行work

static irqreturn_t  ft5x0x_ts_interrupt(int irq, void *dev_id)

{

struct ft5x0x_ts_data *ft5x0x_ts =dev_id;

if(!work_pending(&ft5x0x_ts->pen_event_work)) {//Pending:这个工作是否正在等待处理标志,加入到工作队列后置此标志

disable_irq_nosync(ft5x0x_ts->pdata->irq);

queue_work(ft5x0x_ts->ts_workqueue,&ft5x0x_ts->pen_event_work);//将工作添加到指定的工作队列中,
  //Linux唤醒daemon去处理任务。

  //提交工作pen_event_work给工作者线程ts_workqueue,ts_workqueue会被唤醒并执行pen_event_work,
  //即执行ft5x0x_ts_pen_irq_work

}

return IRQ_HANDLED;

}

这就是所谓的Bottom half,这个函数运行在内核进程的上下文,主要是通过IIC读取触摸的坐标并上报给input子系统

static voidft5x0x_ts_pen_irq_work(structwork_struct *work)

{

int ret = -1;

struct ft5x0x_ts_data *ft5x0x_ts =container_of(work, struct ft5x0x_ts_data,pen_event_work);

FT5XXX_DBG("enter %s\n",__func__);

ret = ft5x0x_read_data();

if (ret == 0) {

ft5x0x_report_value();

}

enable_irq(ft5x0x_ts->pdata->irq);

}

我认为主要的关键点:
workqueue如何进入睡眠以及如何被唤醒:

工作结构体work_struct被连接成链表,对于某个工作队列,在每个处理器上都存在这样一个链表。当调用queue_work时,工作者线程被唤醒,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去,当链表上不再有对象的时候,它就会继续休眠。

通常下半部在中断处理程序一返回就会马上执行,下半部的关键在于当他们运行的时候,允许响应所有的中断。

一直比较疑惑的地方:
按说读取触摸屏坐标应该对实时性要求也是比较高的,一旦来了中断应该立即去读(在Top half完成),但事实是看了几个触摸屏的驱动都是放到了Bottom half里面去做,
即使上半部一返回就立马去执行下半部,也有可能会在下半部里再次来中断?而且按照上面的代码逻辑好像workqueue上最多也只会有1个work

触摸时的log()

<4>[   72.715496]  ft5x0x_ts_interrupt E
<4>[   72.715508]  ft5x0x_ts_interrupt :work_pending
<4>[   72.715516]  ft5x0x_ts_interrupt Before queue_work
<4>[   72.715530]  ft5x0x_ts_interrupt After queue_work
<4>[   72.715536]  ft5x0x_ts_interrupt X
<4>[   72.715556]  ft5x0x_ts_pen_irq_work E
<4>[   72.719246]  ft5x0x_ts_pen_irq_work X
从log上可以得到2点:

1.ft5x0x_ts_interrupt完全退出后才会执行 ft5x0x_ts_pen_irq_work

2..ft5x0x_ts_interrupt退出后立即执行ft5x0x_ts_pen_irq_work

  • 理论基础

              LKD 第6章 中断和中断处理程序  第7章 下半部和推后执行的工作

3. 创建工作
3.1 创建工作queue
a. create_singlethread_workqueue(name)
        该函数的实现机制如下图所示,函数返回一个类型为struct workqueue_struct的指针变量,该指针变量所指向的内存地址在函数内部调用kzalloc动态生成。所以driver在不再使用该work queue的情况下调用:

        void destroy_workqueue(struct workqueue_struct *wq)来释放此处的内存地址。

 

        图中的cwq是一per-CPU类型的地址空间。对于create_singlethread_workqueue而言,即使是对于多CPU系统,内核也只负责创建一个worker_thread内核进程。该内核进程被创建之后,会先定义一个图中的wait节点,然后在一循环体中检查cwq中的worklist,如果该队列为空,那么就会把wait节点加入到cwq中的more_work中,然后休眠在该等待队列中。

        Driver调用queue_work(struct workqueue_struct *wq, struct work_struct *work)向wq中加入工作节点。work会依次加在cwq->worklist所指向的链表中。queue_work向cwq->worklist中加入一个work节点,同时会调用wake_up来唤醒休眠在cwq->more_work上的worker_thread进程。wake_up会先调用wait节点上的autoremove_wake_function函数,然后将wait节点从cwq->more_work中移走。

        worker_thread再次被调度,开始处理cwq->worklist中的所有work节点...当所有work节点处理完毕,worker_thread重新将wait节点加入到cwq->more_work,然后再次休眠在该等待队列中直到Driver调用queue_work...

b. create_workqueue

 

 

 

       相对于create_singlethread_workqueue, create_workqueue同样会分配一个wq的工作队列,但是不同之处在于,对于多CPU系统而言,对每一个CPU,都会为之创建一个per-CPU的cwq结构,对应每一个cwq,都会生成一个新的worker_thread进程。但是当用queue_work向cwq上提交work节点时,是哪个CPU调用该函数,那么便向该CPU对应的cwq上的worklist上增加work节点。

c.小结
       当用户调用workqueue的初始化接口create_workqueue或者create_singlethread_workqueue对workqueue队列进行初始化时,内核就开始为用户分配一个workqueue对象,并且将其链到一个全局的workqueue队列中。然后Linux根据当前CPU的情况,为workqueue对象分配与CPU个数相同的cpu_workqueue_struct对象,每个cpu_workqueue_struct对象都会存在一条任务队列。紧接着,Linux为每个cpu_workqueue_struct对象分配一个内核thread,即内核daemon去处理每个队列中的任务。至此,用户调用初始化接口将workqueue初始化完毕,返回workqueue的指针。

        workqueue初始化完毕之后,将任务运行的上下文环境构建起来了,但是具体还没有可执行的任务,所以,需要定义具体的work_struct对象。然后将work_struct加入到任务队列中,Linux会唤醒daemon去处理任务。

       上述描述的workqueue内核实现原理可以描述如下:

 

 

3.2  创建工作
       要使用工作队列,首先要做的是创建一些需要推后完成的工作。可以通过DECLARE_WORK在编译时静态地建该结构:
       DECLARE_WORK(name,void (*func) (void *), void *data);
      这样就会静态地创建一个名为name,待执行函数为func,参数为data的work_struct结构。
      同样,也可以在运行时通过指针创建一个工作:
      INIT_WORK(structwork_struct *work, woid(*func) (void *), void *data);

4. 调度
a. schedule_work

       在大多数情况下, 并不需要自己建立工作队列,而是只定义工作, 将工作结构挂接到内核预定义的事件工作队列中调度, 在kernel/workqueue.c中定义了一个静态全局量的工作队列static struct workqueue_struct *keventd_wq;默认的工作者线程叫做events/n,这里n是处理器的编号,每个处理器对应一个线程。比如,单处理器的系统只有events/0这样一个线程。而双处理器的系统就会多一个events/1线程。
       调度工作结构, 将工作结构添加到全局的事件工作队列keventd_wq,调用了queue_work通用模块。对外屏蔽了keventd_wq的接口,用户无需知道此参数,相当于使用了默认参数。keventd_wq由内核自己维护,创建,销毁。这样work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。

b. schedule_delayed_work(&work,delay);
      有时候并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,同时也可以利用timer来进行延时调度,到期后才由默认的定时器回调函数进行工作注册。延迟delay后,被定时器唤醒,将work添加到工作队列wq中。

      工作队列是没有优先级的,基本按照FIFO的方式进行处理。

 5. 示例

[cpp] view plaincopy
  1. #include <linux/module.h>  
  2. #include <linux/init.h>  
  3. #include <linux/workqueue.h>  
  4.   
  5. static struct workqueue_struct *queue=NULL;  
  6. static struct work_struct   work;  
  7.   
  8. staticvoid work_handler(struct work_struct *data)  
  9. {  
  10.        printk(KERN_ALERT"work handler function.\n");  
  11. }  
  12.   
  13. static int __init test_init(void)  
  14. {  
  15.       queue=create_singlethread_workqueue("hello world");/*创建一个单线程的工作队列*/  
  16.       if (!queue)  
  17.             goto err;  
  18.   
  19.        INIT_WORK(&work,work_handler);  
  20.        schedule_work(&work);  
  21.   
  22.       return0;  
  23. err:  
  24.       return-1;  
  25. }  
  26.   
  27. static   void __exit test_exit(void)  
  28. {  
  29.        destroy_workqueue(queue);  
  30. }  
  31. MODULE_LICENSE("GPL");  
  32. module_init(test_init);  
  33. module_exit(test_exit);  



原创粉丝点击