Linux内核:工作队列

来源:互联网 发布:明治天皇 知乎 编辑:程序博客网 时间:2024/04/30 10:05

在我的上一篇文章Linux内核:中断、软中断、tasklet中,我们已经了解了中断底半部的两种实现方式,即软中断和tasklet微线程。但是这两种方式归根结底都是采用软中断机制的,其根本上还是在中断的上下文中执行,所以这也就要求了采用这两种方式编写中断底半部,不能出现一些可能导致程序休眠或者是延迟的函数(虽然当发生中断嵌套时会生成Ksoftirq线程,但这个是不确定的,所以我们在编写程序时,还是不能采用具有休眠或者延时的函数)。因为这样一种缺陷,所以我们的Linux设计师发明了一种新的将操作延迟的方法,那就是工作队列(workqueue)。由于工作队列是工作在一个内核线程上,因此其工作环境为进程的上下文,从而工作函数可以休眠任意时间。

对于每一个通过队列,内核都会为其创建一个新的内核守护线程,也就是说,每一个工作队列都有唯一的一个内核线程与其对应。工作时,该内核线程就会轮询地执行这个工作队列上所有的工作节点上对应的处理函数(这一点有点像tasklet,只不过现在是在一个线程上执行该工作队列),工作队列由一个workqueue_struct数据结构体描述。

