Linux 中的 waitqueue 机制详解

源码基于:Linux5.10

0. 前言

等待队列(waitqueue) 这个机制在Linux 内核中使用的频率很高,与进程调度机制紧密相关联,可以用来同步对系统资源的访问、异步事件通知、跨进程通信等。网上关于等待队列使用的优秀文章也很多,之所以笔者也写一篇,一是想更新下最新代码中的使用,二是融入些自己的拙见,方便自己回头查看,也希望能有助于后来读者。

1. 原理

引用网上一个专家的一句话:

A "wait queue" in the Linux kernel is a data structure to manage threads that are waiting for some condition to become true;

笔者个人觉得还是这句话概括的挺好。等待队列就是一个数据结构,该数据结构用来管理那些正在等待某个condition 变成true 的进程。

等待队列用 wait_queue_head_t 这个数据结构串联、管理所有正在等待的进程,每一次等待都会创建一个 wait_queue_entry_t,用该数据结构来更细致管理每个进程:

  • private:存放每个正在等待的进程 task_struct 的结构体指针;
  • flags:用以指定该进程处于什么等待属性;
  • func:当该进程被唤醒时,执行一个特殊的回调函数;

本文将结合该图,细致剖析:

  • 等待队列的创建过程;
  • 每个进程的等待过程;
  • 等待队列中每个等待的唤醒过程;

1. waitqueue的基本概念
1.1 waitqueue 的数据结构

include/linux/wait.h
 
struct wait_queue_entry {
    unsigned int        flags;        //该entry的属性
    void            *private;         //该entry与进程绑定,存放该进程的task_struct指针
    wait_queue_func_t    func;         //回调函数,当等待队列被唤醒时的回调函数
    struct list_head    entry;
};
 
struct wait_queue_head {
    spinlock_t        lock;             //等待队列的自旋锁,用以同步整个等待队列
    struct list_head    head;         //等待队列的头,用以串联整个等待队列
};
typedef struct wait_queue_head wait_queue_head_t;
typedef struct wait_queue_entry wait_queue_entry_t;


下面来看下等待队列 entry 的flags:

include/linux/wait.h
 
#define WQ_FLAG_EXCLUSIVE    0x01
#define WQ_FLAG_WOKEN        0x02
#define WQ_FLAG_BOOKMARK    0x04
#define WQ_FLAG_CUSTOM        0x08
#define WQ_FLAG_DONE        0x10

下面来看下等待队列 entry 的回调函数:

typedef int (*wait_queue_func_t)(struct wait_queue_entry *wq_entry, unsigned mode, int flags, void *key);
int default_wake_function(struct wait_queue_entry *wq_entry, unsigned mode, int flags, void *key);
第一行,指定等待队列回调函数的函数指针类型 wait_queue_func_t;

第二行,指定系统默认的唤醒回调函数 default_wake_function();

1.2 等待队列的创建和初始化

include/linux/wait.h
 
#define init_waitqueue_head(wq_head)                        \
    do {                                    \
        static struct lock_class_key __key;                \
                                        \
        __init_waitqueue_head((wq_head), #wq_head, &__key);        \
    } while (0)
kernel/sched/wait.c
 
void __init_waitqueue_head(struct wait_queue_head *wq_head, const char *name, struct lock_class_key *key)
{
    spin_lock_init(&wq_head->lock);
    lockdep_set_class_and_name(&wq_head->lock, key, name);
    INIT_LIST_HEAD(&wq_head->head);
}
EXPORT_SYMBOL(__init_waitqueue_head);


等待队列的名称为:传入 init_waitqueue_head() 的变量名 (字符串化);

另外,也可以使用宏 DECLARE_WAIT_QUEUE_HEAD() 进行创建和初始化:

include/linux/wait.h
 
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {                    \
    .lock        = __SPIN_LOCK_UNLOCKED(name.lock),            \
    .head        = { &(name).head, &(name).head } }
 
#define DECLARE_WAIT_QUEUE_HEAD(name) \
    struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)


1.3 等待队列 entry 的创建和初始化

1.3.1 使用DECLARE_WAITQUEUE()创建

include/linux/wait.h
 
#define __WAITQUEUE_INITIALIZER(name, tsk) {                    \
    .private    = tsk,                            \
    .func        = default_wake_function,                \
    .entry        = { NULL, NULL } }
 
#define DECLARE_WAITQUEUE(name, tsk)                        \
    struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)


