LINUX中的工作队列

工作队列一般用来做滞后的工作,比如在中断里面要做很多事,但是比较耗时,这时就可以把耗时的工作放到工作队列。说白了就是系统延时调度的一个自定义函数。

 

工作队列提供一个通用的办法将任务延迟到 bottom halves(底半部分) 处于核心的是工作队列(结构体 workqueue_struct 任务被安排到该结构体当中。 任务由结构体 work_struct 来说明 用来鉴别哪些任务被延迟以及使用哪个延迟函数(参见  3)。 events/X 内核线程(每 CPU 一个)从工作队列中抽取任务并激活一个 bottom-half 处理程序(由处理程序函数在结构体work_struct 中指定)。

 3. 工作队列背后的处理过程

Linux中的工作队列 - Tanatseng - Tanatseng Blog

由于 work_struct 中指出了要采用的处理程序函数, 因此可以利用工作队列来为不同的处理程序进行任务排队。 现在,让我们看一下能够用于工作队列的 API 函数。

 

工作队列 API:

工作队列 API 比微线程稍复杂,主要是因为它支持很多选项。 我们首先探讨一下工作队列,然后再看一下任务和变体。

通过  3 可以回想工作队列的核心结构体是队列本身 该结构体用于将任务安排出 top half ,进入 bottom half ,从而延迟它的执行 工作队列通过宏调用生成 create_workqueue,返回一个 workqueue_struct 参考值。当用完一个工作队列,可以通过调用函数 destroy_workqueue 来去掉它(如果需要):

struct workqueue_struct *create_workqueue( name );

struct workqueue_struct *create_singlethread_workqueue(const char *name);

void destroy_workqueue( struct workqueue_struct * );

 

一个工作队列必须明确的在使用前创建,若使用 create_workqueue, 就得到一个工作队列它在系统的每个处理器上有一个专用的线程。在很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用 create_singlethread_workqueue 来创建工作队列。

通过工作队列与之通信的任务可以由结构体 work_struct 来定义 通常,该结构体是用来进行任务定义的结构体的第一个元素(后面有相关例子)。 工作队列 API 提供三个函数来初始化任务(通过一个事先分配的缓存); 参见 清单 6  INIT_WORK 提供必需的初始化数据以及处理程序函数的配置(由用户传递进来)。 如果开发人员需要在任务被排入工作队列之前发生延迟,可以使用宏 INIT_DELAYED_WORK  INIT_DELAYED_WORK_DEFERRABLE

清单 6. 任务初始化宏:               

INIT_WORK( work, func );

INIT_DELAYED_WORK( work, func );

INIT_DELAYED_WORK_DEFERRABLE( work, func );

 

INIT_* 做更加全面的初始化结构的工作,在第一次建立结构时使用。

任务结构体的初始化完成后,接下来要将任务安排进工作队列。 可采用多种方法来完成这一操作(参见 清单 7)。 首先,利用 queue_work 简单地将任务安排进工作队列(这将任务绑定到当前的 CPU)。 或者,可以通过 queue_work_on 来指定处理程序在哪个 CPU 上运行。 两个附加的函数为延迟任务提供相同的功能(其结构体装入结构体 work_struct 之中,并有一个 计时器用于任务延迟 )。

 

清单 7. 工作队列函数:              

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

int queue_work_on( int cpu, struct workqueue_struct *wq, struct work_struct *work );

int queue_delayed_work( struct workqueue_struct *wq,

                     struct delayed_work *dwork, unsigned long delay );

int queue_delayed_work_on( int cpu, struct workqueue_struct *wq,

                     struct delayed_work *dwork, unsigned long delay );

 

每个都添加work到给定的workqueue。如果使用 queue_delay_work, 则实际的工作至少要经过指定的 jiffies 才会被执行。 这些函数若返回 1 则工作被成功加入到队列若为0,则意味着这个work 已经在队列中等待,不能再次加入。

可以使用全局的内核全局工作队列,利用 4 个函数来为工作队列定位。 这些函数(见 清单 8)模拟 清单 7,只是不需要定义工作队列结构体。

 

清单 8. 内核全局工作队列函数:       

int schedule_work( struct work_struct *work );

int schedule_work_on( int cpu, struct work_struct *work );

int scheduled_delayed_work( struct delayed_work *dwork, unsigned long delay );

int scheduled_delayed_work_on(

              int cpu, struct delayed_work *dwork, unsigned long delay );

 

还有一些帮助函数用于清理或取消工作队列中的任务。想清理特定的任务项目并阻塞任务, 直到任务完成为止, 可以调用 flush_work 来实现。 指定工作队列中的所有任务能够通过调用flush_workqueue 来完成。 这两种情形下,调用者阻塞直到操作完成为止。 为了清理内核全局工作队列,可调用 flush_scheduled_work

 

int flush_work( struct work_struct *work );

int flush_workqueue( struct workqueue_struct *wq );

void flush_scheduled_work( void );

 

 flush_workqueue 返回后没有在这个调用前提交的函数在系统中任何地方运行。

还没有在处理程序当中执行的任务可以被取消。 调用 cancel_work_sync 将会终止队列中的任务或者阻塞任务直到回调结束(如果处理程序已经在处理该任务)。 如果任务被延迟,可以调用 cancel_delayed_work_sync

 

int cancel_work_sync( struct work_struct *work );

int cancel_delayed_work_sync( struct delayed_work *dwork );

 

最后,可以通过调用 work_pending 或者 delayed_work_pending 来确定任务项目是否在进行中。

 

work_pending( work );

delayed_work_pending( work );

 

这就是工作队列 API 的核心。在 ./kernel/workqueue.c 中能够找到工作队列 API 的实现方法, API  ./include/linux/workqueue.h 中定义。 下面我们看一个工作队列 API 的简单例子。

 

工作队列简单例子

 

下面的例子说明了几个核心的工作队列 API 函数。 如同微线程的例子一样,为方便起见,可将这个例子部署在内核模块上下文。

 

首先,看一下将用于实现 bottom half 的任务结构体和处理程序函数(参见 清单 9)。 首先您将注意到工作队列结构体参考的定义 my_wq)以及 my_work_t 的定义。 my_work_t 类型定义的头部包括结构体 work_struct 和一个代表任务项目的整数。 处理程序(回调函数)将 work_struct 指针引用改为 my_work_t 类型。 发送出任务项目(来自结构体的整数)之后,任务指针将被释放。

 

