wozijisunfly 2018-11-12
在我的文章Linux内核:软中断、tasklet中,我们已经了解了中断底半部的两种实现方式,即软中断和tasklet微线程。但是这两种方式归根结底都是采用软中断机制的,其根本上还是在中断的上下文中执行,所以这也就要求了采用这两种方式编写中断底半部,不能出现一些可能导致程序休眠或者是延迟的函数(虽然当发生中断嵌套时会生成Ksoftirq线程,但这个是不确定的,所以我们在编写程序时,还是不能采用具有休眠或者延时的函数)。因为这样一种缺陷,所以我们的Linux设计师发明了一种新的将操作延迟的方法,那就是工作队列(workqueue)。由于工作队列是工作在一个内核线程上,因此其工作环境为进程的上下文,从而工作函数可以休眠任意时间。
那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。
对于每一个工作队列,内核都会为其创建一个新的内核守护线程,也就是说,每一个工作队列都有唯一的一个内核线程与其对应。工作时,该内核线程就会轮询地执行这个工作队列上所有的工作节点上对应的处理函数(这一点有点像tasklet,只不过现在是在一个线程上执行该工作队列),工作队列由一个workqueue_struct数据结构体描述。
首先要注意本文的两个概念:(1)使用内核提供的工作队列, (2)自己创建工作队列
/*
* 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_LOCKDEP
struct lockdep_maplockdep_map;
#endif
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 */
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
了解了上面两个结构体之后,我们因该能够大致了解工作队列的工作机制,大体上与tasklet差不多。下面我们就来看一下工作队列最为重要的成员----工作,work_struct。work_struct是工作队列里面的成员,里面会定义该work_struct的处理函数。
struct work_struct {
atomic_long_t data;
struct list_head entry; //指向与其相邻的前后两个work_struct
work_func_t func; //该work_struct节点的处理函数。
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
1
2
3
4
5
6
7
8
9
这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。
实现原理
工作队列的组织结构
即workqueue_struct、cpu_workqueue_struct与work_struct的关系。
一个工作队列对应一个work_queue_struct,工作队列中每cpu的工作队列由cpu_workqueue_struct表示,而work_struct为其上的具体工作。
关系如下图所示:
相关API
创建一个队列就会有一个内核线程,一般不要轻易创建队列
位于进程上下文--->可以睡眠
定义:
struct work_struct work;
初始化:
INIT_WORK(struct work_struct *work, void (*func)(struct work_struct *work));
定义并初始化:
DECLARE_WORK(name, void (*func)(struct work_struct *work));
===========================================================
共享队列:
调度:
int schedule_work(struct work_struct *work);
返回1成功, 0已经添加在队列上
延迟调度:
int schedule_delayed_work(struct work_struct *work, unsigned long delay);
===========================================================
自定义队列:
创建新队列和新工作者线程:
struct workqueue_struct *create_workqueue(const char *name);
调度指定队列:
int queue_work(struct workqueue_struct *wq, struct work_struct *work);
延迟调度指定队列:
int queue_delayed_work(struct workqueue_struct *wq,
struct work_struct *work, unsigned long delay);
销毁队列:
void destroy_workqueue(struct workqueue_struct *wq);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
通常我们如果要在驱动程序中使用工作队列时,一共有两种方式:
采用共享工作队列
自定义工作队列
采用共享工作队列
在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_work函数只能往共享工作队列上添加工作节点(work_struct),所以我们必须要采用queue_work 函数。
接下来,我们来实现这个demo:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/fb.h>
#include <linux/backlight.h>
#include <linux/err.h>
#include <linux/pwm.h>
#include <linux/slab.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <linux/gpio.h>
#include <mach/gpio.h>
#include <plat/gpio-cfg.h>
#include <linux/timer.h> /*timer*/
#include <asm/uaccess.h> /*jiffies*/
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
struct tasklet_struct task_t ;
struct workqueue_struct *mywork ;
//定义一个工作队列结构体
struct work_struct work;
static void task_fuc(unsigned long data)
{
if(in_interrupt()){
printk("%s in interrupt handle!",__FUNCTION__);
}
}
//工作队列处理函数
static void mywork_fuc(struct work_struct *work)
{
if(in_interrupt()){
printk("%s in interrupt handle!",__FUNCTION__);
}
msleep(2);
printk("%s in process handle!",__FUNCTION__);
}
static irqreturn_t irq_fuction(int irq, void *dev_id)
{
tasklet_schedule(&task_t);
//调度工作
schedule_work(&work);
if(in_interrupt()){
printk("%s in interrupt handle!",__FUNCTION__);
}
printk("key_irq:%d",irq);
return IRQ_HANDLED ;
}
static int __init tiny4412_Key_irq_test_init(void)
{
int err = 0 ;
int irq_num1 ;
int data_t = 100 ;
//创建新队列和新工作者线程
mywork = create_workqueue("my work");
//初始化
INIT_WORK(&work,mywork_fuc);
//调度指定队列
queue_work(mywork,&work);
tasklet_init(&task_t,task_fuc,data_t);
printk("irq_key init");
irq_num1 = gpio_to_irq(EXYNOS4_GPX3(2));
err = request_irq(irq_num1,irq_fuction,IRQF_TRIGGER_FALLING,"tiny4412_key1",(void *)"key1");
if(err != 0){
free_irq(irq_num1,(void *)"key1");
return -1 ;
}
return 0 ;
}
static void __exit tiny4412_Key_irq_test_exit(void)
{
int irq_num1 ;
printk("irq_key exit");
irq_num1 = gpio_to_irq(EXYNOS4_GPX3(2));
//销毁一条工作队列
destroy_workqueue(mywork);
free_irq(irq_num1,(void *)"key1");
}
module_init(tiny4412_Key_irq_test_init);
module_exit(tiny4412_Key_irq_test_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YYX");
MODULE_DESCRIPTION("Exynos4 KEY Driver");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
工作队列是一种将工作推后执行的形式,交由一个内核线程去执行在进程上下文执行,其不能访问用户空间。最重要特点的就是工作队列允许重新调度甚至是睡眠。工作队列子系统提供了一个默认的工作者线程来处理这些工作。默认的工作者线程叫做events/n,这里n是处理器的编号,每个处理器对应一个线程,也可以自己创建工作者线程。