注意,此种创建的进程是传入的 tsk,另外,唤醒时的回调函数是default_wake_function()

1.3.2 使用DEFINE_WAIT() 创建

include/linux/wait.h
 
#define DEFINE_WAIT_FUNC(name, function)                    \
    struct wait_queue_entry name = {                    \
        .private    = current,                    \
        .func        = function,                    \
        .entry        = LIST_HEAD_INIT((name).entry),            \
    }
 
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)


注意,此种创建的进程是当前进程,另外,唤醒时的回调函数是autoremove_wake_function() 

1.3.3 使用init_wait() 初始化

include/linux/wait.h
 
#define init_wait(wait)                                \
    do {                                    \
        (wait)->private = current;                    \
        (wait)->func = autoremove_wake_function;            \
        INIT_LIST_HEAD(&(wait)->entry);                    \
        (wait)->flags = 0;                        \
    } while (0)


也可以init_wait() 对定义好的 wait_queue_entry_t 指针 进行初始化。

该entry 绑定的是当前进程,另外,唤醒时的回调函数是autoremove_wake_function() 

1.3.4 在___wait_event()时使用init_wait_entry() 初始化

kernel/sched/wait.c
 
void init_wait_entry(struct wait_queue_entry *wq_entry, int flags)
{
    wq_entry->flags = flags;
    wq_entry->private = current;
    wq_entry->func = autoremove_wake_function;
    INIT_LIST_HEAD(&wq_entry->entry);
}
EXPORT_SYMBOL(init_wait_entry);


与 init_wait() 区别是多了一个 flags 参数。

详细的可以查看下文的 __wait_event() 函数和wake_up() 函数。

1.4 添加和移除等待队列
1.4.1 添加到等待队列 

include/linux/wait.h
 
static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    list_add(&wq_entry->entry, &wq_head->head);
}
 
 
static inline void
__add_wait_queue_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    wq_entry->flags |= WQ_FLAG_EXCLUSIVE;
    __add_wait_queue(wq_head, wq_entry);
}


前插到等待队列中,__add_wait_queue_exclusive() 将要插入的entry 指定为 EXCLUSIVE 属性。

include/linux/wait.h
 
static inline void __add_wait_queue_entry_tail(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    list_add_tail(&wq_entry->entry, &wq_head->head);
}
 
static inline void
__add_wait_queue_entry_tail_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    wq_entry->flags |= WQ_FLAG_EXCLUSIVE;
    __add_wait_queue_entry_tail(wq_head, wq_entry);
}

后插到等待队列中,__add_wait_queue_entry_tail_exclusive() 将要插入的entry 指定为 EXCLUSIVE 属性。

注意:

对于使用 ___wait_event() 创建的 entry,如果是 EXCLUSIVE 属性,该entry 会被插入到等待队列的尾部,其他属性的entry 将选择前插。

1.4.2 等待队列的移除

include/linux/wait.h
 
static inline void
__remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    list_del(&wq_entry->entry);
}


直接就是调用 list_del() 将指定的 entry 从等待队列中移除。

2. wait event
在上一节中已经讲述了等待队列的基本概念,包括wait_queue_head 和 wait_queue_entry 的创建和初始化。

在初始化结束,会在某个进程中指定一个等待事件,用以阻塞等待唤醒,当 condition 成立时退出此次等待事件。

等待事件的接口有很多,我们接下来分批来看。

2.1 UNINTERRUPTIBLE 等待事件
2.1.1 __wait_event()
#define __wait_event(wq_head, condition)                    \
    (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0,    \
                schedule())
2.1.2 __io_wait_event()
#define __io_wait_event(wq_head, condition)                    \
    (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0,    \
                io_schedule())
2.1.3 __wait_event_timeout()
#define __wait_event_timeout(wq_head, condition, timeout)            \
    ___wait_event(wq_head, ___wait_cond_timeout(condition),            \
              TASK_UNINTERRUPTIBLE, 0, timeout,                \
              __ret = schedule_timeout(__ret))
2.1.4 __wait_event_exclusive_cmd()
#define __wait_event_exclusive_cmd(wq_head, condition, cmd1, cmd2)        \
    (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 1, 0,    \
                cmd1; schedule(); cmd2)
2.1.5 __wait_event_cmd()
#define __wait_event_cmd(wq_head, condition, cmd1, cmd2)            \
    (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0,    \
                cmd1; schedule(); cmd2)
