带您进入内核开发的大门 | 内核中的工作队列

639 阅读6分钟

配套的代码可以从本号的github下载,github.com/shuningzhan… 文本有些图片来自网络,在此表示感谢,如有侵权请联系删除。

工作队列是一种将工作交给其它线程执行的机制。也就是当线程A期望做某件事,但自己由不想做,或者不能做的情况下,它可以将该事情(工作 work)加入到一个队列当中,然后有后台线程会从队列中获取该工作,并执行该工作。 这里的的其它线程可以自己创建,也可以不用自己创建。因为,在操作系统起来的时候在每个CPU上都创建了一组工作线程,并创建了默认工作队列。如图通过ps命令可以看到内核创建的工作线程。

1.png

由于Linux内核默认为我们做了很多工作,因此在常规情况下工作队列的使用非常简单。我们这里先看一下最简单情况下如何使用工作队列机制。本文的介绍从4个方面进行,分别如下:

  1. 基本接口的介绍
  2. 基本功能的应用示例
  3. 工作队列的实现原理
  4. 高级功能的简介

基本接口

在具体使用之前,我们先了解一下提供给我们的接口有那些。首先我们看一下涉及到的数据结构,了解了数据结构,才能比较容易的理解如何使用工作队列。从使用层面上来说,我们主要关注如下数据结构,这个数据结构代表一项工作。

