[toc]
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)”:
1
2
3
4
5
6
7
8while (!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 初始化等待队列头
1 |
|
__add_wait_queue_entry_tail 添加等待队列条目尾部
1 | static inline void __add_wait_queue_entry_tail(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry) |
__add_wait_queue 添加等待队列条目
1 | static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry) |
wait_event: 睡眠直到某个条件成立
此宏是Linux内核中一个基础且非常重要的同步原语. 它的核心作用是让当前任务(进程或内核线程)进入睡眠状态, 直到指定的 C 语言表达式 condition
的计算结果为真. 这是一个安全的睡眠机制, 它通过与等待队列 (waitqueue
) 和唤醒机制 (wake_up
) 配合使用, 避免了竞态条件.
wait_event(wq_head, condition)
(顶层宏)
这是提供给内核开发者使用的公共接口.
1 | /* |
__wait_event(wq_head, condition)
(中层宏)
这个宏是 wait_event
的一个简单封装, 它为更底层的 ___wait_event
宏提供了默认参数.
1 |
|
___wait_event(...)
(底层核心实现)
这是实现等待逻辑的核心宏, 使用了GCC的扩展语法(语句表达式和局部标签).
1 |
|
等待队列移除:任务从挂起状态中恢复
本代码片段定义了与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 | // 1. 准备进入睡眠 |
remove_wait_queue
确保了在任务继续执行其正常逻辑之前,它已经不再位于任何可能被再次唤醒的等待队列中,避免了状态混乱。
代码分析
1 | // __remove_wait_queue: 从等待队列中移除一个等待节点(无锁版本)。 |
kernel/sched/wait.c
autoremove_wake_function
1 | int autoremove_wake_function(struct wait_queue_entry *wq_entry, unsigned mode, int sync, void *key) |
prepare_to_wait_event: 原子地将任务加入等待队列并设置睡眠状态
此函数是 wait_event
宏能够安全工作的基石. 它的核心作用是在一个原子操作中, 完成将当前任务加入等待队列和设置任务为睡眠状态这两个步骤. 这是为了完美地解决”丢失唤醒” (Lost Wakeup) 这一经典的并发问题.
核心原理: 如果一个任务先检查条件(为假), 然后决定睡眠, 在这两个步骤之间, 另一个任务可能已经改变了条件并执行了唤醒操作. 如果不加保护, 这个唤醒就会丢失, 导致第一个任务永久睡眠. prepare_to_wait_event
通过在一个锁的保护下执行”加入队列”和”设置状态”这两个操作, 确保了在任务被标记为可唤醒之后, 到它真正放弃CPU之前, 不会错过任何唤醒信号.
1 | /* |
finish_wait 清理线程在等待队列中的状态
1 | /** |
__wake_up 唤醒等待队列中的线程
1 | /* |
等待队列添加:一种支持优先级的任务挂起机制
本代码片段展示了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
指针所指向的节点之后。最终效果是,新的(普通优先级)节点被精确地插入到了所有高优先级节点之后,但在所有已存在的普通优先级节点之前。
代码分析
1 | // __add_wait_queue: 将一个等待节点添加到等待队列中(无锁版本)。 |
kernel/sched/wait_bit.c
wait_bit_init 等待位初始化
1 |
|
bit_wait_table 全局数组,存储所有等待队列头
1 |
|
var_wake_function 唤醒等待队列中的线程
1 | static int |
init_wait_var_entry 初始化等待队列条目
- 初始化等待队列条目(wait_bit_queue_entry),以支持线程在特定变量(var)的状态发生变化时进行同步操作。等待队列是 Linux 内核中用于线程阻塞和唤醒的机制
1 | void init_wait_var_entry(struct wait_bit_queue_entry *wbq_entry, void *var, int flags) |
__var_waitqueue 根据变量地址(p)计算并返回与该变量关联的等待队列头(wait_queue_head_t)
1 | wait_queue_head_t *__var_waitqueue(void *p) |
__wake_up_bit 唤醒等待特定位的线程
1 |
|
唤醒等待特定变量(内核地址)的线程
1 | /** |
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
1 | void __init_swait_queue_head(struct swait_queue_head *q, const char *name, |
__SWAIT_QUEUE_HEAD_INITIALIZER
1 |
kernel/sched/swait.c
swake_up_locked 唤醒等待队列中的线程
1 | /* |
__prepare_to_swait 用于将当前线程添加到指定的等待队列中
1 | void __prepare_to_swait(struct swait_queue_head *q, struct swait_queue *wait) |
__finish_swait 将当前线程从指定的等待队列中移除,并将线程的状态设置为运行状态 (TASK_RUNNING)
1 | void __finish_swait(struct swait_queue_head *q, struct swait_queue *wait) |
swake_up_locked 唤醒一个等待队列中的线程
1 | /** |
swake_up_all_locked 唤醒所有等待者
1 | /* * 唤醒所有等待者。这个接口仅用于完成,不用于一般使用。 * * 它故意与 swake_up_all() 不同,以允许在硬中断上下文和禁用中断的区域中使用。 */ |