/* * The externally visible workqueue abstraction is an array of * per-CPU workqueues: */struct workqueue_struct {unsigned intflags;/* I: WQ_* flags *///这个共用体表示该workqueue_struct属于哪个CPU的队列。union {struct cpu_workqueue_struct __percpu*pcpu;struct cpu_workqueue_struct*single;unsigned longv;} cpu_wq;/* I: cwq's */struct list_headlist;/* W: list of all workqueues *///用来连接work_struct的队列头struct mutexflush_mutex;/* protects wq flushing */intwork_color;/* F: current work color */intflush_color;/* F: current flush color */atomic_tnr_cwqs_to_flush; /* flush in progress */struct wq_flusher*first_flusher;/* F: first flusher */struct list_headflusher_queue;/* F: flush waiters */struct list_headflusher_overflow; /* F: flush overflow list */mayday_mask_tmayday_mask;/* cpus requesting rescue */struct worker*rescuer;/* I: rescue worker */intsaved_max_active; /* W: saved cwq max_active */const char*name;/* I: workqueue name */#ifdef CONFIG_LOCKDEPstruct lockdep_maplockdep_map;#endif};
workqueue_struct结构体比较复杂,一般没有必要了解所有成员的含义。在workqueue_struct涉及到一个cpu_workqueue_struct结构体,该结构体有什么用呢?

与原来的tasklet一样,一个工作队列也是只能工作在一个CPU上面的,即每一个CPU都有一个工作队列。而cpu_workqueue_sruct就是描述该CPU的工作队列的结构体。

/* * The per-CPU workqueue.  The lower WORK_STRUCT_FLAG_BITS of * work_struct->data are used for flags and thus cwqs need to be * aligned at two's power of the number of flag bits. */struct cpu_workqueue_struct {struct global_cwq*gcwq;/* I: the associated gcwq */struct workqueue_struct *wq;/* I: the owning workqueue ,指向属于该CPU的workqueue_struct结构体*/intwork_color;/* L: current color */intflush_color;/* L: flushing color */intnr_in_flight[WORK_NR_COLORS];/* L: nr of in_flight works */intnr_active;/* L: nr of active works */intmax_active;/* L: max active works */struct list_headdelayed_works;/* L: delayed works */};
了解了上面两个结构体之后,我们因该能够大致了解工作队列的工作机制,大体上与tasklet差不多。下面我们就来看一下工作队列最为重要的成员,work_struct。work_struct是工作队列里面的成员,里面会定义该work_struct的处理函数。

struct work_struct {atomic_long_t data;struct list_head entry;  //指向与其相邻的前后两个work_structwork_func_t func;  //该work_struct节点的处理函数。#ifdef CONFIG_LOCKDEPstruct lockdep_map lockdep_map;#endif};
下面我们再来看看工作队列的调度函数schedule_work(类似于 tasklet 的 schedule_tasklet 吧)。

/** * schedule_work - put work task in global workqueue * @work: job to be done * * Returns zero if @work was already on the kernel-global workqueue and * non-zero otherwise. * * This puts a job in the kernel-global workqueue if it was not already * queued and leaves it in the same position on the kernel-global * workqueue otherwise. */int schedule_work(struct work_struct *work){return queue_work(system_wq, work); //将一个新的work_struct添加进workqueue队列}
可以看到schedule_work的工作内容很简单,即将一个新的work_struct加入我们的工作队列(这里的工作队列是已经被创建的系统工作队列)上,这个也与我们之前的tasklet有些相似,只不过tasklet多了一个raise函数(将tasklet 软中断类型挂起)。工作队列由于只有这一个队列,所以是不需要有挂起函数这个操作的。由于该函数只有一句代码,所以我们在实际的使用过程中也可以直接使用queue_work函数。(一般自己创建的工作队列,在往工作队列加入work_struct时,需要使用到该queue_work函数,因为schedule_work函数默认是将work_struct加入系统队列中。)

了解完工作队列的基本操作方法之后呢?下面我要开始讲解如何执行一个工作队列了。前面我们了解到工作队列是由一个内核线程执行的,同时这个内核线程是具有CPU属性的,也就是说每一个CPU都有独属于自己的内核工作线程(当然这个线程需要我们自己创建的,在创建的时候我们也可以指定该内核线程属于哪个CPU。创建的方式后面会讲到)。创建完内核线程之后(这个时候内核线程其实已经被唤醒了,因为在创建内核线程的代码里面有唤醒该线程的函数),当然要执行该内核线程的内核函数,这个函数就是woker_thread,在该函数里面呢,woker_thread主要会调用run_workqueue函数,因为该函数负责执行工作队列里面的work_struct关联的函数。



通常我们如果要在驱动程序中使用工作队列时,一共有两种方式:

  1. 采用共享工作队列
  2. 自定义工作队列

采用共享工作队列

在Linux系统中,内核为了方便用户编程,已经默认实现了一个所有进程都可以使用的工作队列(其对应的内核线程是kevent线程,其在Linux启动时创建,该线程被创建之后就处于sleep状态,当我们使用schedule_work函数时,才会唤醒该线程,当工作队列上的所有节点被执行完毕,该线程又会处于休眠状态,知道schedule_work再次被调用)。因此采用共享工作队列,在用户的实现上是非常简单的。
第一步:编写自己的 work_struct 工作函数。
第二步:定义自己的 work_struct 结构体。
第三步:初始化work_struct结构体,使工作函数地址指向work_struct ->func
第四步:可以在适当位置使用schedule_work函数完成向系统工作队列添加自己的work_struct 


采用自定义工作队列

采用共享工作队列会有一个弊端,因为毕竟共享队列采用的是kevent线程,系统里面的其它工作也会使用到该共享队列。如果我们在该工作队列加入太多耗时的程序,无疑会降低系统性能,因此一般在驱动程序中,我们会偏向于使用自定义工作队列,采用自定义工作队列也比较简单,相对于共享工作队列,这里多了一个创建自定义工作的函数,即:create_queue函数(注意这个函数会在每一个CPU上都创建一个一个工作队列和相应的线程,这未免太过于消耗资源,因此我们还可以采用在某一指定的CPU上创建一个工作队列,例如采用create_singlethread_workqueue函数,就会在编号为第一个的CPU上创建内核线程和工作队列。)对于自定义的工作队列,在这里我们不能使用schedule_struct函数将work_struct添加进工作队列了,这是因为schedule_struct函数只能往共享工作队列上添加工作节点(work_struct),所以我们必须要采用queue_work 函数。


0 0