[Linux]学习笔记系列 -- [kernel]wait
Linux内核中的等待队列(Wait Queues)是用于实现进程阻塞与唤醒的核心同步机制,解决了CPU资源高效利用问题。其核心原理是通过wait_queue_head_t和wait_queue_entry_t数据结构实现"等待-唤醒"协议:进程在条件不满足时加入队列睡眠,条件满足后被生产者唤醒。主要优势在于高效性、通用性和灵活性,支持可中断/不可中断睡眠及独占/广播唤醒。广泛
title: wait
categories:
- linux
- kernel
tags: - linux
- kernel
abbrlink: 7dee83e5
date: 2025-10-03 09:01:49
文章目录
- kernel/sched/wait 等待队列(Wait Queues) 内核同步与阻塞的核心机制
- include/linux/wait.h
- kernel/sched/wait.c
- kernel/sched/wait_bit.c
- kernel/sched/swait 简单等待队列(Simple Wait Queues) 一种轻量级的内核阻塞原语
- include/linux/swait.h
- kernel/sched/swait.c
https://github.com/wdfk-prog/linux-study
kernel/sched/wait 等待队列(Wait Queues) 内核同步与阻塞的核心机制
历史与背景
这项技术是为了解决什么特定问题而诞生的?
kernel/sched/wait.c 实现的等待队列(Wait Queues)机制是为了解决多任务操作系统中最基本、最普遍的同步问题:如何让一个任务(进程或线程)在某个特定条件尚不满足时,能够高效地暂停执行(睡眠),并在条件满足时被其他任务唤醒。
在没有等待队列的情况下,一个任务要等待某个事件,只能采用**忙等待(Busy-Waiting)**的方式,即在一个循环中不断地检查条件是否满足。这种方式会100%占用CPU时间,造成巨大的资源浪费,严重降低系统整体性能。
等待队列机制的诞生就是为了取代忙等待,它解决了以下核心问题:
- CPU资源利用:允许等待的进程放弃CPU,进入睡眠状态,从而让CPU可以去执行其他有用的工作。
- 生产者-消费者模型:为经典的生产者-消费者问题提供了基础解决方案。当缓冲区为空时,消费者进程需要睡眠等待;当生产者向缓冲区放入数据后,需要唤醒消费者。
- 通用同步原语:提供一个通用的、底层的同步原语,内核中其他更高级的同步机制,如信号量(Semaphores)、互斥锁(Mutexes)、完成量(Completions)和Futex,都是基于等待队列构建的。
它的发展经历了哪些重要的里程碑或版本迭代?
等待队列的概念自Unix诞生之初就已存在,在Linux中的演进主要体现在效率和功能的精细化上。
- 基础实现:早期的实现提供了基本的睡眠/唤醒功能。
- 独占式等待(Exclusive Wait):这是一个重要的里程碑。最初的
wake_up()会唤醒等待队列上的所有进程,这在某些场景下会导致“惊群效应(Thundering Herd)”——大量进程被唤醒,但只有一个能成功获取资源,其余的又得重新睡眠,造成了不必要的调度开销。引入独占式等待(WQ_FLAG_EXCLUSIVE)后,wake_up()只会唤醒一个独占式等待的进程,大大提高了效率。 - 可中断睡眠(Interruptible Sleep):区分了
TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE两种睡眠状态。处于可中断睡眠的进程不仅可以被显式地唤醒,还可以被信号(Signal)中断。这对于提升系统的响应性和健壮性至关重要,允许用户(例如通过Ctrl+C)终止一个被不当阻塞的进程。 - 与Futex集成:等待队列成为内核实现
futex(Fast Userspace Mutex)的关键部分,而futex是现代用户空间多线程库(如glibc的NPTL)实现高性能锁和条件变量的基础。
目前该技术的社区活跃度和主流应用情况如何?
kernel/sched/wait.c的代码是内核中最稳定、最核心的部分之一。其基本原理和接口很少发生根本性变化。社区的活动主要集中在修复一些与等待队列相关的、非常微妙的竞态条件(Race Condition)bug,以及在新的内核子系统和驱动中正确地使用它。
它的应用遍布Linux内核的每一个角落,是内核的“血液”:
- 所有阻塞式I/O:当进程对一个空的管道(pipe)或无数据的套接字(socket)进行
read()时,它就会在等待队列上睡眠。 - 所有同步原语:
mutex_lock(),down()(semaphore),wait_for_completion()的底层都会在获取不到锁或资源时,使用等待队列来挂起当前进程。 select/poll/epoll:这些I/O多路复用机制的核心就是将当前进程同时加入到所有被监视的文件描述符的等待队列上。
核心原理与设计
它的核心工作原理是什么?
等待队列的核心是两个数据结构和一套标准的“等待-唤醒”协议。
数据结构:
wait_queue_head_t: 代表一个等待队列的“头部”。它包含一个自旋锁和一个链表头,是所有等待者和唤醒者共同操作的目标。wait_queue_entry_t: 代表一个在队列中等待的“节点”。每个等待的进程都会在自己的内核栈上创建一个这样的节点,节点中包含一个指向该进程task_struct的指针。
核心协议:
1. 等待者(消费者)的流程:
a. 定义与初始化:在栈上定义一个wait_queue_entry_t。
b. 加入队列:调用add_wait_queue()将自己的节点加入到目标的wait_queue_head_t的链表中。
c. 循环检查:必须在一个循环中进行等待,以处理“伪唤醒(Spurious Wakeup)”:c while (!condition_is_met) { // 将自身状态设置为可中断或不可中断睡眠 set_current_state(TASK_INTERRUPTIBLE); // 如果条件已满足,则跳出循环 if (condition_is_met) break; // 放弃CPU,进入睡眠 schedule(); }
d. 离开队列:条件满足后,调用remove_wait_queue()将自己的节点从链表中移除。
内核提供了prepare_to_wait()和finish_wait()等宏来简化这个过程。
2. 唤醒者(生产者)的流程:
a. 满足条件:生产者完成其工作,使得等待者所等待的条件成立(例如,向缓冲区写入了数据)。
b. 执行唤醒:调用wake_up()或wake_up_interruptible()等函数,操作同一个wait_queue_head_t。
c. 唤醒逻辑:wake_up()函数会获取等待队列头部的锁,遍历链表中的wait_queue_entry_t节点,找到对应的进程,并调用try_to_wake_up()将其状态从睡眠更改为TASK_RUNNING,然后将其放回调度器的运行队列中。
它的主要优势体现在哪些方面?
- 高效性:它使CPU利用率最大化。进程在等待时完全不消耗CPU周期。
- 通用性:它是一个非常底层的构建块,可以用来实现几乎任何形式的同步逻辑。
- 灵活性:支持可中断和不可中断的等待,以及独占式和广播式的唤醒,可以满足各种复杂场景的需求。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 编程复杂,易于出错:直接使用等待队列需要开发者手动处理状态设置、条件检查和竞态条件,逻辑比较复杂。特别是“伪唤醒”问题要求必须在循环中检查条件,这是新手常犯的错误。
- 惊群效应:如果错误地使用了
wake_up_all()(或类似的广播唤醒),而实际上只有一个等待者能够继续执行,会造成不必要的调度开销。 - 不可在原子上下文中使用:等待队列的本质是让进程睡眠,而睡眠是绝对禁止在硬中断、软中断或持有自旋锁的上下文(统称原子上下文)中发生的。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
直接使用等待队列通常是在构建新的、自定义的同步机制时,或者是内核中无法用更高级原语简单描述的复杂条件等待场景。
- I/O多路复用:
poll()机制的实现是等待队列的经典应用。它需要将当前进程添加到多个等待队列上,任何一个队列的事件都能将其唤醒。这种“等待多个事件之一”的逻辑无法用简单的信号量或互斥锁实现。 - 驱动中的数据到达通知:一个字符设备驱动,当用户调用
read()但设备尚无数据时,驱动可以将用户进程放入一个等待队列中。当硬件通过中断通知数据到达时,中断处理程序的下半部会调用wake_up()唤醒等待的进程。 - 构建其他同步原语:在内核中实现一个新的锁类型或同步工具时,等待队列是其底层不可或缺的组成部分。
是否有不推荐使用该技术的场景?为什么?
- 简单的互斥访问:如果只是为了保护一个临界区,防止多个线程同时进入,应该使用
mutex。它提供了更简单的mutex_lock/unlock接口,并处理了所有权等问题。 - 简单的完成信号:如果一个线程需要等待另一个线程完成某个一次性任务,应该使用
completion。它提供了更简洁的wait_for_completion和complete接口。 - 在原子上下文中的任何等待:如上所述,绝对禁止。在这些场景下,如果需要等待,必须使用自旋锁(Spinlock)进行忙等待。
对比分析
请将其 与 其他相似技术 进行详细对比。
| 特性 | 等待队列 (Wait Queue) | 自旋锁 (Spinlock) | 信号量/互斥锁 (Semaphore/Mutex) | 完成量 (Completion) |
|---|---|---|---|---|
| 基本行为 | 睡眠 (放弃CPU)。 | 自旋 (忙等待,占用CPU)。 | 睡眠 (基于等待队列)。 | 睡眠 (基于等待队列)。 |
| 使用上下文 | 进程上下文 (可以睡眠)。 | 任何上下文 (硬中断、软中断、进程)。 | 进程上下文 (可以睡眠)。 | 进程上下文 (可以睡眠)。 |
| 等待时间 | 适用于长时间的等待。 | 只适用于极短时间的等待。 | 适用于长时间的等待。 | 适用于长时间的等待。 |
| 抽象层次 | 底层原语。编程复杂,灵活。 | 底层原语。用于硬件级并发控制。 | 高层抽象。提供结构化的锁(互斥锁)或计数(信号量)功能。 | 高层抽象。专门用于“任务完成”的信号通知。 |
| 核心用途 | 等待任意的、自定义的布尔条件成立。 | 保护临界区,防止多CPU并发访问。 | 保护临界区或管理有限的资源。 | 一个线程等待另一个线程完成特定工作。 |
include/linux/wait.h
init_waitqueue_head 初始化等待队列头
#define init_waitqueue_head(wq_head) \
do { \
/*
* 如果禁用 lockdep,则类键不占用空间:
* struct lock_class_key { };
*/
static struct lock_class_key __key; \
\
__init_waitqueue_head((wq_head), #wq_head, &__key); \
} while (0)
__add_wait_queue_entry_tail 添加等待队列条目尾部
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);
}
__add_wait_queue 添加等待队列条目
static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
struct list_head *head = &wq_head->head;
struct wait_queue_entry *wq;
list_for_each_entry(wq, &wq_head->head, entry) {
/* 当前条目没有设置优先级标志(WQ_FLAG_PRIORITY),则停止遍历 */
if (!(wq->flags & WQ_FLAG_PRIORITY))
break;
/* 如果条目具有优先级标志,更新插入位置为当前条目的后面(wq->entry) */
head = &wq->entry;
}
list_add(&wq_entry->entry, head);
}
wait_event: 睡眠直到某个条件成立
此宏是Linux内核中一个基础且非常重要的同步原语. 它的核心作用是让当前任务(进程或内核线程)进入睡眠状态, 直到指定的 C 语言表达式 condition 的计算结果为真. 这是一个安全的睡眠机制, 它通过与等待队列 (waitqueue) 和唤醒机制 (wake_up) 配合使用, 避免了竞态条件.
wait_event(wq_head, condition) (顶层宏)
这是提供给内核开发者使用的公共接口.
/*
* wait_event - 睡眠直到某个条件成立
* @wq_head: 用于等待的等待队列头.
* @condition: 一个C语言表达式, 作为等待的事件条件.
*
* 进程会被置于不可中断睡眠状态(TASK_UNINTERRUPTIBLE), 直到 @condition 的计算结果为真.
* 每当 @wq_head 等待队列被唤醒时, @condition 都会被重新检查.
*
* 在改变任何可能影响等待条件结果的变量后, 必须调用 wake_up().
*/
#define wait_event(wq_head, condition) \
do { \
/*
* might_sleep() 是一个调试辅助宏. 它会通知内核的锁验证器(lockdep),
* 表明当前代码路径可能会发生睡眠. 如果在不允许睡眠的上下文(例如持有自旋锁时)
* 调用了此宏, 内核会打印警告. 在最终编译的内核中, 此宏没有运行时开销.
*/
might_sleep(); \
/*
* 这是一个重要的优化: 如果条件在调用时已经为真, 就不需要执行任何
* 设置等待队列和睡眠的复杂操作. 直接通过 break 跳出 do-while 循环,
* 函数立即返回.
*/
if (condition) \
break; \
/*
* 如果条件不满足, 则调用内部的 __wait_event 宏来执行实际的等待逻辑.
*/
__wait_event(wq_head, condition); \
/*
* do { ... } while(0) 是一个标准的C语言技巧, 它将多行语句的宏封装成一个
* 行为类似于单一语句的代码块, 可以安全地用于 if/else 等控制结构中,
* 避免因宏展开而导致的语法问题.
*/
} while (0)
__wait_event(wq_head, condition) (中层宏)
这个宏是 wait_event 的一个简单封装, 它为更底层的 ___wait_event 宏提供了默认参数.
#define __wait_event(wq_head, condition) \
/*
* (void) 用于抑制编译器关于"未使用返回值"的警告.
* 此处调用了最底层的 ___wait_event 宏, 并为其传递了固定的参数.
* wq_head: 等待队列头, 从上层透传下来.
* condition: 等待条件, 从上层透传下来.
* TASK_UNINTERRUPTIBLE: 将任务设置为不可中断睡眠状态. 在此状态下, 任务不会被信号唤醒,
* 只能被显式的 wake_up() 唤醒. 这是等待硬件或资源的典型状态.
* 0: 代表 exclusive 参数为假. 表示这不是一个排他性的等待, 允许多个任务在同一个队列上等待.
* 0: 代表 ret 参数的初始值, 对于不可中断等待, 返回值没有特殊意义.
* schedule(): 这是 cmd 参数, 即在循环中实际执行睡眠的命令. schedule() 函数会
* 启动内核调度器, 切换到另一个可运行的任务.
*/
(void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, \
schedule())
___wait_event(...) (底层核心实现)
这是实现等待逻辑的核心宏, 使用了GCC的扩展语法(语句表达式和局部标签).
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
/*
* ({ ... }) 是GCC的一个扩展, 称为"语句表达式". 它允许多个语句像一个表达式一样
* 使用, 并且可以返回一个值(表达式中最后一个语句的值). 这使得宏可以拥有局部变量.
*/
({ \
/*
* __label__ 是GCC的另一个扩展, 用于定义一个局部标签, 它的作用域仅限于当前的语句表达式.
* 这避免了与代码中其他地方的标签发生命名冲突.
*/
__label__ __out; \
/*
* 在当前任务的内核栈上定义一个等待队列条目.
*/
struct wait_queue_entry __wq_entry; \
/*
* 定义一个局部变量 __ret, 并用传入的 ret 参数初始化它.
* "explicit shadow" 注释意为, 这个局部变量 __ret "遮蔽"了同名的宏参数 ret.
*/
long __ret = ret; \
\
/*
* 初始化等待队列条目. 将其关联到当前任务(current), 并根据 exclusive 参数
* 设置 WQ_FLAG_EXCLUSIVE 标志(如果需要).
*/
init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
/*
* 这是一个无限循环, 只有当 condition 满足时才会通过 break 退出.
* 这个循环是处理"伪唤醒"的关键: 即使被唤醒, 也要重新检查条件.
*/
for (;;) { \
/*
* 调用 prepare_to_wait_event(). 这是实现安全等待的关键步骤.
* 此函数会原子地执行两个操作:
* 1. 将 __wq_entry 添加到 wq_head 等待队列的链表中.
* 2. 将当前任务的状态设置为指定的 state (此处为 TASK_UNINTERRUPTIBLE).
* 从此行代码之后, 任何对 wq_head 的 wake_up() 调用都能唤醒本任务.
*/
long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
\
/*
* 在准备好睡眠之后, 再次检查条件. 这是为了防止"丢失唤醒"的竞态条件.
* 如果在上次检查之后、到本行代码之前, 条件已经变为真, 那么就可以在这里捕获到,
* 无需实际进入睡眠状态.
*/
if (condition) \
break; \
\
/*
* 这段是为可中断睡眠(TASK_INTERRUPTIBLE)准备的. 对于 wait_event,
* state 是 TASK_UNINTERRUPTIBLE, ___wait_is_interruptible 会返回假,
* 所以这段代码不会被执行.
* 如果是可中断睡眠, 并且 __int 不为0(表示被信号唤醒), 则会直接跳转到 __out.
*/
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
goto __out; \
} \
\
/*
* 执行传入的 cmd 命令, 对于 wait_event 来说, 就是执行 schedule().
* 当前任务在此处放弃CPU, 进入睡眠, 直到被 wake_up() 唤醒.
*/
cmd; \
\
/*
* 这是一个优化. 从 schedule() 返回(被唤醒)后, 立即再次检查条件.
* 如果条件已满足, 就可以直接跳出循环, 而不必等到下一次循环的开始再检查.
*/
if (condition) \
break; \
} \
/*
* 当循环因为 condition 为真而退出后, 调用 finish_wait().
* 此函数执行与 prepare_to_wait_event() 相反的操作:
* 1. 将当前任务状态设置回 TASK_RUNNING.
* 2. 将 __wq_entry 从 wq_head 等待队列中移除.
*/
finish_wait(&wq_head, &__wq_entry); \
/*
* 局部标签的定义点.
*/
__out: /*
* 语句表达式的最后一个语句, 其值 __ret 将作为整个宏表达式的返回值.
*/
__ret; \
})
等待队列移除:任务从挂起状态中恢复
本代码片段定义了与add_wait_queue相对应的函数remove_wait_queue。其核心功能是将一个代表当前任务的等待节点(wait_queue_entry)从一个等待队列头(wait_queue_head)中安全地移除。这个操作通常在任务被唤醒并准备继续执行后,或者在任务决定放弃等待时进行,是任务同步和阻塞I/O机制中不可或缺的一环。
实现原理分析
与add_wait_queue类似,该功能也采用了包裹函数加锁、内部函数执行核心操作的设计模式,以确保操作的原子性和安全性。
-
核心操作 (
__remove_wait_queue):- 这个内联函数是实际执行移除操作的地方。它非常简洁,只调用了内核链表库中的
list_del(&wq_entry->entry)函数。 list_del是一个标准操作,它会将wq_entry->entry这个链表节点从其所在的双向链表中解开,并将其前后指针都指向自身,使其成为一个孤立的、只包含自己的链表。这个操作是原子的(相对于链表指针的修改),但它不是CPU指令级别的原子操作,因此需要外部的锁来保护。
- 这个内联函数是实际执行移除操作的地方。它非常简洁,只调用了内核链表库中的
-
安全封装 (
remove_wait_queue):- 这是供内核其他部分调用的标准接口。
- 它首先通过
spin_lock_irqsave获取等待队列的自旋锁并禁用本地中断。这与add_wait_queue中的理由完全相同:防止在修改链表指针时,被其他CPU或本地中断服务程序(ISR)中的wake_up或add_wait_queue等操作干扰,从而保证了对整个等待队列操作的原子性。 - 在锁的保护下,它调用
__remove_wait_queue来执行链表节点的移除。 - 操作完成后,通过
spin_unlock_irqrestore释放锁并恢复之前的中断状态。
一个典型的任务睡眠-唤醒-恢复流程如下:
// 1. 准备进入睡眠
DEFINE_WAIT(wait); // 在栈上创建一个wait_queue_entry
add_wait_queue(wq_head, &wait);
set_current_state(TASK_INTERRUPTIBLE);
// 2. 检查条件并可能睡眠
if (condition_is_false) {
schedule();
}
set_current_state(TASK_RUNNING);
// 3. 任务被唤醒后,或决定不等了
remove_wait_queue(wq_head, &wait);
remove_wait_queue确保了在任务继续执行其正常逻辑之前,它已经不再位于任何可能被再次唤醒的等待队列中,避免了状态混乱。
代码分析
// __remove_wait_queue: 从等待队列中移除一个等待节点(无锁版本)。
// @wq_head: 指向等待队列头的指针。
// @wq_entry: 指向要移除的等待节点的指针。
static inline void
__remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
// 调用内核链表库函数list_del,将wq_entry从其所在的双向链表中解开。
list_del(&wq_entry->entry);
}
// remove_wait_queue: 从等待队列中移除一个等待节点(加锁的安全版本)。
// @wq_head: 指向等待队列头的指针。
// @wq_entry: 指向要移除的等待节点的指针。
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);
}
// 导出符号,使得内核其他部分(包括模块)可以使用此函数。
EXPORT_SYMBOL(remove_wait_queue);
waitqueue_active & wq_has_sleeper: 等待队列的无锁状态检查
本代码片段展示了Linux内核中用于检查一个等待队列(wait_queue_head)上是否有任务正在等待的辅助函数。其核心功能是提供一个快速、无锁的方式来“窥探”等待队列的状态,这通常被用作一种优化,以避免在队列为空时执行代价更高的wake_up操作。然而,这种无锁访问引入了复杂的内存排序问题,因此wq_has_sleeper通过插入一个内存屏障来提供一个安全的版本,而waitqueue_active则是其不安全的底层实现。
实现原理分析
这段代码的精妙之处不在于其代码本身(它们只是对链表操作的简单封装),而在于它所要解决的、由无锁编程带来的微妙的竞态问题。
-
基本实现:
waitqueue_active: 直接调用!list_empty()来检查等待队列的head链表是否为空。这是最底层的检查。wq_has_single_sleeper: 调用list_is_singular()检查链表是否只有一个节点,用于某些需要唤醒单个等待者的特殊优化场景。wq_has_sleeper: 这是对外推荐的安全接口。它在调用waitqueue_active之前,先插入了一个全内存屏障 (smp_mb())。
-
核心问题:丢失的唤醒 (Lost Wakeup):
-
为什么需要
smp_mb()?内核文档中的注释给出了一个经典的“唤醒者-等待者”(Waker-Waiter)竞态场景。如果没有内存屏障,CPU和编译器为了优化,可能会对指令进行重排序,从而导致灾难性的后果。 -
失败场景分析:
- 初始状态: 一个条件变量
cond为false,等待队列wq_head为空。 - 唤醒者 (Waker, CPU0) 的代码逻辑是:
cond = true; if (waitqueue_active(&wq_head)) wake_up(&wq_head); - 等待者 (Waiter, CPU1) 的代码逻辑是:
prepare_to_wait(...); if (cond) break; schedule(); - 发生重排序: 唤醒者的CPU可能会将
waitqueue_active()(一个内存读取操作)重排到cond = true;(一个内存写入操作)之前执行。 - 竞态过程:
a. Waker 执行被重排后的waitqueue_active()。此时Waiter还没来得及把自己加入队列,所以Waker看到队列是空的。
b. Waiter 执行prepare_to_wait(),把自己加入了等待队列。
c. Waker 执行cond = true;。因为它之前看到队列是空的,所以它决定不调用wake_up(),然后就退出了。
d. Waiter 现在检查if (cond),发现cond已经是true了,于是它也退出了循环。在这种情况下,一切正常。
e. 真正的“丢失唤醒”场景:
i. Waiter 执行prepare_to_wait(),把自己加入了队列。
ii. Waker 执行重排后的waitqueue_active(),看到了非空的队列。
iii. Waiter 检查if (cond),发现还是false,于是调用schedule()进入睡眠。
iv. Waker 执行cond = true;,然后调用wake_up()。这种情况下也一切正常。
- 让我们重新审视内核注释中的关键点:
waitqueue_active()load to get hoisted over the@condstore。 - 正确的失败场景:
- Waker (CPU0) reorders: It executes
waitqueue_active(&wq_head)first. At this exact moment, the Waiter has not yet added itself to the queue. The Waker sees an empty queue. - Waiter (CPU1): Executes
prepare_to_wait(), adding itself to the queue. - Waiter (CPU1): Is about to check
cond, but gets preempted. - Waker (CPU0): Now executes
cond = true. Because it previously saw an empty queue, it does not callwake_up(). The waker’s job is done. - Waiter (CPU1): Resumes execution. It checks
if (cond), finds itfalse(because the change from the waker is not yet visible to this CPU’s cache), and callsschedule()to go to sleep. - 结果: Waiter进入了永久的睡眠。Waker已经设置了条件并检查了队列,但因为它检查得太早,所以错过了Waiter,导致了一次“丢失的唤醒”。
- Waker (CPU0) reorders: It executes
- 初始状态: 一个条件变量
-
-
解决方案:内存屏障:
smp_mb()就像一道栅栏。它强制CPU(和编译器)将所有在屏障之前的内存操作(如此处的cond = true;)全部完成,并使其结果对系统中的其他部分可见,然后才能执行在屏障之后的内存操作(如此处的waitqueue_active())。wq_has_sleeper通过内置这个内存屏障,保证了唤醒者在检查等待队列是否为空之前,它对条件变量的修改一定已经“广播”出去了。- 这与等待者在
prepare_to_wait()之后、检查条件之前的set_current_state()中隐含的另一个内存屏障相配对,共同确保了正确的事件顺序,从而杜绝了“丢失的唤醒”问题。
代码分析
/**
* @brief 无锁地测试等待队列上是否有等待者。
* @param wq_head 要测试的等待队列头。
* @return int 如果等待列表非空,则返回true。
*
* @warning 注意:此函数是无锁的,需要小心使用,不正确的使用将
* 导致零星且不明显的失败。它必须在持有等待队列的锁,
* 或者与一个配对的smp_mb()一起使用时才安全。
*/
static inline int waitqueue_active(struct wait_queue_head *wq_head)
{
/* 直接检查内部的链表是否为空。 */
return !list_empty(&wq_head->head);
}
/**
* @brief 检查等待队列上是否只有一个等待者。
* @param wq_head 等待队列头。
* @return bool 如果wq_head的列表上只有一个等待者,则返回true。
* @details 请参考 waitqueue_active 的注释以了解其使用注意事项。
*/
static inline bool wq_has_single_sleeper(struct wait_queue_head *wq_head)
{
/* 检查内部链表是否只有一个节点。 */
return list_is_singular(&wq_head->head);
}
/**
* @brief (安全地)检查是否有任何正在等待的进程。
* @param wq_head 等待队列头。
* @return bool 如果wq_head上有等待的进程,则返回true。
*
* @details 请参考 waitqueue_active 的注释以了解其使用注意事项。
* 这个函数是推荐使用的、更安全的版本。
*/
static inline bool wq_has_sleeper(struct wait_queue_head *wq_head)
{
/*
* 我们需要确保与 add_wait_queue 对等待队列的修改保持同步。
* 这个内存屏障应与等待方的另一个内存屏障(通常在
* set_current_state() 中)配对,以防止“丢失的唤醒”竞态。
*/
smp_mb();
/* 在确保内存顺序后,调用底层的无锁检查函数。 */
return waitqueue_active(wq_head);
}
kernel/sched/wait.c
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_careful(&wq_entry->entry);
return ret;
}
EXPORT_SYMBOL(autoremove_wake_function);
prepare_to_wait_event: 原子地将任务加入等待队列并设置睡眠状态
此函数是 wait_event 宏能够安全工作的基石. 它的核心作用是在一个原子操作中, 完成将当前任务加入等待队列和设置任务为睡眠状态这两个步骤. 这是为了完美地解决"丢失唤醒" (Lost Wakeup) 这一经典的并发问题.
核心原理: 如果一个任务先检查条件(为假), 然后决定睡眠, 在这两个步骤之间, 另一个任务可能已经改变了条件并执行了唤醒操作. 如果不加保护, 这个唤醒就会丢失, 导致第一个任务永久睡眠. prepare_to_wait_event 通过在一个锁的保护下执行"加入队列"和"设置状态"这两个操作, 确保了在任务被标记为可唤醒之后, 到它真正放弃CPU之前, 不会错过任何唤醒信号.
/*
* long prepare_to_wait_event(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
* @wq_head: 指向目标等待队列头的指针, 任务将在这个队列上等待.
* @wq_entry: 指向代表当前任务的等待队列条目的指针.
* @state: 任务将要被设置成的睡眠状态 (例如 TASK_UNINTERRUPTIBLE 或 TASK_INTERRUPTIBLE).
* @return: 如果因为收到信号而需要中断等待, 则返回 -ERESTARTSYS, 否则返回 0.
*/
long prepare_to_wait_event(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
/*
* 定义一个无符号长整型变量 flags, 用于保存当前CPU的中断状态.
*/
unsigned long flags;
/*
* 定义一个长整型变量 ret 作为返回值, 并初始化为0 (表示成功).
*/
long ret = 0;
/*
* 获取等待队列头中的自旋锁, 并禁用本地中断.
* 之前的中断状态(开或关)被保存在 flags 变量中.
* 这是实现原子性的关键: 从这里开始到 spin_unlock_irqrestore 之间是一个临界区.
* 在单核STM32上, 这可以防止中断处理程序或其他任务抢占此段代码的执行.
*/
spin_lock_irqsave(&wq_head->lock, flags);
/*
* 检查是否有待处理的信号, 并且指定的睡眠状态 state 是否允许被信号中断.
* 对于 wait_event 来说, state 是 TASK_UNINTERRUPTIBLE, 所以 signal_pending_state 会返回假,
* 这个 if 分支通常不会被进入. 它是为可中断睡眠(wait_event_interruptible)准备的.
*/
if (signal_pending_state(state, current)) {
/*
* 如果一个排他性等待者(exclusive waiter)已经被唤醒选中, 它就不能因为信号而失败,
* 它必须"消耗掉"我们等待的那个条件.
* 调用者会重新检查条件, 如果我们已经被唤醒, 就会返回成功. 我们不会错过事件,
* 因为唤醒操作也会锁定和解锁同一个 wq_head->lock.
* 但我们需要确保, 在我们失败后, "设置条件+唤醒"的操作看不到我们, 它应该去唤醒另一个排他性等待者.
*/
/*
* 将等待条目从它可能所在的任何链表中删除并重新初始化.
* 这是为了确保如果因为信号而提前退出, 不会在等待队列中留下一个无效的"僵尸"条目.
*/
list_del_init(&wq_entry->entry);
/*
* 设置返回值为 -ERESTARTSYS. 这是一个特殊的值, 它会告诉上层调用栈
* (例如系统调用处理层)这个等待被信号中断了, 相关的系统调用可能需要被重启.
*/
ret = -ERESTARTSYS;
} else {
/*
* 这是正常的执行路径.
* 检查等待条目是否已经在某个链表中. 它可以防止在循环等待中重复添加同一个条目.
*/
if (list_empty(&wq_entry->entry)) {
/*
* 检查此等待是否是"排他性"的.
* 排他性等待者一次只唤醒一个; 非排他性等待者会一次性全部唤醒.
* wait_event 使用的是非排他性等待.
*/
if (wq_entry->flags & WQ_FLAG_EXCLUSIVE)
/*
* 如果是排他性等待, 将条目添加到等待队列的尾部.
*/
__add_wait_queue_entry_tail(wq_head, wq_entry);
else
/*
* 如果是非排他性等待, 将条目添加到等待队列的头部.
*/
__add_wait_queue(wq_head, wq_entry);
}
/*
* 这是与加入队列配对的另一个原子操作: 将当前任务的状态设置为指定的睡眠状态.
* 在此调用之后, 内核调度器就会认为当前任务是睡眠的, 不会再主动调度它,
* 除非它的状态被明确改回 TASK_RUNNING.
*/
set_current_state(state);
}
/*
* 释放等待队列头的自旋锁, 并恢复到进入临界区之前的中断状态.
*/
spin_unlock_irqrestore(&wq_head->lock, flags);
/*
* 返回操作结果.
*/
return ret;
}
/*
* 将 prepare_to_wait_event 函数导出, 以便内核的其他部分可以使用这个核心的等待辅助函数.
*/
EXPORT_SYMBOL(prepare_to_wait_event);
finish_wait 清理线程在等待队列中的状态
/**
* finish_wait - 在队列中等待后进行清理
* @wq_head:waitqueue waited on
* @wq_entry:等待描述符
*
* 负责在线程完成等待后将其从等待队列中移除,并将线程的状态恢复为运行状态(TASK_RUNNING)。
* 该函数确保等待队列的正确性,同时避免竞争条件导致的不一致。
*/
void finish_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;
/* 恢复线程状态 */
__set_c urrent_state(TASK_RUNNING);
/*
* 我们可以检查锁外的列表为空
* 国际电影节:
* - 我们使用“小心”检查来验证 next 和 prev 指针,这样其他 CPU 上就不会有任何我们尚未看到的半待处理更新正在进行中(这仍然可能更改堆栈区域。
*和
* - 所有其他用户都接受锁定(即我们只能有 _one_ 其他 CPU 来查看或修改列表)。
*/
/* 检查等待队列条目是否仍在队列中
list_empty_careful 是一种安全的检查方法,
验证链表的 next 和 prev 指针是否为空,
避免其他 CPU 的未完成更新导致的竞争条件*/
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);
__wake_up 唤醒等待队列中的线程
/*
* 核心唤醒功能。非独占唤醒 (nr_exclusive == 0) 只是唤醒所有内容。如果它是一个独占唤醒 (nr_exclusive == 小 ve 数),那么我们会唤醒该数量的独占任务,并可能唤醒所有非独占任务。通常,独占任务将位于列表的末尾,任何非独占任务将首先被唤醒。优先级任务可能位于列表的开头,并且可以在不唤醒任何其他任务的情况下使用事件。
*
* 在某些情况下,我们可以尝试唤醒已经开始运行但未处于状态 TASK_RUNNING 的任务。在这种(罕见的)情况下,try_to_wake_up() 返回 0,我们通过继续扫描队列来处理它。
*/
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 *curr, *next;
lockdep_assert_held(&wq_head->lock);
curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);
/* 如果队列为空(即头部指向自身 */
if (&curr->entry == &wq_head->head)
return nr_exclusive;
/* 历等待队列中的条目。 */
list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
unsigned flags = curr->flags;
int ret;
/* 对每个条目调用其唤醒函数(curr->func),传递唤醒模式(mode)、唤醒标志(wake_flags)和键(key) */
ret = curr->func(curr, mode, wake_flags, key);
if (ret < 0)
break;
/* 如果唤醒成功且条目是独占条目(flags & WQ_FLAG_EXCLUSIVE),减少 nr_exclusive 的计数。如果计数达到零,停止遍历 */
if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
return nr_exclusive;
}
static int __wake_up_common_lock(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
unsigned long flags;
int remaining;
spin_lock_irqsave(&wq_head->lock, flags);
remaining = __wake_up_common(wq_head, mode, nr_exclusive, wake_flags,
key);
spin_unlock_irqrestore(&wq_head->lock, flags);
/* 成功唤醒的独占线程数量 */
return nr_exclusive - remaining;
}
/**
* __wake_up - 唤醒在 waitqueue 上阻塞的线程。
* @wq_head:waitqueue
* @mode:哪些线程
* @nr_exclusive:要唤醒的 Wake-One 或 Wake-Many 线程数
* @key:直接传递给唤醒函数
*
* 如果该函数唤醒了一个任务,它会在访问任务状态之前执行一个满内存屏障。
* 返回已唤醒的 exclusivetask 的数量。
*/
int __wake_up(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, void *key)
{
return __wake_up_common_lock(wq_head, mode, nr_exclusive, 0, key);
}
EXPORT_SYMBOL(__wake_up);
```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);
}
等待队列添加:一种支持优先级的任务挂起机制
本代码片段展示了Linux内核中一个基础且极为重要的同步原语:add_wait_queue。其核心功能是将一个代表当前任务的等待节点(wait_queue_entry)添加到一个等待队列头(wait_queue_head)中。这是一个任务进入睡眠状态前的准备步骤。该函数的实现并非简单的链表尾部追加,而是支持优先级的插入,确保高优先级的等待者(如实时任务)始终位于等待队列的前部,从而能够被优先唤醒。
实现原理分析
该功能的实现分为两个函数:一个外部包裹函数add_wait_queue和一个内部核心逻辑函数__add_wait_queue。这种设计是内核中的常见模式,用于将加锁/解锁逻辑与核心算法分离。
-
锁定与安全封装 (
add_wait_queue):- 此函数是外部调用的标准接口。它首先通过
spin_lock_irqsave获取等待队列头内部的自旋锁。这个特定的锁类型不仅能防止多核处理器上的并发访问,还能在获取锁的同时禁用本地中断。禁用中断是至关重要的,因为唤醒操作(wake_up)可能发生在中断上下文中,如果不禁用中断,链表操作可能会被中断处理程序打断,导致数据结构损坏。 - 它明确地清除了等待节点中的
WQ_FLAG_EXCLUSIVE标志。这意味着调用此函数的任务是一个“非独占”的等待者。当事件发生时,所有非独占的等待者都会被唤醒。 - 在锁的保护下,它调用
__add_wait_queue来执行实际的链表插入操作。 - 操作完成后,通过
spin_unlock_irqrestore释放锁并恢复之前的中断状态。
- 此函数是外部调用的标准接口。它首先通过
-
优先级插入 (
__add_wait_queue):- 这是等待队列优先级机制的核心。等待队列本质上是一个双向链表。
- 该函数并非简单地将新节点添加到链表头或尾。它首先遍历整个等待队列,
list_for_each_entry会依次访问队列中的每个等待节点。 - 遍历目的: 循环的条件是检查每个节点是否设置了
WQ_FLAG_PRIORITY标志。它会跳过所有设置了此标志的高优先级节点。 - 确定插入点: 循环在遇到第一个没有设置
WQ_FLAG_PRIORITY标志的普通节点时停止。此时,head指针指向了最后一个高优先级节点的entry成员。 - 执行插入:
list_add(&wq_entry->entry, head)将新的等待节点插入到head指针所指向的节点之后。最终效果是,新的(普通优先级)节点被精确地插入到了所有高优先级节点之后,但在所有已存在的普通优先级节点之前。
代码分析
// __add_wait_queue: 将一个等待节点添加到等待队列中(无锁版本)。
// @wq_head: 指向等待队列头的指针。
// @wq_entry: 指向要添加的等待节点的指针。
// 此函数实现了支持优先级的插入逻辑。
static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
// head指针初始化为指向队列的头部,作为默认插入点。
struct list_head *head = &wq_head->head;
struct wait_queue_entry *wq;
// 遍历等待队列中的每一个节点。
list_for_each_entry(wq, &wq_head->head, entry) {
// 检查当前节点是否是普通优先级(即没有设置WQ_FLAG_PRIORITY)。
if (!(wq->flags & WQ_FLAG_PRIORITY))
// 如果是,说明我们已经越过了所有高优先级的节点,
// 循环在此处中断。head指针保持在最后一个高优先级节点的位置。
break;
// 如果当前节点是高优先级的,更新head指针,使其指向当前节点的链表成员,
// 以便下一个插入发生在此节点之后。
head = &wq->entry;
}
// 将新的等待节点wq_entry插入到head指针之后。
// 这确保了新的普通节点被放置在所有高优先级节点之后。
list_add(&wq_entry->entry, head);
}
// add_wait_queue: 将一个等待节点添加到等待队列中(加锁的安全版本)。
// @wq_head: 指向等待队列头的指针。
// @wq_entry: 指向要添加的等待节点的指针。
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;
// 明确清除WQ_FLAG_EXCLUSIVE标志,确保这是一个非独占的等待者。
// 非独占等待者在wake_up时会被全部唤醒。
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);
}
// 导出符号,使得内核其他部分(包括模块)可以使用此函数。
EXPORT_SYMBOL(add_wait_queue);
kernel/sched/wait_bit.c
wait_bit_init 等待位初始化
#define WAIT_TABLE_BITS 8
#define WAIT_TABLE_SIZE (1 << WAIT_TABLE_BITS)
void __init wait_bit_init(void)
{
int i;
for (i = 0; i < WAIT_TABLE_SIZE; i++)
init_waitqueue_head(bit_wait_table + i);
}
bit_wait_table 全局数组,存储所有等待队列头
#define WAIT_TABLE_BITS 8
#define WAIT_TABLE_SIZE (1 << WAIT_TABLE_BITS)
static wait_queue_head_t bit_wait_table[WAIT_TABLE_SIZE] __cacheline_aligned;
var_wake_function 唤醒等待队列中的线程
static int
var_wake_function(struct wait_queue_entry *wq_entry, unsigned int mode,
int sync, void *arg)
{
struct wait_bit_key *key = arg;
struct wait_bit_queue_entry *wbq_entry =
container_of(wq_entry, struct wait_bit_queue_entry, wq_entry);
if (wbq_entry->key.flags != key->flags ||
wbq_entry->key.bit_nr != key->bit_nr)
return 0;
return autoremove_wake_function(wq_entry, mode, sync, key);
}
init_wait_var_entry 初始化等待队列条目
- 初始化等待队列条目(wait_bit_queue_entry),以支持线程在特定变量(var)的状态发生变化时进行同步操作。等待队列是 Linux 内核中用于线程阻塞和唤醒的机制
void init_wait_var_entry(struct wait_bit_queue_entry *wbq_entry, void *var, int flags)
{
*wbq_entry = (struct wait_bit_queue_entry){
/* 用于标识与该条目关联的变量 */
.key = {
.flags = (var),
/* 通常表示该条目不关联具体的位标志 */
.bit_nr = -1,
},
/* 表示线程在等待队列中的具体信息 */
.wq_entry = {
/* 控制等待行为 */
.flags = flags,
/* 表示该条目与当前线程关联 */
.private = current,
/* 回调函数,用于唤醒等待线程 */
.func = var_wake_function,
/* 将该条目加入等待队列 */
.entry = LIST_HEAD_INIT(wbq_entry->wq_entry.entry),
},
};
}
EXPORT_SYMBOL(init_wait_var_entry);
__var_waitqueue 根据变量地址(p)计算并返回与该变量关联的等待队列头(wait_queue_head_t)
wait_queue_head_t *__var_waitqueue(void *p)
{
/* 使用 hash_ptr 对变量地址(p)进行哈希计算。
哈希计算的结果是一个索引,用于定位等待队列表中的具体条目*/
return bit_wait_table + hash_ptr(p, WAIT_TABLE_BITS);
}
EXPORT_SYMBOL(__var_waitqueue);
__wake_up_bit 唤醒等待特定位的线程
#define __WAIT_BIT_KEY_INITIALIZER(word, bit) \
{ .flags = word, .bit_nr = bit, }
void __wake_up_bit(struct wait_queue_head *wq_head, unsigned long *word, int bit)
{
/* 该结构将变量地址(word)和位编号(bit)关联起来 */
struct wait_bit_key key = __WAIT_BIT_KEY_INITIALIZER(word, bit);
/* 检查等待队列是否有线程正在等待
!list_empty(&wq_head->head);*/
if (waitqueue_active(wq_head))
/* TASK_NORMAL 指定唤醒的线程类型为普通任务
唤醒操作会根据 key 唤醒那些明确等待该位的线程,确保唤醒的精准性 */
__wake_up(wq_head, TASK_NORMAL, 1, &key);
}
EXPORT_SYMBOL(__wake_up_bit);
唤醒等待特定变量(内核地址)的线程
/**
* wake_up_var - 唤醒在变量(内核地址)上等待的进程
* @var: 被等待的变量的地址
*
* 唤醒任何在 wait_var_event() 或类似函数中等待该变量改变的进程。
* wait_var_event() 可以等待任意条件为真,并将该条件与一个地址关联。
* 调用 wake_up_var() 表示该条件已经为真,但并不严格要求条件必须使用给定的地址。
*
* 唤醒操作会发送到通过哈希从共享池中选出的等待队列。
* 只有那些在该队列上请求针对该特定地址唤醒的任务会被唤醒。
*
* 为了让该函数正常工作,在变量被更新(更准确地说,是等待的条件变为真)之后,
* 并且在调用此函数之前,必须有一个完整的内存屏障。
* 如果变量是通过原子操作(如 atomic_dec())更新的,可以使用 smb_mb__after_atomic()。
* 如果变量是通过完全有序的操作(如 atomic_dec_and_test())更新的,则不需要额外的屏障。
* 否则需要使用 smb_mb()。
*
* 通常,变量应该通过具有 RELEASE 语义的操作(如 smp_store_release())进行更新
* (即条件变为真),这样在变量更新之前对内存的任何更改都能保证在匹配的 wait_var_event() 完成后可见。
*/
void wake_up_var(void *var)
{
__wake_up_bit(__var_waitqueue(var), var, -1);
}
EXPORT_SYMBOL(wake_up_var);
kernel/sched/swait 简单等待队列(Simple Wait Queues) 一种轻量级的内核阻塞原语
历史与背景
这项技术是为了解决什么特定问题而诞生的?
kernel/sched/swait.c 实现的**简单等待队列(Simple Wait Queues)**是为了解决一个性能优化问题:标准的等待队列(wait.c)对于许多简单的同步场景来说过于“重”。
标准的wait_queue_entry_t结构体非常灵活,它包含一个函数指针(func),允许唤醒者执行一个自定义的回调函数,而不仅仅是唤醒进程。这种灵活性在像poll()这样的复杂机制中是必需的。然而,在内核中绝大多数的等待场景中,其逻辑非常简单:
- 一个任务等待某个事件。
- 另一个任务触发该事件,并只需要唤醒等待的任务。
在这些简单的场景下,标准等待队列的灵活性就成了一种不必要的开销:
- 内存开销:
wait_queue_entry_t结构体比swait_queue_entry_t要大,因为它需要存储函数指针、私有数据等。虽然单个开销不大,但在高频创建的场景下(如锁竞争),累积的栈空间使用和缓存占用是值得优化的。 - 性能开销:
wake_up()的逻辑需要检查并调用函数指针,这是一个间接调用,比直接调用try_to_wake_up()要慢一点。对于性能极其敏感的路径(如用户空间锁futex),这种微小的开销也需要被消除。
swait的诞生就是为了提供一个精简版、高性能的替代品,专门用于那些只需要“睡眠-唤醒”而不需要自定义唤醒逻辑的场景。
它的发展经历了哪些重要的里程碑或版本迭代?
swait本身没有复杂的演进历史,它的出现本身就是一个重要的优化里程碑。
- 作为优化被引入:它是在内核发展到一定阶段,社区开始对核心同步原语进行深度性能剖析时被引入的。开发者发现
futex等高频路径上的等待队列开销可以被削减,于是设计了swait。 - 在核心同步原语中被采用:
swait被引入后,迅速被内核中一些最核心的、对性能要求最高的同步机制所采用,例如futex、completion、mutex等。这标志着它在内核中的地位得到了确立。
目前该技术的社区活跃度和主流应用情况如何?
swait.c的代码和wait.c一样,是内核调度器和同步机制的核心基础,非常稳定。它不经常变动,但其存在对于内核性能至关重要。
它的应用场景高度集中,但都极其关键:
- Futex:用户空间多线程库(如pthread)的锁、条件变量等几乎都构建在
futex之上,而futex的内核实现大量使用swait来进行线程的阻塞和唤醒。 - 内核锁:内核中一些现代的锁实现,在需要让任务睡眠时,会使用
swait。 - 完成量(Completions):
wait_for_completion()的底层也是由swait实现的。
核心原理与设计
它的核心工作原理是什么?
swait的核心原理与标准等待队列完全相同,都是基于一个“等待者链表”和一套“睡眠-唤醒”协议。但它的实现被极大地简化了。
数据结构对比:
swait_queue_head_t: 和wait_queue_head_t几乎一样,包含一个锁和一个链表头。swait_queue_entry_t: 这是关键区别。它只包含一个指向task_struct的指针和一个链表节点。它没有标准wait_queue_entry_t中的func函数指针、flags和private数据。
核心协议:
1. 等待者的流程:
- 与标准等待队列非常相似,使用
prepare_to_swait()/swait_event()/finish_swait()等一系列API。 - 流程是:加入队列 -> 设置睡眠状态 -> 检查条件 ->
schedule()-> 离开队列。
2. 唤醒者的流程:
- 调用
swake_up()或swake_up_all()。 - 核心简化:
swake_up()的实现不需要去调用一个自定义的函数。它直接遍历链表,从swait_queue_entry_t中获得task_struct指针,然后直接调用try_to_wake_up()。这个执行路径更短、更直接。
它的主要优势体现在哪些方面?
- 高性能/低开销:
- 更小的内存占用:
swait_queue_entry_t在栈上的开销更小。 - 更快的执行路径:唤醒操作是直接调用,没有间接函数调用的开销,分支预测更友好。
- 更小的内存占用:
- 代码简洁:由于功能专一,其实现和使用都比标准等待队列更简单明了。
它存在哪些已知的劣势、局-限性或在特定场景下的不适用性?
- 缺乏灵活性:这是它最大的“劣势”,也是其设计的初衷。它牺牲了标准等待队列的灵活性(自定义唤醒函数)来换取性能。任何需要非标准唤醒逻辑的场景都无法使用
swait。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
swait是那些对性能要求极高且等待/唤醒逻辑标准化的场景下的首选。
- Futex实现:这是
swait的“杀手级应用”。用户线程在获取futex锁失败时需要睡眠,释放锁时需要唤醒等待者。这个过程每秒可能发生数百万次,且逻辑固定,是swait的完美应用场景。 - 实现其他同步原语:当在内核中实现一个新的锁或同步工具时,如果其等待逻辑只是简单的“睡眠直到被唤醒”,那么应该优先使用
swait而不是wait。例如,completion机制的实现就从wait切换到了swait。
是否有不推荐使用该技术的场景?为什么?
- 需要自定义唤醒逻辑的场景:这是绝对不能使用
swait的场景。最典型的例子就是**poll/epoll**。当一个文件描述符就绪时,poll的唤醒机制不仅要唤醒等待的进程,还需要调用一个回调函数(pollwake)来更新状态,告诉进程是哪个文件描述符就绪了。这种复杂的逻辑必须使用标准的等待队列。 - 大多数设备驱动:普通的设备驱动程序通常使用标准的等待队列,因为它们的等待逻辑可能更复杂,而且对
swait带来的微小性能提升不敏感。标准等待队列提供的灵活性和广泛的文档支持对驱动开发者更友好。
对比分析
请将其 与 标准等待队列(wait)进行详细对比。
| 特性 | 简单等待队列 (swait) | 标准等待队列 (wait) |
|---|---|---|
| 核心数据结构 | swait_queue_entry_t (仅含task_struct*和list_head) |
wait_queue_entry_t (包含func指针, flags, private数据等) |
| 内存开销 | 更小 | 较大 |
| 性能 | 更高 (唤醒路径更直接) | 略低 (有间接函数调用开销) |
| 灵活性 | 低。唤醒逻辑是固定的:总是直接唤醒任务。 | 高。允许通过func指针提供自定义的唤醒回调函数。 |
| 编程模型 | 简单,API如swait_event() |
复杂,API如wait_event() |
| 首选应用场景 | 高性能同步原语:Futex, Completions, Mutexes等。 | 通用内核同步:设备驱动,I/O多路复用(poll/epoll)。 |
| 设计哲学 | 为速度和效率而生的专才 | 为通用性和灵活性而生的通才 |
include/linux/swait.h
init_swait_queue_head
void __init_swait_queue_head(struct swait_queue_head *q, const char *name,
struct lock_class_key *key)
{
raw_spin_lock_init(&q->lock);
lockdep_set_class_and_name(&q->lock, key, name);
INIT_LIST_HEAD(&q->task_list);
}
EXPORT_SYMBOL(__init_swait_queue_head);
#define init_swait_queue_head(q) \
do { \
static struct lock_class_key __key; \
__init_swait_queue_head((q), #q, &__key); \
} while (0)
__SWAIT_QUEUE_HEAD_INITIALIZER
#define __SWAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __RAW_SPIN_LOCK_UNLOCKED(name.lock), \
.task_list = LIST_HEAD_INIT((name).task_list), \
}
#define DECLARE_SWAITQUEUE(name) \
struct swait_queue name = __SWAITQUEUE_INITIALIZER(name)
kernel/sched/swait.c
swake_up_locked 唤醒等待队列中的线程
/*
* 关于 wake_up_state() 返回值的事情;我认为我们可以忽略它。
*
* 如果由于某种原因返回 0,则表示之前等待的
* 任务已经在运行,因此它将观察条件 true(或已经)。
*/
void swake_up_locked(struct swait_queue_head *q, int wake_flags)
{
struct swait_queue *curr;
/* 如果等待队列的任务列表为空,直接返回。
这确保函数不会尝试唤醒不存在的任务 */
if (list_empty(&q->task_list))
return;
/* 使用 list_first_entry 获取任务列表中的第一个任 */
curr = list_first_entry(&q->task_list, typeof(*curr), task_list);
/* TASK_NORMAL 表示任务的唤醒状态为正常运行状态。
wake_flags 是唤醒操作的标志,用于控制唤醒行为。 */
try_to_wake_up(curr->task, TASK_NORMAL, wake_flags);
list_del_init(&curr->task_list);
}
EXPORT_SYMBOL(swake_up_locked);
__prepare_to_swait 用于将当前线程添加到指定的等待队列中
void __prepare_to_swait(struct swait_queue_head *q, struct swait_queue *wait)
{
wait->task = current;
if (list_empty(&wait->task_list))
list_add_tail(&wait->task_list, &q->task_list);
}
__finish_swait 将当前线程从指定的等待队列中移除,并将线程的状态设置为运行状态 (TASK_RUNNING)
void __finish_swait(struct swait_queue_head *q, struct swait_queue *wait)
{
/* 示线程已经从等待状态恢复,可以重新参与调度并执行 */
__set_current_state(TASK_RUNNING);
if (!list_empty(&wait->task_list))
list_del_init(&wait->task_list);
}
swake_up_locked 唤醒一个等待队列中的线程
/**
* 关于 wake_up_state() 返回值的事情;我认为我们可以忽略它。 *
* 如果因为某种原因它返回 0,那意味着之前等待的任务已经在运行,所以它将观察到条件为真(或者已经观察过了)。 */
void swake_up_locked(struct swait_queue_head *q, int wake_flags)
{
struct swait_queue *curr;
if (list_empty(&q->task_list))
return;
curr = list_first_entry(&q->task_list, typeof(*curr), task_list);
try_to_wake_up(curr->task, TASK_NORMAL, wake_flags);
list_del_init(&curr->task_list);
}
EXPORT_SYMBOL(swake_up_locked);
swake_up_all_locked 唤醒所有等待者
/* * 唤醒所有等待者。这个接口仅用于完成,不用于一般使用。 * * 它故意与 swake_up_all() 不同,以允许在硬中断上下文和禁用中断的区域中使用。 */
void swake_up_all_locked(struct swait_queue_head *q)
{
while (!list_empty(&q->task_list))
swake_up_locked(q, 0);
}1
更多推荐


所有评论(0)