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

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

工作队列是一种将工作交给其它线程执行的机制。也就是当线程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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容