2.1.6 __wait_event_lock_irq()
#define __wait_event_lock_irq(wq_head, condition, lock, cmd)            \
    (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0,    \
                spin_unlock_irq(&lock);                \
                cmd;                        \
                schedule();                        \
                spin_lock_irq(&lock))
注意:

除了 __ion_wait_event() 这个等待事件使用的是 io_schedule(),其他等待事件都是使用 schedule() 函数使得当前进程让出调度,进入休眠状态。

2.2 INTERRUPTIBLE 等待事件
2.2.1 __wait_event_freezable()
#define __wait_event_freezable(wq_head, condition)                \
    ___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0,        \
                freezable_schedule())
2.2.2 __wait_event_freezable_timeout()
#define __wait_event_freezable_timeout(wq_head, condition, timeout)        \
    ___wait_event(wq_head, ___wait_cond_timeout(condition),            \
              TASK_INTERRUPTIBLE, 0, timeout,                \
              __ret = freezable_schedule_timeout(__ret))
2.2.3 __wait_event_freezable_exclusive()
#define __wait_event_freezable_exclusive(wq, condition)                \
    ___wait_event(wq, condition, TASK_INTERRUPTIBLE, 1, 0,            \
            freezable_schedule())
2.2.4 __wait_event_interruptible()
#define __wait_event_interruptible(wq_head, condition)                \
    ___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0,        \
              schedule())
2.2.5 __wait_event_interruptible_timeout()
#define __wait_event_interruptible_timeout(wq_head, condition, timeout)        \
    ___wait_event(wq_head, ___wait_cond_timeout(condition),            \
              TASK_INTERRUPTIBLE, 0, timeout,                \
              __ret = schedule_timeout(__ret))
2.2.6 __wait_event_interruptible_exclusive()
#define __wait_event_interruptible_exclusive(wq, condition)            \
    ___wait_event(wq, condition, TASK_INTERRUPTIBLE, 1, 0,            \
              schedule())
2.2.7 __wait_event_interruptible_lock_irq()
#define __wait_event_interruptible_lock_irq(wq_head, condition, lock, cmd)    \
    ___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0,        \
              spin_unlock_irq(&lock);                    \
              cmd;                            \
              schedule();                        \
              spin_lock_irq(&lock))
2.3 KILLABLE 等待事件
2.3.1 __wait_event_killable()
#define __wait_event_killable(wq, condition)                    \
    ___wait_event(wq, condition, TASK_KILLABLE, 0, 0, schedule())
2.3.2 __wait_event_killable_exclusive()
#define __wait_event_killable_exclusive(wq, condition)                \
    ___wait_event(wq, condition, TASK_KILLABLE, 1, 0,            \
              schedule())
2.3.3 __wait_event_killable_timeout()
#define __wait_event_killable_timeout(wq_head, condition, timeout)        \
    ___wait_event(wq_head, ___wait_cond_timeout(condition),            \
              TASK_KILLABLE, 0, timeout,                \
              __ret = schedule_timeout(__ret))
2.4 IDLE 等待事件
2.4.1 wait_event_idle()
#define wait_event_idle(wq_head, condition)                    \
do {                                        \
    might_sleep();                                \
    if (!(condition))                            \
        ___wait_event(wq_head, condition, TASK_IDLE, 0, 0, schedule());    \
} while (0)
2.4.2 wait_event_idle_exclusive()
#define wait_event_idle_exclusive(wq_head, condition)                \
do {                                        \
    might_sleep();                                \
    if (!(condition))                            \
        ___wait_event(wq_head, condition, TASK_IDLE, 1, 0, schedule());    \
} while (0)
2.4.3 __wait_event_idle_timeout()
#define __wait_event_idle_timeout(wq_head, condition, timeout)            \
    ___wait_event(wq_head, ___wait_cond_timeout(condition),            \
              TASK_IDLE, 0, timeout,                    \
              __ret = schedule_timeout(__ret))
2.4.4 __wait_event_idle_exclusive_timeout()
#define __wait_event_idle_exclusive_timeout(wq_head, condition, timeout)    \
    ___wait_event(wq_head, ___wait_cond_timeout(condition),            \
              TASK_IDLE, 1, timeout,                    \
              __ret = schedule_timeout(__ret))
2.5 ___wait_event()
对于不同状态的等待事件上面已经列举了,最终调用的都是 ___wait_event(),注意函数名是三个下划线:

include/linux/wait.h
 
#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;                                \
    }                                    \
    finish_wait(&wq_head, &__wq_entry);                    \
__out:    __ret;                                    \
})

参数:

wq_head:wait_queue_head_t 变量,为什么不使用指针呢? 
condition:退出等待的条件,只有为true 时才能退出等待 (__wait_event 函数是个死循环);
state:用以等待的 task 状态;
exclusive:标记是否是独占等待;
ret:一般用于有 timeout时,会将其临时存放在 __ret 中,在cmd 中作为参数带入;
cmd:一般是 schedule() 函数或者 schedule_timeout() 函数;
注意其中的 condition 参数,当等待事件中存在 timeout 时,该参数一般会是 ___wait_cond_timeout(condition):

#define ___wait_cond_timeout(condition)                        \
({                                        \
    bool __cond = (condition);                        \
    if (__cond && !__ret)                            \
        __ret = 1;                            \
    __cond || !__ret;                            \
})
即,当condition 为true 或者是 timeout 到时了,___wait_event() 的condition 才为 true。

另外,___wait_event() 是个死循环,只有当condition 为true,或者当前进程收到 SIGKILL 信号时,才会退出该死循环。

2.5.1 init_wait_entry()
struct wait_queue_entry __wq_entry;    
 
init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0);
当调用 ___wait_event() 时,会创建一个 wait_queue_entry 变量,并调用 init_wait_entry() 对其进行初始化,其中第二个参数 flags 根据 ___wait_event() 的入参 exclusive 决定;

2.5.2 prepare_to_wait_event()

kernel/sched/wait.c
 
long prepare_to_wait_event(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
    unsigned long flags;
    long ret = 0;
 
    spin_lock_irqsave(&wq_head->lock, flags);
    if (signal_pending_state(state, current)) {
        /*
         * Exclusive waiter must not fail if it was selected by wakeup,
         * it should "consume" the condition we were waiting for.
         *
         * The caller will recheck the condition and return success if
         * we were already woken up, we can not miss the event because
         * wakeup locks/unlocks the same wq_head->lock.
         *
         * But we need to ensure that set-condition + wakeup after that
         * can't see us, it should wake up another exclusive waiter if
         * we fail.
         */
        list_del_init(&wq_entry->entry);
        ret = -ERESTARTSYS;
    } else {
        if (list_empty(&wq_entry->entry)) {
            if (wq_entry->flags & WQ_FLAG_EXCLUSIVE)
                __add_wait_queue_entry_tail(wq_head, wq_entry);
            else
                __add_wait_queue(wq_head, wq_entry);
        }
        set_current_state(state);
    }
    spin_unlock_irqrestore(&wq_head->lock, flags);
 
    return ret;
}
EXPORT_SYMBOL(prepare_to_wait_event);

每次在等待事件进入shedule 之前,会调用 signal_pending_state() 确定进程是否立即返回 RUNNING 状态:

如果不允许信号处理,返回0,表示不需要返回;
如果允许信号处理,但该进程中没有信号等待处理,也表示不需要返回;
除此,如果是 TASK_INTERRUPTIBLE 或当前进程可以处理信号且收到了 SIGKILL,则需要立即返回RUNNING 状态;
include/linux/sched/signal.h
 
static inline int signal_pending_state(long state, struct task_struct *p)
{
    //是否允许信号处理,不允许返回0
    if (!(state & (TASK_INTERRUPTIBLE | TASK_WAKEKILL)))
        return 0;
 
    //当允许信号处理时,确定当前进程是否有信号挂起,如果没有返回0
    if (!signal_pending(p))
        return 0;
 
    return (state & TASK_INTERRUPTIBLE) || __fatal_signal_pending(p);
}
回到 prepare_to_wait_event() 函数,如果 signal_pending_state() 需要进程立即返回 RUNNING 状态,则认为此次等待事件无效,立即退出此次等待。

如果不需要返回 RUNNING,则会将此次的等待队列 entry 添加到等待队列中。不过需要注意的是,对于 exclusive 属性的entry,会将其添加到等待队列的尾部,其他属性的 entry 添加到等待队列的头部。

prepare_to_wait_event() 函数的最后会调用 set_current_state() 将进程状态标记上。

2.5.3 执行 cmd
cmd 是 ___wait_event() 的最后一个参数,可能是:

schedule()
io_schedule()
schedule_timeout()
freezable_schedule()
freezable_schedule_timeout()
cmd1; schedule(); cmd2
详细可以查看 include/linux/wait.h 文件中调用 ___wait_event() 的地方。

2.5.4 finish_wait()
当退出等待事件需要退出时,在 ___wait_event() 的最后会调用 finish_wait() 函数:

kernel/sched/wait.c
 
void finish_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    unsigned long flags;
 
    __set_current_state(TASK_RUNNING);
    /*
     * We can check for list emptiness outside the lock
     * IFF:
     *  - we use the "careful" check that verifies both
     *    the next and prev pointers, so that there cannot
     *    be any half-pending updates in progress on other
     *    CPU's that we haven't seen yet (and that might
     *    still change the stack area.
     * and
     *  - all other users take the lock (ie we can only
     *    have _one_ other CPU that looks at or modifies
     *    the list).
     */
    if (!list_empty_careful(&wq_entry->entry)) {
        spin_lock_irqsave(&wq_head->lock, flags);
        list_del_init(&wq_entry->entry);
        spin_unlock_irqrestore(&wq_head->lock, flags);
    }
}
EXPORT_SYMBOL(finish_wait);

主要做了两件事情:

将进程状态改成 RUNNING;
确定该 entry是否还在等待队列中,如果还在队列中,将其从等待队列中移除;
2.6 总结
等待队列的wait event 有很多种,按照进程将要进入的状态分为:
INTERRUPTIBLE 等待事件;
UNINTERRUPTIBLE 等待事件;
KILLABLE 等待事件;
IDLE 等待事件;
所有等待事件最终会调用 ___wait_event() 函数;
首先会创建一个 entry,并调用 init_wait_entry() 进行初始化;
进入死循环,直到condition 为true 或进程允许信号中断并收到了 SIGKILL 信号;
___wait_event() 中会调用最后一个参数 cmd,通常是利用 schedule() 交出调度后进入休眠;
当被唤醒后会确定当前进程状态或condition,进而判断是否退出死循环;
当___wait_event() 中退出死循环后,会调用 finish_wait() 将进程状态改为 RUNNING,并将此次的 entry 从等待队列中删除;


3. wake up
3.1 wake_up TASK_NORMAL
include/linux/wait.h
 
#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)
有三点注意:

wake_up 不携带interruptible 的接口,指的是唤醒 TASK_NORMAL 状态的进程,而TASK_NORMAL 表示的是 (TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE);
wake_up 最终调用的有两个接口:__wake_up() 和 __wake_up_locked(),区别在于 __wake_up_locked() 的调用是已经在等待队列的自旋锁中了,不需要再加锁,最终实现都需要调用 __wake_up_common() 函数;
wake_up 中有个nr 的参数,用以决定唤醒几个等待的 exclusive entry;
3.2 wake_up INTERRUNPTIBLE
include/linux/wait.h
 
#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)
3.3 __wake_up()
kernel/sched/wait.c
 
void __wake_up(struct wait_queue_head *wq_head, unsigned int mode,
            int nr_exclusive, void *key)
{
    __wake_up_common_lock(wq_head, mode, nr_exclusive, 0, key);
}
EXPORT_SYMBOL(__wake_up);
 
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;
    wait_queue_entry_t bookmark;
 
    bookmark.flags = 0;
    bookmark.private = NULL;
    bookmark.func = NULL;
    INIT_LIST_HEAD(&bookmark.entry);
 
    do {
        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);
    } while (bookmark.flags & WQ_FLAG_BOOKMARK);
}

函数流程比较清晰:

定义了一个 bootmark,用以串联还需要再处理的 entry;
调用 __wake_up_common() 进行实际的唤醒操作;
注意:

这里引入了一个 bookmark,是为了 __wake_up_common() 进去后唤醒的 entry 过多而导致自旋锁持有时间太长。当唤醒的 entry 超过 WAITQUEUE_WALK_BREAK_CNT (默认64) 时,将后面的 entry 放到bookmark 尾部并退出 __wake_up_common() 函数。通过这种机制,实现了进程分批次唤醒,避免了等待队列中自旋锁被持有时间过长

下面单独来看下 __wake_up_common() 的处理:

3.4 __wake_up_common()
kernel/sched/wait.c
 
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;
    int cnt = 0;
 
    // 判断自旋锁已经被持有
    lockdep_assert_held(&wq_head->lock);
 
    //如果是再处理的entry,找到第一个entry,并初重新初始化bootmark
    if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
        curr = list_next_entry(bookmark, entry);
 
        list_del(&bookmark->entry);
        bookmark->flags = 0;
    } else //如果bookmark中没有entry,则获取等待队列的第一个entry
        curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);
 
    //确定等待队列是否为空
    if (&curr->entry == &wq_head->head)
        return nr_exclusive;
 
    //从第一个entry,即curr 开始依次处理等待队列的entry
    list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
        unsigned flags = curr->flags;
        int ret;
 
        //跳过等待队列上标记 WQ_FLAG_BOOKMARK的entry,目前系统中没有主动标记的entry
        if (flags & WQ_FLAG_BOOKMARK)
            continue;
 
        /**
          * 调用等待队列entry绑定的唤醒回调函数
          * 具体唤醒什么样的进程,是INTERRUPTIBLE / UNINTERRUPTIBLE,需要根据mode 决定,
          *   如果需要唤醒的进程mode与当前进程的状态state 不符合,则返回0,
          *   详细查看 try_to_wake_up()函数;
          */
        ret = curr->func(curr, mode, wake_flags, key);
        if (ret < 0)
            break;
 
        //如果成功唤醒,且当前entry标记了WQ_FLAG_EXCLUSIVE,则nr_exclusive计数要减1
        //  当nr_exclusive 个entry都唤醒了,就不再接着去唤醒了,退出此次wake_up
        if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
            break;
 
        /**
          * 之所以引入临时的bootmark,就为了性能考虑
          * 当连续唤醒的entry 超过了WAITQUEUE_WALK_BREAK_CNT(系统默认为64)时,
          *   会将剩下的entry移入到bootmark之后,返回到上一级函数
          * 通过这种机制,实现了进程分批次唤醒,避免了等待队列中自旋锁被持有时间过长
          */
        if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) &&
                (&next->entry != &wq_head->head)) {
            bookmark->flags = WQ_FLAG_BOOKMARK;
            list_add_tail(&bookmark->entry, &next->entry);
            break;
        }
    }
 
    return nr_exclusive;
}

参数:

wq_head:等待队列的头;
mode:唤醒进程的状态,TASK_NORMAL 或 TASK_INTERRUPTIBLE;
nr_exclusive:指定需要唤醒的 exclusive entry 数量;
wake_flags:只有在 __wake_up_sync_key() 函数调用时会传入 WF_SYNC,其他默认都为 0;
bootmark:用以标记再处理的 entry;
3.5 总结
wake up 的接口比较多,注意 __wake_up() 和 __wake_up_locked() 的区别在于后者已经处于等待队列的自旋锁中。另外 __wake_up_common_lock() 函数中多了 bookmark机制,实现进程的分批次唤醒。

wake up 最终都是通过唤醒回调函数达到唤醒等待队列的目的,系统默认的回调函数最终都是通过 try_to_wake_up() 来实现。

下面单独来分析唤醒回调函数。

4. 唤醒回调函数
上面 wake_up() 函数中没轮询一个 entry 时,都会调用该entry 的唤醒回调函数。

在 entry 上文第 1.3 节中提到系统默认有两种方式:

default_wake_function
autoremove_wake_function
唯一区别在于 autoremove_wake_function() 函数在调用 default_wake_function() 之后会将该 entry 从等待队列中移除,如下:

4.1 autoremove_wake_function()
kernel/sched/wait.c
 
int autoremove_wake_function(struct wait_queue_entry *wq_entry, unsigned mode, int sync, void *key)
{
    int ret = default_wake_function(wq_entry, mode, sync, key);
 
    if (ret)
        list_del_init_careful(&wq_entry->entry);
 
    return ret;
}
EXPORT_SYMBOL(autoremove_wake_function);
5. 另外一种休眠1
上文第 2、3、4节 基本上剖析的是系统 ___wait_event() 和 wake_up() 方式的休眠和唤醒。

而在第 1.3 节中我们还看到了其他中等待队列 entry 的创建和初始化方式,例如:

include/linux/wait.h
 
#define DEFINE_WAIT_FUNC(name, function)                    \
    struct wait_queue_entry name = {                    \
        .private    = current,                    \
        .func        = function,                    \
        .entry        = LIST_HEAD_INIT((name).entry),            \
    }
 
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
这种有模块自己定义的 entry,如何使用呢?

