Linux等待队列(Wait Queue)
1. Linux等待队列概述
Linux内核的等待队列(Wait Queue)是重要的数据结构,与进程调度机制紧密相关联,可以用来同步对系统资源的访问、异步事件通知、跨进程通信等。
在Linux中,等待队列以循环链表为基础结构,包括两种数据结构:等待队列头(wait queue head)和等待队列元素(wait queue),整个等待队列由等待队列头进行管理。下文将用内核源码(基于Linux kernel 5.2)对等待队列进行介绍,详细说明采用等待队列实现进程阻塞和唤醒的方法。
2. 等待队列头和等待队列元素
等待队列以循环链表为基础结构,链表头和链表项分别为等待队列头和等待队列元素,分别用结构体 wait_queue_head_t 和 wait_queue_entry_t 描述(定义在 linux/wait.h )。
2.1 基本概念
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
}; typedef struct wait_queue_head wait_queue_head_t;
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_entry::flags */
#define WQ_FLAG_EXCLUSIVE 0x01
#define WQ_FLAG_WOKEN 0x02
#define WQ_FLAG_BOOKMARK 0x04 /*
* A single wait-queue entry structure:
*/
struct wait_queue_entry {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head entry;
}; typedef struct wait_queue_entry wait_queue_entry_t;
等待队列头结构包括一个自旋锁和一个链表头。等待队列元素除了包括链表项,还包括:
- flags : 标识队列元素状态和属性
- *private : 用于指向关联进程 task_struct 结构体的指针
- func : 函数指针,用于指向等待队列被唤醒时的回调的唤醒函数
以进程阻塞和唤醒的过程为例,等待队列的使用场景可以简述为:
进程 A 因等待某些资源(依赖进程 B 的某些操作)而不得不进入阻塞状态,便将当前进程加入到等待队列 Q 中。进程 B 在一系列操作后,可通知进程 A 所需资源已到位,便调用唤醒函数 wake up 来唤醒等待队列上 Q 的进程,注意此时所有等待在队列 Q 上的进程均被置为可运行状态。
借助上述描述场景,说明等待队列元素属性 flags 标志的作用,下文也将结合源码进行详细解读。
(1) WQ_FLAG_EXCLUSIVE
上述场景中看到,当某进程调用 wake up 函数唤醒等待队列时,队列上所有的进程均被唤醒,在某些场合会出现唤醒的所有进程中,只有某个进程获得了期望的资源,而其他进程由于资源被占用不得不再次进入休眠。如果等待队列中进程数量庞大时,该行为将影响系统性能。
内核增加了"独占等待”(WQ_FLAG_EXCLUSIVE)来解决此类问题。一个独占等待的行为和通常的休眠类似,但有如下两个重要的不同:
- 等待队列元素设置 WQ_FLAG_EXCLUSIVE 标志时,会被添加到等待队列的尾部,而非头部。
- 在某等待队列上调用 wake up 时,执行独占等待的进程每次只会唤醒其中第一个(所有非独占等待进程仍会被同时唤醒)。
(2) WQ_FLAG_WOKEN
暂时还未理解,TODO
(3) WQ_FLAG_BOOKMARK
用于 wake_up() 唤醒等待队列时实现分段遍历,减少单次对自旋锁的占用时间。
2.2 等待队列的创建和初始化
等待队列头的定义和初始化有两种方式: init_waitqueue_head(&wq_head) 和宏定义 DECLARE_WAITQUEUE(name, task) 。
#define init_waitqueue_head(wq_head) \
do { \
static struct lock_class_key __key; \
__init_waitqueue_head((wq_head), #wq_head, &__key); \
} while (0) 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);
}
#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 } }
2.3 等待队列元素的创建和初始化
创建等待队列元素较为普遍的一种方式是调用宏定义 DECLARE_WAITQUEUE(name, task) ,将定义一个名为 name 的等待队列元素, private 数据指向给定的关联进程结构体 task ,唤醒函数为 default_wake_function() 。后文介绍唤醒细节时详细介绍唤醒函数的工作。
#define DECLARE_WAITQUEUE(name, tsk) \
struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk) #define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \
.entry = { NULL, NULL } }
内核源码中还存在其他定义等待队列元素的方式,调用宏定义 DEFINE_WAIT(name) 和 init_wait(&wait_queue) 。
这两种方式都将当前进程(current)关联到所定义的等待队列上,唤醒函数为 autoremove_wake_function() ,注意此函数与上述宏定义方式时不同(上述定义中使用 default_wake_function() )。
下文也将介绍 DEFINE_WAIT() 和 DECLARE_WAITQUEUE() 在使用场合上的不同。
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function) #define DEFINE_WAIT_FUNC(name, function) \
struct wait_queue_entry name = { \
.private = current, \
.func = function, \
.entry = LIST_HEAD_INIT((name).entry), \
}
#define init_wait(wait) \
do { \
(wait)->private = current; \
(wait)->func = autoremove_wake_function; \
INIT_LIST_HEAD(&(wait)->entry); \
(wait)->flags = 0; \
} while (0)
2.4 添加和移除等待队列
内核提供了两个函数(定义在 kernel/sched/wait.c )用于将等待队列元素 wq_entry 添加到等待队列 wq_head 中: add_wait_queue() 和 add_wait_queue_exclusive() 。
- add_wait_queue() :在等待队列头部添加普通的等待队列元素(非独占等待,清除 WQ_FLAG_EXCLUSIVE 标志)。
- add_wait_queue_exclusive() :在等待队列尾部添加独占等待队列元素(设置了 WQ_FLAG_EXCLUSIVE 标志)。
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags; // 清除WQ_FLAG_EXCLUSIVE标志
wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
__add_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
} 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);
}
void add_wait_queue_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags; // 设置WQ_FLAG_EXCLUSIVE标志
wq_entry->flags |= WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
__add_wait_queue_entry_tail(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
} 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);
}
remove_wait_queue() 函数用于将等待队列元素 wq_entry 从等待队列 wq_head 中移除。
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags; spin_lock_irqsave(&wq_head->lock, flags);
__remove_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
} static inline void
__remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
list_del(&wq_entry->entry);
}
添加和移除等待队列的示意图如下所示:
3. 等待事件
内核中提供了等待事件 wait_event() 宏(以及它的几个变种),可用于实现简单的进程休眠,等待直至某个条件成立,主要包括如下几个定义:
wait_event(wq_head, condition)
wait_event_timeout(wq_head, condition, timeout)
wait_event_interruptible(wq_head, condition)
wait_event_interruptible_timeout(wq_head, condition, timeout)
io_wait_event(wq_head, condition)
上述所有形式函数中, wq_head 是等待队列头(采用”值传递“的方式传输函数), condition 是任意一个布尔表达式。使用 wait_event ,进程将被置于非中断休眠,而使用 wait_event_interruptible 时,进程可以被信号中断。
另外两个版本 wait_event_timeout 和 wait_event_interruptible_timeout 会使进程只等待限定的时间(以jiffy表示,给定时间到期时,宏均会返回0,而无论 condition 为何值)。
详细介绍 wait_event() 函数的实现原理。
#define wait_event(wq_head, condition) \
do { \
might_sleep(); \
// 如果condition满足,提前返回 \
if (condition) \
break; \
__wait_event(wq_head, condition); \
} while (0) #define __wait_event(wq_head, condition) \
(void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, schedule()) /* 定义等待队列元素,并将元素加入到等待队列中
* 循环判断等待条件condition是否满足,若条件满足,或者接收到中断信号,等待结束,函数返回
* 若condition满足,返回0;否则返回-ERESTARTSYS
*/
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({ \
__label__ __out; \
struct wait_queue_entry __wq_entry; \
long __ret = ret; /* explicit shadow */ \
\
// 初始化等待队列元素__wq_entry,关联当前进程,根据exclusive参数初始化属性标志 \
// 唤醒函数为autoremove_wake_function() \
init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
// 等待事件循环 \
for (;;) { \
// 如果进程可被信号中断并且刚好有信号挂起,返回-ERESTARTSYS \
// 否则,将等待队列元素加入等待队列,并且设置进程状态,返回0 \
long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
\
// 当前进程让出调度器前,判断condition是否成立。若成立,提前结束,后续将返回0 \
if (condition) \
break; \
\
// 当前进程让出调度器前,判断当前进程是否接收到中断信号(或KILL信号) \
// 如果成立,将提前返回-ERESTARTSYS \
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
goto __out; \
} \
\
// 此处实际执行schedule(),当前进程让出调度器 \
cmd; \
} \
// 设置进程为可运行状态,并且将等待队列元素从等待队列中删除 \
finish_wait(&wq_head, &__wq_entry); \
__out: __ret; \
}) 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);
} 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);
// 返回非0值条件:可被信号中断并且确实有信号挂起
if (signal_pending_state(state, current)) {
// 将等待队列元素从等待队列中删除,返回-ERESTARTSYS
list_del_init(&wq_entry->entry);
ret = -ERESTARTSYS;
} else {
// 判断wq_entry->entry是否为空,即等待队列元素是否已经被添加到等待队列中
if (list_empty(&wq_entry->entry)) {
// WQ_FLAG_EXCLUSIVE标志被设置时,将等待队列元素添加到等待队列尾部(独占等待)
// 否则,将等待队列元素添加到等待队列头部。同2.1中对WQ_FLAG_EXCLUSIVE标志介绍。
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;
} // 用state_value改变当前的进程状态,并且执行了一次内存屏障
// 注意,只是改变了调度器处理该进程的方式,但尚未使该进程让出处理器
#define set_current_state(state_value) \
do { \
WARN_ON_ONCE(is_special_task_state(state_value));\
current->task_state_change = _THIS_IP_; \
smp_store_mb(current->state, (state_value)); \
} while (0) /* 设置进程为可运行状态,并且将等待队列元素从等待队列中删除 */
void finish_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;
// 将当前进程状态改为可运行状态(TASK_RUNNING)
// 类似set_current_state(),差别在于未进行内存屏障
__set_current_state(TASK_RUNNING); // 等待队列元素若仍在等待队列中,则将其删除
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);
}
}
经过源码分析可以看到, wait_event 使进程进入非中断休眠状态,循环等待直至特定条件满足,否则进程继续保持休眠状态。
可以简单总结出使用等待队列使进程休眠的一般步骤:
- 将当前进程关联的等待队列元素加入到等待队列中。 __add_wait_queue()/__add_wait_queue_entry_tail()
- 设置当前进程状态(可中断 TASK_INTERRUPTIBLE 或不可中断 TASK_UNINTERRUPTIBLE)。 set_current_state()
- 判断资源是否得到,或是否捕获中断信号。
- 进程让出调度器,进入休眠状态。 schedule()
- 资源得到满足时,将等待队列元素从等待队列中移除。
4. 等待队列唤醒
前文已经简单提到, wake_up 函数可用于将等待队列上的所有进程唤醒,和 wait_event 相对应, wake_up 函数也包括多个变体。主要包括:
wake_up(&wq_head)
wake_up_interruptible(&wq_head)
wake_up_nr(&wq_head, nr)
wake_up_interruptible_nr(&wq_head, nr)
wake_up_interruptible_all(&wq_head)
4.1 wake_up()
wake_up() 可以用来唤醒等待队列上的所有进程,而 wake_up_interruptible() 只会唤醒那些执行可中断休眠的进程。因此约定, wait_event() 和 wake_up() 搭配使用,而 wait_event_interruptible() 和 wake_up_interruptible() 搭配使用。
前文提到,对于独占等待的进程, wake_up() 只会唤醒第一个独占等待进程。 wake_up_nr() 函数提供功能,它能唤醒给定数目nr个独占等待进程,而不是只有一个。
wake_up() 函数的实现如下:
#define TASK_NORMAL (TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE)
// 可以看出wake_up函数将唤醒TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的所有进程
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL) 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);
} 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); // 第一次尝试调用__wake_up_common(),如果需要进行BOOKMARK过程,bookmark.flags会被置为WQ_FLAG_BOOKMARK
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); // 如果还有需要处理的元素,那么bookmark.flags肯定置上WQ_FLAG_BOOKMARK;否则,在一个loop内便处理完成
while (bookmark.flags & WQ_FLAG_BOOKMARK) {
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);
}
} #define WAITQUEUE_WALK_BREAK_CNT 64 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); // 如果bookmark元素中标志`WQ_FLAG_BOOKMARK`已被设置,则curr被设置为bookmark下一个元素
// 同时将bookmark从等待队列中删除,bookmark->flags清零
// 否则,curr设置为等待队列wq_head的第一个元素(实际上为第一次调用__wake_up_common)
if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
curr = list_next_entry(bookmark, entry); list_del(&bookmark->entry);
bookmark->flags = 0;
} else
curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry); if (&curr->entry == &wq_head->head)
return nr_exclusive; // 在等待队列头指向的链表上,从curr指向的元素开始依次遍历元素
list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
unsigned flags = curr->flags;
int ret; // 跳过标记为WQ_FLAG_BOOKMARK的元素,等待队列元素被置上WQ_FLAG_BOOKMARK?
if (flags & WQ_FLAG_BOOKMARK)
continue; // 调用等待队列元素绑定的唤醒回调函数
// 注意,具体唤醒何种进程(TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE),作为参数传递给唤醒函数处理
// 当进程不符合唤醒条件时,ret为0,详见try_to_wake_up()
ret = curr->func(curr, mode, wake_flags, key);
if (ret < 0)
break; // 如果当前等待队列元素为独占等待,并且独占等待个数已经等于nr_exclusive,提前退出循环
// 如2.1所述,独占等待进程被加入到等待队列的尾部,因此此时表明所有唤醒工作已经完成
if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break; // 连续唤醒的进程数目达到指定数目WAITQUEUE_WALK_BREAK_CNT(仍有进程元素需要处理),
// 标记bookmark->flags为WQ_FLAG_BOOKMARK,同时将下一个要处理的元素添加到bookmark作为头节点的链表尾部,并退出遍历循环
// 通过这种机制,实现了进程分批次唤醒,避免了等待队列中自旋锁被持有时间过长
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;
}
wake_up() 函数会遍历等待队列上的所有元素(包括TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE)),根据 nr_exclusive 参数的要求唤醒进程,同时实现了分批次唤醒工作。最终会回调等待队列元素所绑定的唤醒函数。
前文已经提到,定义等待队列元素时主要涉及到两种唤醒回调函数:
- default_wake_function() :宏定义 DECLARE_WAITQUEUE(name, tsk) 使用的唤醒函数。
- autoremove_wake_function() : DEFINE_WAIT(name) , init_wait(wait) 和 wait_event() 中调用的 init_wait_entry() 使用此唤醒函数。
4.2 default_wake_function()
int default_wake_function(wait_queue_entry_t *curr, unsigned mode, int wake_flags, void *key)
{
return try_to_wake_up(curr->private, mode, wake_flags);
} static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
unsigned long flags;
int cpu, success = 0; raw_spin_lock_irqsave(&p->pi_lock, flags);
smp_mb__after_spinlock();
// 此处对进程的状态进行筛选,跳过不符合状态的进程(TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE)
if (!(p->state & state))
goto out; trace_sched_waking(p); /* We're going to change ->state: */
success = 1;
cpu = task_cpu(p); smp_rmb();
if (p->on_rq && ttwu_remote(p, wake_flags))
goto stat; ... ... // Try-To-Wake-Up
ttwu_queue(p, cpu, wake_flags);
stat:
ttwu_stat(p, cpu, wake_flags);
out:
raw_spin_unlock_irqrestore(&p->pi_lock, flags); return success;
} static void ttwu_queue(struct task_struct *p, int cpu, int wake_flags)
{
struct rq *rq = cpu_rq(cpu);
struct rq_flags rf; ... ...
rq_lock(rq, &rf);
update_rq_clock(rq);
ttwu_do_activate(rq, p, wake_flags, &rf);
rq_unlock(rq, &rf);
} static void
ttwu_do_activate(struct rq *rq, struct task_struct *p, int wake_flags,
struct rq_flags *rf)
{
int en_flags = ENQUEUE_WAKEUP | ENQUEUE_NOCLOCK; lockdep_assert_held(&rq->lock); ... ...
activate_task(rq, p, en_flags);
ttwu_do_wakeup(rq, p, wake_flags, rf);
} /*
* Mark the task runnable and perform wakeup-preemption.
*/
static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags,
struct rq_flags *rf)
{
check_preempt_curr(rq, p, wake_flags);
p->state = TASK_RUNNING;
trace_sched_wakeup(p);
... ...
}
从函数调用过程中可以看到, default_wake_function() 实现唤醒进程的过程为:
default_wake_function() --> try_to_wake_up() --> ttwu_queue() --> ttwu_do_activate() --> ttwu_do_wakeup()
值得一提的是, default_wake_function() 的实现中并未将等待队列元素从等待队列中删除。因此,编写程序时不能忘记添加步骤将等待队列元素从等待队列元素中删除。
4.3 autoremove_wake_function()
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(&wq_entry->entry); return ret;
}
autoremove_wake_function() 相比于 default_wake_function() ,在成功执行进程唤醒工作后,会自动将等待队列元素从等待队列中移除。
5. 源码实例
等待队列在内核中有着广泛的运用,此处以 MMC 驱动子系统中 mmc_claim_host() 和 mmc_release_host() 来说明等待队列的运用实例。
mmc_claim_host() 的功能为:借助等待队列申请获得 MMC 主控制器 (host) 的使用权,相对应, mmc_release_host() 则是放弃 host 使用权,并唤醒所有等待队列上的进程。
static inline void mmc_claim_host(struct mmc_host *host)
{
__mmc_claim_host(host, NULL, NULL);
} int __mmc_claim_host(struct mmc_host *host, struct mmc_ctx *ctx, atomic_t *abort)
{
struct task_struct *task = ctx ? NULL : current; // 定义等待队列元素,关联当前进程,唤醒回调函数为default_wake_function()
DECLARE_WAITQUEUE(wait, current);
unsigned long flags;
int stop;
bool pm = false; might_sleep(); // 将当前等待队列元素加入到等待队列host->wq中
add_wait_queue(&host->wq, &wait);
spin_lock_irqsave(&host->lock, flags);
while (1) {
// 当前进程状态设置为 TASK_UPINTERRUPTIBLE,此时仍未让出CPU
set_current_state(TASK_UNINTERRUPTIBLE);
stop = abort ? atomic_read(abort) : 0;
// 真正让出CPU前判断等待的资源是否已经得到
if (stop || !host->claimed || mmc_ctx_matches(host, ctx, task))
break;
spin_unlock_irqrestore(&host->lock, flags);
// 调用调度器,让出CPU,当前进程可进入休眠
schedule();
spin_lock_irqsave(&host->lock, flags);
}
// 从休眠中恢复,设置当前进程状态为可运行(TASK_RUNNING)
set_current_state(TASK_RUNNING);
if (!stop) {
host->claimed = 1;
mmc_ctx_set_claimer(host, ctx, task);
host->claim_cnt += 1;
if (host->claim_cnt == 1)
pm = true;
} else
// 可利用abort参数执行一次等待队列唤醒工作
wake_up(&host->wq);
spin_unlock_irqrestore(&host->lock, flags); // 等待队列结束,将等待队列元素从等待队列中移除
remove_wait_queue(&host->wq, &wait); if (pm)
pm_runtime_get_sync(mmc_dev(host)); return stop;
} void mmc_release_host(struct mmc_host *host)
{
unsigned long flags; WARN_ON(!host->claimed); spin_lock_irqsave(&host->lock, flags);
if (--host->claim_cnt) {
/* Release for nested claim */
spin_unlock_irqrestore(&host->lock, flags);
} else {
host->claimed = 0;
host->claimer->task = NULL;
host->claimer = NULL;
spin_unlock_irqrestore(&host->lock, flags); // 唤醒等待队列host->wq上的所有进程
wake_up(&host->wq);
pm_runtime_mark_last_busy(mmc_dev(host));
if (host->caps & MMC_CAP_SYNC_RUNTIME_PM)
pm_runtime_put_sync_suspend(mmc_dev(host));
else
pm_runtime_put_autosuspend(mmc_dev(host));
}
}
从源码实现过程可以看到,此实例中等待队列的使用和第3节中总结得基本过程一致,使用到的函数依次为:
- DECLARE_WAITQUEUE(wait, current)
- add_wait_queue(&host->wq, &wait)
- set_current_state(TASK_UNINTERRUPTIBLE)
- schedule()
- set_current_state(TASK_RUNNING)
- remove_wait_queue(&host->wq, &wait)
6. 另一种休眠方式
回顾上文的介绍,2.3节中介绍了另外一种初始化等待队列元素的方式 DEFINE_WAIT() ,而至目前仍未见使用。实际上此宏定义和另一个函数搭配使用: prepare_to_wait() 。
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);
}
可以看到 prepare_to_wait() 实际做的事情也就是将等待队列元素加入到等待队列中,然后更新当前进程状态。可以看出此过程依旧符合之前介绍的等待队列一般使用流程,只是内核源码将部分流程封装成为此函数。
prepare_to_wait() 配合 finish_wait() 函数可实现等待队列。
7. 总结
综上文分析,等待队列的使用主要有三种方式:
(1) 等待事件方式
wait_event() 和 wake_up() 函数配合,实现进程阻塞睡眠和唤醒。
(2) 手动休眠方式1
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);
(3) 手动休眠方式2(借助内核封装函数)
DELARE_WAIT_QUEUE_HEAD(queue);
DEFINE_WAIT(wait); while (! condition) {
prepare_to_wait(&queue, &wait, TASK_INTERRUPTIBLE);
if (! condition)
schedule();
finish_wait(&queue, &wait)
}
参考资料
[1] LINUX 设备驱动程序(LDD3),2012年
[2] Linux设备驱动开发详解(基于最新的Linux4.0内核),宋宝华编著,2016年
[3] linux设备驱动模型:https://blog.csdn.net/qq_40732350/article/details/82992904
[4] Linux 等待队列 (wait queue):https://xyfu.me/posts/236f51d8/
[5] Linux Wait Queue 等待队列:https://www.cnblogs.com/gctech/p/6872301.html
[6] 源码解读Linux等待队列:http://gityuan.com/2018/12/02/linux-wait-queue/
[7] Driver porting: sleeping and waking up:https://lwn.net/Articles/22913/
Linux等待队列(Wait Queue)的更多相关文章
- Linux等待队列与唤醒
1.数据结构 1.1等待队列头 struct __wait_queue_head { spinlock_t lock; struct list_head task_list; }; typedef s ...
- linux中的阻塞机制及等待队列
阻塞与非阻塞是设备访问的两种方式.驱动程序需要提供阻塞(等待队列,中断)和非阻塞方式(轮询,异步通知)访问设备.在写阻塞与非阻塞的驱动程序时,经常用到等待队列. 一.阻塞与非阻塞 阻塞调用是没有获得资 ...
- linux中的阻塞机制及等待队列【转】
转自:http://www.cnblogs.com/gdk-0078/p/5172941.html 阻塞与非阻塞是设备访问的两种方式.驱动程序需要提供阻塞(等待队列,中断)和非阻塞方式(轮询,异步通知 ...
- 深入理解Linux内核 学习笔记(3)
第三章 进程 可以看到很多熟悉的结构体 进程状态: 可运行状态(TASK_ RUNNING) 进程要么在CPU上执行,要么准备执行. 可巾断的等待状态(TASK_ INTERRUPTIBLE) 进程被 ...
- Linux内核入门到放弃-内核活动-《深入Linux内核架构》笔记
中断 中断类型 同步中断和异常.这些由CPU自身产生,针对当前执行的程序 异步中断.这是经典的中断类型,由外部设备产生,可能发生在任意时间. 在退出中断中,内核会检查下列事项. 调度器是否应该选择一个 ...
- Linux内核同步
Linux内核剖析 之 内核同步 主要内容 1.内核请求何时以交错(interleave)的方式执行以及交错程度如何. 2.内核所实现的基本同步机制. 3.通常情况下如何使用内核提供的同步机制. 内核 ...
- Linux驱动总结3- unlocked_ioctl和堵塞(waitqueue)读写函数的实现 【转】
转自:http://blog.chinaunix.net/uid-20937170-id-3033633.html 学习了驱动程序的设计,感觉在学习驱动的同时学习linux内核,也是很不错的过程哦,做 ...
- LINUX按键驱动程序
<<混杂设备驱动模型>> <混杂设设备的描述> <混在设备的概念> 在linux系统中,存在一类字符设备,他们拥有相同的主设备号(10),但是次设备号不 ...
- Linux内核线程之深入浅出【转】
转自:http://blog.csdn.net/yiyeguzhou100/article/details/53126626 [-] 线程和进程的差别 线程的分类 1 内核线程 2 轻 ...
随机推荐
- Java多线程_生产者消费者模式2
在我的上一条博客中,已经介绍到了多线程的经典案列——生产者消费者模式,但是在上篇中用的是传统的麻烦的非阻塞队列实现的.在这篇博客中我将介绍另一种方式就是:用阻塞队列完成生产者消费者模式,可以使用多种阻 ...
- 使用 Swift Package Manager 集成依赖库
本文首发于 Ficow Shen's Blog,原文地址: 使用 Swift Package Manager 集成依赖库. 内容概览 前言 添加依赖包 在项目中使用依赖 管理已导入的依赖 在团 ...
- [CSP-S2019]Emiya 家今天的饭 题解
CSP-S2 2019 D2T1 很不错的一题DP,通过这道题学到了很多. 身为一个对DP一窍不通的蒟蒻,在考场上还挣扎了1h来推式子,居然还有几次几乎推出正解,然而最后还是只能打个32分的暴搜滚粗 ...
- mysql8.0的下载、安装、可视化软件(下载、安装、破解)
获取下面相关资源,请关注微信公众号“带你做毕设或者添加小编微信,有不明白的联系小编,可以提供远程帮助哦 mysql压缩版最新版本下载地址https://dev.mysql.com/downloads/ ...
- 详细分析栈和队列的数据结构的实现过程(Java 实现)
目录 栈和队列的数据结构的实现过程(Java 实现) 栈的数据结构的实现 栈的基础知识回顾 栈的常见应用 基于数组的栈的实现 具体代码设计 基于数组的栈简单的时间复杂度分析 关于栈的一个算法应用:括号 ...
- 教你如何在linux操作系统下玩【俄罗斯方块】高清+语音教程
主讲人小冰QQ:986945193 新浪微博:http://weibo.com/mcxiaobing 百度贴吧:忆驹家族小冰 腾讯微博:http://t.qq.com/q986945193 高清视频 ...
- Photon PUN 二 大厅 & 房间
一, 简介 玩过 LOL , dota2, 王者荣耀 等MOBA类的游戏,就很容易理解大厅和房间了. LOL中一个服务器就相当与一个大厅; 什么电一,电二 ,,, 联通一区等 每一个区就相当于一个大厅 ...
- 记一次内存飙升的Windbg
背景 突然间接到运维的报警,我们一个服务,内存找过了6GB的占用.才6GB 也不是很大,因为在处理别的事情,服务dump一下暂时一放,然后半小时之后,接到了运维的Kafka堆积报警.然后切换着重启了一 ...
- UEFI、BIOS、GPT、MBR等概念的辨析
(本文转移自本人的旧博客) 从各个地方包括知乎,Wiki,CSDN搜索到的一些整理,这些概念极易混淆. 先说互相的关系 BIOS和UEFI是两种固件接口标准 MBR和GPT是两种分区表 Legacy模 ...
- jzoj 6798. 【2014广州市选day2】regions
Description 在平面上堆叠着若干矩形,这些矩形的四边与平面X坐标轴或Y坐标轴平行.下图展示了其中一种情况,3个矩形的边将平面划分成8个区域: 下面展示了另一种稍稍复杂一些的情况: 你的任务是 ...