带您进入内核开发的大门 | 内核中的等待队列

配套的代码可以从本号的github下载: https://github.com/shuningzhang/linux_kernel
内核相关电子书可以在这里下载: https://u19702000.ctfile.com/dir/19702000-33344559-0b7371/

等待队列是一种基于资源状态的线程管理的机制,它可以使线程在资源不满足的情况下处于休眠状态,让出CPU资源,而资源状态满足时唤醒线程,使其继续进行业务的处理。
等待队列(wait queue)用于使线程等待某一特定的事件发生而无需频繁的轮询,进程在等待期间睡眠,在某件事发生时由内核自动唤醒。它是以双循环链表为基础数据结构,与进程的休眠唤醒机制紧密相联,是实现异步事件通知、跨进程通信、同步资源访问等技术的底层技术支撑。

基本接口

wait_queue_head_t
使用等待队列时,最基本的数据结构是struct wait_queue_head_t,也就是等待队列头,这个可以理解为等待队列的实体。队列头中包含一个双向链表,用于记录在该等待队列中处于等待状态的线程等信息。该结构体的定义如下:

struct __wait_queue_head {
    spinlock_t        lock;  //用于互斥访问的自旋锁
    struct list_head    task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

可以通过宏定义 DECLARE_WAIT_QUEUE_HEAD直接定义一个队列头变量,并完成初始化,该宏定义如下:

#define DECLARE_WAIT_QUEUE_HEAD(name) \
    struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
    
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {                    \
    .lock        = __SPIN_LOCK_UNLOCKED(name.lock),            \
    .head        = { &(name).head, &(name).head } }

或者是通过结构体wait_queue_head_t定义后,调用函数init_waitqueue_head进行初始化。虽然方式不同,但基本原理是一样的,主要是对结构体内自旋锁和链表的初始化。

wait_event
函数wait_event用于在某个线程中调用,当调用该函数时,如果参数中的条件不满足,则该线程会进入休眠状态。下面代码是该函数的定义:

#define wait_event(wq, condition)                    \
do {                                    \
    if (condition)                            \
        break;                            \
    __wait_event(wq, condition);                    \
} while (0)

#define __wait_event(wq, condition)                    \
    (void)___wait_event(wq, condition, TASK_UNINTERRUPTIBLE, 0, 0, schedule())

wake_up
函数wake_up用于对处于阻塞状态的线程进行唤醒,其参数就是队列头。如下是该函数的定义,我们这里暂时不展开介绍。

#define wake_up(x)            __wake_up(x, TASK_NORMAL, 1, NULL)

了解了上面1个数据结构及相关函数后就可以使用等待队列了,当然只是基本的使用。

示例程序

我们这里给出一个示例程序,程序很简单。示例程序中有2个线程,分别是服务线程和客户线程。其中服务线程起来后会检查条件是否满足,并视情况进入休眠状态。而客户进程会每隔5秒将条件变成可用状态,并唤醒服务线程。

/* 这个例程用于说明等待队列的用法,在本例程中有2个线程,分别是
 * 客户端和服务端。逻辑很简单,服务线程起来的时候会等待事件发生
 * 并阻塞,客户端每隔5秒中唤醒一次服务端。*/
#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/wait.h>

#define BUF_SIZE 1024

struct task_struct *main_task;
struct task_struct *client_task;
wait_queue_head_t wqh;

/* 这个结构体用于在线程之间共享数据 */
struct thread_stat
{
        int t_can_run;
};

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

static int multhread_server(void *data)
{
        int index = 0;
        struct thread_stat* ts = (struct thread_stat*) data;

        while (!kthread_should_stop()) {
                printk(KERN_NOTICE "server run %d\n", index);
                index ++; 
                /*在这里等待事件, 线程被阻塞在这里。 */
                wait_event(wqh, ts->t_can_run || kthread_should_stop());
                printk(KERN_NOTICE "server event over!\n");
                ts->t_can_run = 0;
        }

        printk(KERN_NOTICE "server thread end\n");
        return 0;
}
static int multhread_init(void)
{
        ssize_t ret = 0;
        struct thread_stat thread_s;
        thread_s.t_can_run = 0;



        printk("Hello, multhread \n");
        /* 初始化等待队列头 */
        init_waitqueue_head(&wqh);

        /* 分别启动2个线程 */
        main_task = kthread_run(multhread_server,
                                &thread_s,
                                "multhread_server");
        if (IS_ERR(main_task)) {
                ret = PTR_ERR(main_task);
                goto failed;
        }

        client_task = kthread_run(multhread_client,
                                  &thread_s,
                                  "multhread_client");
        if (IS_ERR(client_task)) {
                ret = PTR_ERR(client_task);
                goto client_failed;
        }

        return ret;
client_failed:
        kthread_stop(main_task);

failed:
        return ret;
}

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

module_init(multhread_init);
module_exit(multhread_exit);

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

等待队列的原理

关于等待队列的原理,有3点需要重点说明,理解了这几点,也就能够比较清晰的理解等待队列的原理。这3点分别是数据结构、等待函数和唤醒函数
我们这里还是从结构体说起。这里主要有2个结构体,前面已经有所介绍。其中wait_queue_head是等待队列头,定义如下:

struct wait_queue_head {
        spinlock_t              lock;
        struct list_head        head;
};

这里主要是双向链表,所有处于等待状态的线程都被加入到该双向链表中。等后续唤醒时根据该链表中的数据进行唤醒。另外一个数据结构是wait_queue_entry,该结构体是一个等待项,这个结构体对于普通用户通常不必关系,因为内核的API对其进行了封装。

struct wait_queue_entry {
        unsigned int            flags;
        void                    *private;
        wait_queue_func_t       func;
        struct list_head        entry;
};

其中前一个结构体的head成员和后一个结构体的entry成员配合,形成所谓的双向链表。我们先看一下其大概的结构,具体如下图所示。

1.png

关于等待函数
关于等待函数,前面给出了一部分定义,下面我们继续深入介绍。在介绍之前,我们先介绍一下其大概流程,本质上就是将当前线程状态设置为TASK_UNINTERRUPTIBLE状态,然后调用schedule函数将本线程调度出去。理解了这个原理,代码就很容易理解,下面是函数的实现:

#define __wait_event(wq_head, condition)                                        \
        (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0,     \
                            schedule())

直接调用的___wait_event函数,注意观察一下这个函数的几个参数,其中TASK_UNINTERRUPTIBLE是目标状态,而schedule则是在内部要调用的函数。

#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd)           \
({                                                                              \
        __label__ __out;                                                        \
        struct wait_queue_entry __wq_entry;                                     \
        long __ret = ret;       /* explicit shadow */                           \
        /* 这里初始化了前文所说的第二个结构体,也就是等待队列项 */    \
        init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0);        \
        for (;;) {                                                              \
                /* 这个函数设置线程状态,并将等待队列项添加到等待队列中
                */
                long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
                /* 满足条件的情况下退出等待 */                        \
                if (condition)                                                  \
                        break;                                                  \
                                                                                \
                if (___wait_is_interruptible(state) && __int) {                 \
                        __ret = __int;                                          \
                        goto __out;                                             \
                }                                                               \
                /* 将线程调度出去 */                                   \
                cmd;                                                            \
        }                                         \
        /*将状态重新设置为TASK_RUNNING,并将队列项移出 */                      \
        finish_wait(&wq_head, &__wq_entry);                                     \
__out:  __ret;                                                                  \
})