清单 9. 任务结构体和 bottom-half 处理程序

                           

#include <linux/kernel.h>

#include <linux/module.h>

#include <linux/workqueue.h>

MODULE_LICENSE("GPL");

static struct workqueue_struct *my_wq;

typedef struct {

  struct work_struct my_work;

  int    x;

} my_work_t;

my_work_t *work, *work2;

static void my_wq_function( struct work_struct *work)

{

  my_work_t *my_work = (my_work_t *)work;

  printk( "my_work.x %d\n", my_work->x );

  kfree( (void *)work );

  return;

}

 

清单 10  init_module 函数, 该函数从使用 create_workqueue API 函数生成工作队列开始。 成功生成工作队列之后,创建两个任务项目(通过 kmalloc 来分配)。 利用 INIT_WORK 来初始化每个任务项目,任务定义完成, 接着通过调用 queue_work 将任务安排到工作队列中。 top-half 进程(在此处模拟)完成。如同清单 10 中所示,任务有时会晚些被处理程序处理。


清单 10. 工作队列和任务创建:                   

int init_module( void )

{

  int ret;

 

  my_wq = create_workqueue("my_queue");

  if (my_wq) {

 

   /* Queue some work (item 1) */

    work = (my_work_t *)kmalloc(sizeof(my_work_t), GFP_KERNEL);

    if (work) {

      INIT_WORK( (struct work_struct *)work, my_wq_function );

      work->x = 1;

      ret = queue_work( my_wq, (struct work_struct *)work );

    }

 

    /* Queue some additional work (item 2) */

    work2 = (my_work_t *)kmalloc(sizeof(my_work_t), GFP_KERNEL);

    if (work2) {

      INIT_WORK( (struct work_struct *)work2, my_wq_function );

      work2->x = 2;

      ret = queue_work( my_wq, (struct work_struct *)work2 );

    }

  }

  return 0;

}

 

最终的元素在 清单 11 中展示。 在模块清理过程中,会清理一些特别的工作队列(它们将保持阻塞状态直到处理程序完成对任务的处理), 然后销毁工作队列。

 

清单 11. 工作队列清理和销毁

                           

void cleanup_module( void )

{

  flush_workqueue( my_wq );

  destroy_workqueue( my_wq );

  return;

}

 

参考:

内核的工作队列使用方法

工作队列一般用来做滞后的工作,比如在中断里面要做很多事,但是比较耗时,这时就可以把耗时的工作放到工作队列。说白了就是系统延时调度的一个自定义函数。

 

1、定义struct work_struct irq_queue;

2、初始化INIT_WORK(&irq_queue,do_irq_queuework);

3、调用方法:schedule_work(&rq_queue);

 

注,调用完毕后系统会释放此函数,所以如果想再次执行的话,就再次调用schedule_work()即可。

另外,内核必须挂载文件系统才可以使用工作队列。我的理解是:工作队列也属于调度,如果内核挂了,他就不调度了,当然就不能用工作队列了。

 