typedef void (*work_func_t)(struct work_struct *work);
struct work_struct {
    atomic_long_t data; /* 内核内部使用 */
    struct list_head entry; /* 用于链接到工作队列中*/
    work_func_t func; /* 工作函数*/
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

这里有一点需要说明的是在结构体中有一个函数指针成员,这个是执行具体的工作的实现。Linux内核将工作队列设计为一个通用的机制。

应用示例

工作队列是应用很灵活,我们可以定义自己的工作队列,或者使用操作系统内核预定义的工作队列。这里我们先给出一个最简单的工作队列的实例,这个实例借用内核预定义的工作队列。在这个示例中,我们启动了一个线程,然后定时将工作放入工作队列中。工作队列接收到任务后执行该任务。任务的具体内容也很简单,只是打印一个消息。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mm.h>

#include <linux/in.h>
#include <linux/inet.h>
#include <linux/socket.h>
#include <net/sock.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/workqueue.h>

#define BUF_SIZE 1024
struct task_struct *main_task;

/* 定义自己的工作结构体,用于描述工作,
 * 这里需要包含系统提供的work_struct结构体
 * 作为其第一个成员。 */
struct my_work {
	struct work_struct w;
	int data;
};

/* 实例化我们需要做的工作 */
static struct my_work real_work;

static inline void sleep(unsigned sec)
{
	__set_current_state(TASK_INTERRUPTIBLE);
	schedule_timeout(sec * HZ);
}

/* 工作函数,上述工作的具体工作由该函数完成,这里
 * 只是一个简单的示例,仅仅打印一行文本 ,实际上
 * 可以做很多事情。*/
static void my_work_func(struct work_struct *work)
{
	struct my_work *pwork;
	
	/* 这里使用了一个系统函数,用于根据成员的指针
	 * 获得父结构体的指针。 */
	pwork = container_of(work, struct my_work, w);

	printk(KERN_NOTICE "Do something %d\n", pwork->data);
}

/* 作为独立的线程,每隔1秒对工作数据进行调整,并加入
 * 到工作队列中。 */
static int multhread_server(void *data)
{
	int index = 0;
	/* 初始化一个工作,关键是初始化该工作的执行函数 */
	INIT_WORK(&real_work.w, my_work_func);

	while (!kthread_should_stop()) {
		printk(KERN_NOTICE "server run %d\n", index);
		real_work.data = index;
		
		/* 调度工作,本质是将工作放入工作队列当中。  */
		if (schedule_work(&real_work.w) == 0) {
			printk(KERN_NOTICE "Schedule work failed!\n");
		}
		index ++;
		sleep(1);
	}

	return 0;
}


static int multhread_init(void)
{
	ssize_t ret = 0;

	printk("Hello, workqueue \n");
	main_task = kthread_run(multhread_server,
				  NULL,
				  "multhread_server");
	if (IS_ERR(main_task)) {
		ret = PTR_ERR(main_task);
		goto failed;
	}

failed:
	return ret;
}

static void multhread_exit(void)
{
	printk("Bye!\n");
	kthread_stop(main_task);

}

module_init(multhread_init);
module_exit(multhread_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("SunnyZhang<shuningzhang@126.com>");

基本原理

下面这张图是借用的魅族内核团队博客的,这张图非常清晰的解释了工作队列的架构和数据走向。

工作队列原理
在这里我们先解释一下这张图中比较重要的几个概念: work :工作,这个就是具体要做的事情,通过上文中介绍的结构体表示。 workqueue :工作的集合。workqueue 和 work 是一对多的关系。 worker :工人。在代码中 worker 对应一个 work_thread() 内核线程。 worker_pool:工人的集合。worker_pool 和 worker 是一对多的关系。 pwq(pool_workqueue):中间人 / 中介,负责建立起 workqueue 和 worker_pool 之间的关系。workqueue 和 pwq 是一对多的关系,pwq 和 worker_pool 是一对一的关系。

实际上我们可以将工作队列理解为一个计算集群,工作就是任务,我们将工作提交给工作队列相当于将任务提交给集群。工作队列再将任务根据负载情况分配给具体的工人执行(工作队列线程)。

能力增强

前面介绍的工作队列是借用的内核预创建的线程池,这个是所有人公用的。如果在负载较大的情况下可能会影响任务执行的效率。内核提供了另外的增强功能,用户可以自己创建线程池,这样就可以用独立的线程池处理任务,从而保证任务执行效率。下面这个函数用来创建一个

#define create_workqueue(name)                      \
    alloc_workqueue((name), WQ_MEM_RECLAIM, 1)

#define create_singlethread_workqueue(name)             \
    alloc_ordered_workqueue("%s", WQ_MEM_RECLAIM, name)

这两个宏都会返回一个workqueue_struct结构体的指针,并且都会创建进程(“内核线程”)来执行加入到这个workqueue的work。 create_workqueue:多核CPU,这个宏,会在每个CPU上创建一个专用线程。 create_singlethread_workqueue:单核还是多核,都只在其中一个CPU上创建线程。 核心实现在函数alloc_workqueue和alloc_ordered_workqueue中,我们以前者为例进行介绍。该函数也是一个宏定义,具体定义如下,这里并没有做实质性的工作,是另外一个宏定义。

#ifdef CONFIG_LOCKDEP
#define alloc_workqueue(fmt, flags, max_active, args...)		\
({									\
	static struct lock_class_key __key;				\
	const char *__lock_name;					\
									\
	__lock_name = #fmt#args;					\
									\
	__alloc_workqueue_key((fmt), (flags), (max_active),		\
			      &__key, __lock_name, ##args);		\
})
#else
#define alloc_workqueue(fmt, flags, max_active, args...)		\
、、	
__alloc_workqueue_key((fmt), (flags), (max_active),		\
			      NULL, NULL, ##args)
#endif

我们在进一步看__alloc_workqueue_key函数的定义,这里删除了其它冗余的内容,从函数定义可以看出这里主要是创建工作队列结构体和启动了独立的线程。该函数最终返回创建的workqueue_struct结构体,而后面就可以向该队列发送工作了。

struct workqueue_struct *__alloc_workqueue_key(const char *fmt,
					       unsigned int flags,
					       int max_active,
					       struct lock_class_key *key,
					       const char *lock_name, ...)
{
        wq = kzalloc(sizeof(*wq) + tbl_size, GFP_KERNEL);
        ... ...
	if (flags & WQ_MEM_RECLAIM) {
		struct worker *rescuer;

		rescuer = alloc_worker(NUMA_NO_NODE);
		if (!rescuer)
			goto err_destroy;

		rescuer->rescue_wq = wq;
                /*其实这个核心就是创建一个独立的线程*/
		rescuer->task = kthread_create(rescuer_thread, rescuer, "%s",
					       wq->name);
		if (IS_ERR(rescuer->task)) {
			kfree(rescuer);
			goto err_destroy;
		}

		wq->rescuer = rescuer;
		rescuer->task->flags |= PF_NO_SETAFFINITY;
		wake_up_process(rescuer->task);
	}

	... ...
}

发送工作的函数定义如下,可以看出来这里有2个参数,分别是目的工作队列和希望完成的工作。

bool queue_work(struct workqueue_struct *wq,struct work_struct *work);
bool queue_delayed_work(struct workqueue_struct *wq,
                      struct delayed_work *dwork,
                      unsigned long delay);

最后我们把工作队列涉及到的接口贴到下面,方便大家学习查找。

1.png