这个函数里面所调用的函数的具体实现就不再解释了,代码贴过来太冗余了,本身也比较简单。

关于唤醒函数
唤醒函数前面也做过简单介绍,我们这里直接进入主体,介绍其实现函数。

static void __wake_up_common_lock(struct wait_queue_head *wq_head, 
          unsigned int mode, 
          int nr_exclusive, int wake_flags, void *key)
{
        unsigned long flags;
        ... ...
        spin_lock_irqsave(&wq_head->lock, flags);
        nr_exclusive = __wake_up_common(wq_head, mode, 
                                            nr_exclusive, 
                                            wake_flags, key, &bookmark);
        spin_unlock_irqrestore(&wq_head->lock, flags);

        ...  ...
}

具体实现在函数__wake_up_common中。代码比较长,我们这里删除不必要的代码,只保留必要的代码逻辑。

static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
                        int nr_exclusive, int wake_flags, void *key,
                        wait_queue_entry_t *bookmark)
{
        wait_queue_entry_t *curr, *next;
        ... ...
        /* 主要是这个循环,完成所有等待线程的唤醒, 这里关键是调用func */
        list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
                unsigned flags = curr->flags;
                int ret;
                /* 这个函数是在init_wait_entry中初始化的,函数的名字是
                 * autoremove_wake_function,主要完成线程唤醒的动作。 */
                ret = curr->func(curr, mode, wake_flags, key);
                if (ret < 0)
                        break;
                if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
                        break;
                ... ...
        }
        return nr_exclusive;
}

相信介绍到这里,大家应该对等待队列有了比较清晰的认识。总结起来就是要等待的线程加入队列并休眠,当条件满足时有其它线程将处于休眠状态的线程唤醒

其它接口

本文只介绍了基本的接口,其实系统还提供了很多扩展功能接口,以wake_up为例,还包括如下接口:

#define wake_up(x)                      __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_nr(x, nr)               __wake_up(x, TASK_NORMAL, nr, NULL)
#define wake_up_all(x)                  __wake_up(x, TASK_NORMAL, 0, NULL)
#define wake_up_locked(x)               __wake_up_locked((x), TASK_NORMAL, 1)
#define wake_up_all_locked(x)           __wake_up_locked((x), TASK_NORMAL, 0)

#define wake_up_interruptible(x)        __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x)    __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_interruptible_sync(x)   __wake_up_sync((x), TASK_INTERRUPTIBLE, 1)

接口比较多,这里就不一一介绍了,但使用方法是类似的。

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

推荐阅读更多精彩内容

  • 案例:当串口设备不可读的时候(没有数据可读),那么应用程序应该怎么办? 案例:当按键设备没有操作时(按键数据不可读...
    小叶大孟阅读 4,470评论 0 1
  • select 在内核中大致实现的一个解说:http://janfan.github.io/chinese/2015...
    carlson阅读 2,203评论 0 0
  • iOS多线程编程 基本知识 1. 进程(process) 进程是指在系统中正在运行的一个应用程序,就是一段程序的执...
    陵无山阅读 6,039评论 1 14
  • 走了一辈子,远方仍在远方。向着远方跋涉,向着险峰跋涉,且把景色留在后头,且把希望挂在前头。人生不容易,遗憾...
    冰夫阅读 289评论 0 0
  • 再过十六天,2018年高考将正式拉开大幕。在今年的高考中,又将有一批新的政策落地,其中包括关注度较高的取消部分加分...
    YLS_e592阅读 84评论 0 0