系统提供了另外一个函数 prepare_to_wait() 和 prepare_to_wait_exclusive()。

5.1 prepare_to_wait()
kernel/sched/wait.c
 
void
prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
    unsigned long flags;
 
    wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    if (list_empty(&wq_entry->entry))
        __add_wait_queue(wq_head, wq_entry);
    set_current_state(state);
    spin_unlock_irqrestore(&wq_head->lock, flags);
}
EXPORT_SYMBOL(prepare_to_wait);
将该 entry 添加到等待队列的头部,并调用 set_current_state() 将当前进程状态置上。

5.2 prepare_to_wait_exclusive()
kernel/sched/wait.c
 
bool
prepare_to_wait_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
    unsigned long flags;
    bool was_empty = false;
 
    wq_entry->flags |= WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    if (list_empty(&wq_entry->entry)) {
        was_empty = list_empty(&wq_head->head);
        __add_wait_queue_entry_tail(wq_head, wq_entry);
    }
    set_current_state(state);
    spin_unlock_irqrestore(&wq_head->lock, flags);
    return was_empty;
}
EXPORT_SYMBOL(prepare_to_wait_exclusive);

与 prepare_to_wait() 不同的是,这里指定的 entry 是 WQ_FLAG_EXCLUSIVE,并且该entry 是添加到等待队列尾部。

5.3 示例
了解完 DEFINE_WAIT()、prepare_to_wait() 之后,模块上可以使用如下示例进行管理:

net/atm/svc.c
 
static void svc_disconnect(struct atm_vcc *vcc)
{
    DEFINE_WAIT(wait);
    ...
    if (test_bit(ATM_VF_REGIS, &vcc->flags)) {
        sigd_enq(vcc, as_close, NULL, NULL, NULL);
        for (;;) {
            prepare_to_wait(sk_sleep(sk), &wait, TASK_UNINTERRUPTIBLE);
            if (test_bit(ATM_VF_RELEASED, &vcc->flags) || !sigd)
                break;
            schedule();
        }
        finish_wait(sk_sleep(sk), &wait);
    }
 
    ...

6. 另外一种休眠2
DECLARE_WAIT_QUEUE_HEAD(queue);
DECLARE_WAITQUEUE(wait, current);
 
for (;;) {
    add_wait_queue(&queue, &wait);
    set_current_state(TASK_INTERRUPTIBLE);
    if (condition)
        break;
    schedule();
    remove_wait_queue(&queue, &wait);
    if (signal_pending(current))
        return -ERESTARTSYS;
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&queue, &wait);
这种方式是手动调用等待队列的add、remove 接口。

7. 总结
至此,等待队列基本剖析完成。其实,根本上就是通过一个数据接口 wait_queue_head_t 串联很多 wait_queue_entry_t,主要在这些 entry 的创建或初始化上,因为此时会指定flags、func 以及对应进程的 private 变量。

在不同的进程中会调用 ___wait_event() 可以自动创建等待队列 entry;
也可以通过 DEFINE_WAIT()、DECLARE_WAITQUEUE() 等方式手动创建等待队列entry;
不同的实现方式对应着不同的唤醒方式:

___wait_event() 中必须要指定 condition,只有在condition 为true或者进程挂起信号并收到信号,才能退出等待,进程才能进入之后的 RUNNING 状态;
通过 DEFINE_WAIT() 等方式,可以配合使用prepare_to_wait(),进而由模块自己组织代码,控制schedule 的时机和唤醒后的条件处理;
当然,也可以不适用等待队列,也能达到休眠、唤醒的目的。都是根据不同的实际需求,进而选择合适的方式。

另外,wait event 的state 有下面几种方式:

TASK_UNINTERRUPTIBLE,表示该等待事件只能调用 wake_up 唤醒;
TASK_INTERRUPTIBLE,表示该等待事件可以使用wake_up 唤醒,也可以使用信号唤醒;
TASK_KILLABLE,表示该等待事件只能由 SIGKILL 唤醒或wake_up 唤醒;
TASK_IDLE,表示等待事件只能由wake_up唤醒;
当然,这些state 还组合了 timeout 机制,详细可以查看 schedule_timeout() 处理。

最后,附上等待队列的完整流程图:

文章知识点与官方知识档案匹配,可进一步学习相关知识
————————————————

                            本文为博主原创文章,未经博主允许不得转载!
                        
原文链接:https://blog.csdn.net/shift_wwx/article/details/134994589

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值