工作队列接口

  工作队列接口是在2.5的开发过程中引入的,用于取代任务队列接口(用于调度内核任务)。每个工作队列有一个专门的线程,

所有来自运行队列的任务在进程的上下文中运行(这样它们可以休眠)。驱动程序可以创建并使用它们自己的工作队列,或者使用内核的一个工作队列。

工作队列用以下方式创建:

  struct workqueue_struct *create_workqueue(const char *name); 在这里 name 是工作队列的名字。

  工作队列任务可以在编译时或者运行时创建。任务需要封装为一个叫做 work_struct 的结构体。在编译期初始化一个工作队列任务时要用到:

  DECLARE_WORK(name, void (*function)(void *), void *data); 在这里 name  work_struct 的名字,function 是当任务被调度时调用的函数,data 是指向那个函数的指针。

  在运行期初始化一个工作队列时要用到:

  INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);

    用下面的函数调用来把一个作业(一个类型为work_struct 结构的工作队列作业/任务)加入到工作队列中:

  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_delay_work()中指定delay,是为了保证至少在经过一段给定的最小延迟时间以后,工作队列中的任务才可以真正执行。

 

  工作队列中的任务由相关的工作线程执行,可能是在一个无法预期的时间(取决于负载,中断等等),或者是在一段延迟以后。任何一个在工作队列中等待了无限长的时间也没有运行的任务可以用下面的方法取消:

  int cancel_delayed_work(struct work_struct *work);

 

    如果当一个取消操作的调用返回时,任务正在执行中,那么这个任务将继续执行下去,但不会再加入到队列中。清空工作队列中的所有任务使用:

  void flush_workqueue(struct workqueue_struct *queue);

   

    销毁工作队列使用:

  void destroy_workqueue(struct workqueue_struct *queue);

    不是所有的驱动程序都必须有自己的工作队列。驱动程序可以使用内核提供的缺省工作队列。由于这个工作队列由很多驱动程序共享,

任务可能会需要比较长一段时间才能开始执行。为了解决这一问题,工作函数中的延迟应该保持最小或者干脆不要。

 

  需要特别注意的是缺省队列对所有驱动程序来说都是可用的,但是只有经过GP许可的驱动程序可以用自定义的工作队列:

  int schedule_work(struct work_struct *work); -- 向工作队列中添加一个任务

    int schedule_delayed_work(struct work_struct *work, unsigned long delay); -- 向工作队列中添加一个任务并延迟执行

  当模块被缷载时应该去调用一个 flash_scheduled_work() 函数,这个函数会使等待队列中所有的任务都被执行。


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

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

2.2    工作队列的组织结构
2.2.1      工作队列workqueue_struct
外部可见的工作队列抽象,用户接口,是由每个CPU的工作队列组成的链表

  64struct workqueue_struct {

  65        struct cpu_workqueue_struct *cpu_wq;

  66        const char *name;

  67        struct list_head list;  /* Empty if single thread */

  68};

²      cpu_wq:本队列包含的工作者线程;

²      name:所有本队列包含的线程的公共名称部分,创建工作队列时的唯一用户标识;

²      list:链接本队列的各个工作线程。

在早期的版本中,cpu_wq是用数组维护的,即对每个工作队列,每个CPU包含一个此线程。改成链表的优势在于,创建工作队列的时候可以指定只创建一个内核线程,这样消耗的资源较少。

在该结构体里面,给每个线程分配一个cpu_workqueue_struct,因而也就是给每个处理器分配一个,因为每个处理器都有一个该类型的工作者线程。

2.2.2      工作者线程cpu_workqueue_struct
这个结构是针对每个CPU的,属于内核维护的结构,用户不可见。

  43struct cpu_workqueue_struct {

  45        spinlock_t lock;

  47        long remove_sequence;   /* Least-recently added (next to run) */

  48        long insert_sequence;   /* Next to add */

  50        struct list_head worklist;

  51        wait_queue_head_t more_work;

  52        wait_queue_head_t work_done;

  54        struct workqueue_struct *wq;

  55        struct task_struct *thread;

  57        int run_depth;          /* Detect run_workqueue() recursion depth */

  58} ____cacheline_aligned;

²      lock:操作该数据结构的互斥锁

²      remove_sequence:下一个要执行的工作序号,用于flush

²      insert_sequence:下一个要插入工作的序号

²      worklist:待处理的工作的链表头

²      more_work:标识有工作待处理的等待队列,插入新工作后唤醒对应的内核线程

²      work_done:处理完的等待队列,没完成一个工作后,唤醒可能等待通知处理完成通知的线程

²      wq:所属的工作队列节点

²      thread:关联的内核线程指针

²      run_depth:run_workqueue()循环深度,多处可能调用此函数

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




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值