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.hstruct 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.cvoid __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.cvoid 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.hstatic 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.hstatic 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.hstatic 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(),注意函数名是三个下划线:

#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.clong 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.hstatic inline int signal_pending_state(long state, struct task_struct *p)
{//是否允许信号处理,不允许返回0if (!(state & (TASK_INTERRUPTIBLE | TASK_WAKEKILL)))return 0;//当允许信号处理是,确定当前进程是否有信号挂起,如果没有返回0if (!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()
  • schedule_timeout()
  • freezable_schedule()
  • cmd1; schedule(); cmd2

2.5.4 finish_wait()

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

kernel/sched/wait.cvoid 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.cvoid __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.cstatic 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,并初重新初始化bootmarkif (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {curr = list_next_entry(bookmark, entry);list_del(&bookmark->entry);bookmark->flags = 0;} else //如果bookmark中没有entry,则获取等待队列的第一个entrycurr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);//确定等待队列是否为空if (&curr->entry == &wq_head->head)return nr_exclusive;//从第一个entry,即curr 开始依次处理等待队列的entrylist_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {unsigned flags = curr->flags;int ret;//跳过等待队列上标记 WQ_FLAG_BOOKMARK的entry,目前系统中没有主动标记的entryif (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_upif (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.cint 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.cvoid
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.cbool
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.cstatic 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() 处理。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/230037.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Nginx location+Nginx rewrite(重写)(新版)

Nginx locationNginx rewrite(重写) Nginx locationNginx rewrite(重写)一、location1、常用的Nginx 正则表达式2、location的类型3、location 的匹配规则4、location 优先级5、location 示例说明5.1只修改网页路径5.2修改nginx配置文件和网页路径5.3一般前缀5.4正则匹配5.5前缀…

百分比组件 - elementui改动

<el-slider v-model"value2" style"width: 87%;position: absolute;bottom: 9px;" disabled :show-tooltip"false"></el-slider>value2: 0,// 百分比条 ::v-deep .el-slider__runway.disabled .el-slider__bar {background-color: #…

linux应急响应基础和常用命令

linux应急响应 linux应急响应基础和常用命令基于linux系统本身进行应急响应。 系统基础信息获取 获取linux服务器基本信息 命令&#xff1a; uname -a内存cpu信息 cat /proc/cpuinfo cat /proc/meminfo lscpu free -m lsmod #查看载入的模块信息进程查看 动态进程查看 …

7.实现任务的rebalance

1.设计 1.1 背景 系统启动后&#xff0c;所有任务都在被执行&#xff0c;如果这时某个节点宕机&#xff0c;那它负责的任务就不能执行了&#xff0c;这对有稳定性要求的任务是不能接受的&#xff0c;所以系统要实现rebalance的功能。 1.2 设计 下面是Job分配与执行的业务点…

基于PyCharm实现串口GUI编程

工具效果如下如所示 下面简单介绍一下操作流程 1.打开PyCharm软件 2.创建一个工程 3.给该工程命名 4.在main.py里面黏贴如下的代码 # This is a sample Python script. # Press ShiftF10 to execute it or replace it with your code. # Press Double Shift to search everyw…

【LeetCode刷题笔记(3)】【Python】【最长连续序列】【中等】

文章目录 最长连续序列题目描述示例示例 1示例 2 提示 解决方案解决方案1&#xff1a;【集合去重】【遍历数组查找元素】避免无效计数方案1的可行性分析 解决方案2&#xff1a;【集合去重】 【遍历集合查找元素】运行结果复杂度分析 结束语 最长连续序列 最长连续序列 题目描述…

c语言 文件与文件操作

&#x1f3e0; 一.引言 我们日常生活中会将我们制作的ppt,word等存放在文件里进行归类&#xff0c;你是否知道我们能用cC语言对文件进行操作呢(比如文件的打开&#xff0c;关闭和读写等)&#xff1f;那接下来跟博主一起来学习下吧。 &#x1f3e0;二.什么是文件 磁盘上的文件就…

<VR串流线方案> PICO 4 Pro VR串流线方案 Oculus Quest2 Link串流线方案

虚拟现实技术(英文名称&#xff1a;Virtual Reality&#xff0c;缩写为VR)&#xff0c;又称虚拟实境或灵境技术&#xff0c;是20世纪发展起来的一项全新的实用技术。虚拟现实技术囊括计算机、电子信息、仿真技术&#xff0c;其基本实现方式是以计算机技术为主&#xff0c;利用并…

MES系统工单进度查询:提升生产控制与监控

在MES系统中&#xff0c;工单进度查询是一个至关重要的功能&#xff0c;它为企业提供了实时、准确地追踪和监控生产工单进度的能力。 一、MES系统工单进度查询的重要性 1. 实时监控生产进度&#xff1a;通过工单进度查询&#xff0c;企业能够随时了解每个工单的进展情况&#…

qt实现基本文件操作

先通过ui界面实现基本框架 接下来就要实现每个按键的功能了 我们先来实现新建的的功能&#xff0c;我们右键新建键&#xff0c;可以发现没有转到槽的功能&#xff0c;因此我们要自己写connect来建立关系。 private slots:void newActionSlot(); 在.h文件中加上槽函数。 conne…

【ZYNQ学习】PL第一课

这节课讲什么&#xff1f; 这节课的名字本来是想写为LED&#xff0c;但这一课里除了LED也有按键&#xff0c;又想换为GPIO控制&#xff0c;但关于PL的GPIO控制&#xff0c;不应该这么草率和简单&#xff0c;而且这一课有很多和ZYNQ或者PL关联性不强的东西要说。 所以我写了删删…

【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统(四)用户管理、部门管理模块

第一篇&#xff1a;【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统&#xff08;一&#xff09;搭建项目 第二篇&#xff1a;【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统&#xff08;二&#xff09;日志输出中间件、校验token中间件、配置路由、基础工具函数。 …

眼镜店验光配镜处方单打印管理系统软件教程

一、前言 1、眼镜店原始的手写处方单逐步被电脑打印单取代 2、使用电脑开单&#xff0c;记录可以保存可以查询&#xff0c;而且同一个人配镜可以对比之前的信息 软件下载或技术支持可以点击最下方官网卡片 如上图&#xff0c;该软件有顾客信息模块&#xff0c;旧镜检查模块…

Acre1-6000电气火灾监控系统在工矿企业的应用——安科瑞 顾烊宇

摘要&#xff1a;主要介绍了电气火灾的主要原因、几种电气火灾监控系统的构成和设立意义。参照各规范&#xff0c;讨论了宜设立电气火灾监控系统的场所。该系统的设立可大大减少电气火灾事故的发生&#xff0c;对保证人们的生命财产安全具有重要意义。 关键词:电气火灾&#x…

极智开发 | macwindows本地部署安装AIGC绘图工具Stable Diffusion WebUI

欢迎关注我的公众号 [极智视界],获取我的更多经验分享 大家好,我是极智视界,本文分享一下 mac&windows本地部署安装AIGC绘图工具Stable Diffusion WebUI。 邀您加入我的知识星球「极智视界」,星球内有超多好玩的项目实战源码和资源下载,链接:https://t.zsxq.com/0ai…

Redis-对象

参考资料 极客时间Redis&#xff08;亚风&#xff09; Redis对象 String • 基本编码⽅式是RAW&#xff0c;基于简单动态字符串&#xff08;SDS&#xff09;实现&#xff0c;存储上限为512mb。 • 如果存储的SDS⻓度⼩于44字节&#xff0c;则会采⽤EMBSTR编码&#xff0c;此…

2023年国家基地“楚慧杯”网络空间安全实践能力竞赛 Wp 一点WP

MISC 参考文章&#xff1a; 天权信安“”2023年国家基地“楚慧杯”网络安全实践能力竞赛初赛WriteUp ez-zip 使用脚本解套娃压缩包 import io import zipfilewith open("4096.zip", "rb") as f:data f.read()info "666"while True:with zi…

AttributeError: module ‘jax‘ has no attribute ‘Array‘解决方案

大家好&#xff0c;我是爱编程的喵喵。双985硕士毕业&#xff0c;现担任全栈工程师一职&#xff0c;热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。…

Linux——Ubuntu搭建FTP 时ftp: connect: Connection refused

如何解决ftp: connect: Connection refused&#xff1f; 分析&#xff1a;vsftpd.conf配置文件中默认ipv4:listenNO,ipv6:listen_ipv6YES,默认使用ipv6地址 解决方法&#xff1a;在配置文件中将listenYES开启&#xff0c;并且把listen_ipv6YES注释&#xff0c;重新启动vsftpd…

金蝶云星空和聚水潭单据接口对接

金蝶云星空和聚水潭单据接口对接 接入系统&#xff1a;聚水潭 聚水潭SaaSERP于2014年4月上线&#xff0c;目前累计超过2.5万商家注册使用&#xff0c;成为淘宝应用服务市场ERP类目商家数和商家月订单增速最快的ERP。2014年及2015年“双十一”当天&#xff0c;聚水潭SaaSERP平稳…