Completions - “等待完成” 屏障 API¶
简介:¶
如果您有一个或多个线程必须等待某些内核活动达到某个点或特定状态,那么 completion 可以为此问题提供一个无竞争的解决方案。从语义上讲,它们有点像 pthread_barrier(),并且具有类似的用例。
Completions 是一种代码同步机制,它优于任何对锁/信号量和忙循环的滥用。每当您想到使用 yield() 或某些古怪的 msleep(1) 循环来允许其他事情继续进行时,您可能需要考虑使用 wait_for_completion*() 调用和 complete() 来代替。
使用 completions 的优点是它们具有明确定义的、集中的目的,这使得很容易看到代码的意图,而且它们还可以生成更高效的代码,因为所有线程都可以继续执行,直到实际需要结果,并且等待和信号发送都非常高效,使用低级调度程序睡眠/唤醒工具。
Completions 构建于 Linux 调度器的 waitqueue 和 wakeup 基础设施之上。waitqueue 上的线程正在等待的事件被简化为“struct completion”中的一个简单标志,适当地称为“done”。
由于 completions 与调度相关,因此可以在 kernel/sched/completion.c 中找到代码。
用法:¶
使用 completions 主要有三个部分
“struct completion”同步对象的初始化
通过调用 wait_for_completion() 的一个变体进行的等待部分,
通过调用 complete() 或 complete_all() 进行的信号发送部分。
还有一些辅助函数用于检查 completions 的状态。请注意,虽然初始化必须首先发生,但等待和信号发送部分可以按任何顺序发生。也就是说,一个线程在另一个线程检查是否必须等待 completion 之前,已经将 completion 标记为“done”是完全正常的。
要使用 completions,您需要 #include <linux/completion.h> 并创建类型为“struct completion”的静态或动态变量,该变量只有两个字段
struct completion {
unsigned int done;
struct swait_queue_head wait;
};
这提供了 ->wait waitqueue,用于放置等待的任务(如果有),以及 ->done completion 标志,用于指示它是否已完成。
Completions 应该被命名为引用正在同步的事件。一个好的例子是
wait_for_completion(&early_console_added);
complete(&early_console_added);
良好、直观的命名(一如既往)有助于代码的可读性。将 completion 命名为“complete”是没有帮助的,除非目的非常明显……
初始化 completions:¶
动态分配的 completion 对象最好嵌入到数据结构中,以确保该数据结构在该函数/驱动程序的生命周期内都是有效的,以防止与来自异步 complete() 调用的竞争发生。
当使用 wait_for_completion() 的 _timeout() 或 _killable()/_interruptible() 变体时,应特别注意,因为它必须确保在所有相关活动(complete() 或 reinit_completion()
)发生之前,不会发生内存释放,即使这些等待函数由于超时或信号触发而提前返回。
动态分配的 completion 对象的初始化通过调用 init_completion()
完成
init_completion(&dynamic_object->done);
在此调用中,我们初始化 waitqueue 并将 ->done 设置为 0,即“未完成”。
重新初始化函数 reinit_completion()
只是将 ->done 字段重置为 0(“未完成”),而不触及 waitqueue。此函数的调用者必须确保没有并行的 racy wait_for_completion() 调用正在进行。
在同一个 completion 对象上两次调用 init_completion()
很可能是一个 bug,因为它会将队列重新初始化为一个空队列,并且排队的任务可能会“丢失” - 在这种情况下使用 reinit_completion()
,但要注意其他竞争。
对于静态声明和初始化,可以使用宏。
对于文件范围内的静态(或全局)声明,您可以使用 DECLARE_COMPLETION()
static DECLARE_COMPLETION(setup_done);
DECLARE_COMPLETION(setup_done);
请注意,在这种情况下,completion 在启动时(或模块加载时)初始化为“未完成”,并且不需要 init_completion()
调用。
当 completion 被声明为函数内的局部变量时,初始化应该始终显式使用 DECLARE_COMPLETION_ONSTACK()
,不仅要使 lockdep 高兴,还要清楚地表明已经考虑了有限范围并且是有意的
DECLARE_COMPLETION_ONSTACK(setup_done)
请注意,当使用 completion 对象作为局部变量时,您必须敏锐地意识到函数堆栈的生命周期很短:在所有活动(例如等待线程)都已停止并且 completion 对象完全未使用之前,该函数不得返回到调用上下文中。
再次强调:特别是在使用一些具有更复杂结果的等待 API 变体时,例如超时或信号发送 (_timeout()、_killable() 和 _interruptible()) 变体,等待可能会提前完成,而该对象可能仍被另一个线程使用 - 并且从 wait_on_completion*() 调用者函数返回将释放函数堆栈,如果 complete() 在其他线程中完成,则会导致微妙的数据损坏。简单的测试可能不会触发这些类型的竞争。
如果不确定,请使用动态分配的 completion 对象,最好嵌入到一些其他具有无聊的长生命周期的对象中,该对象的生命周期超过任何使用 completion 对象的辅助线程的生命周期,或者具有锁或其他同步机制来确保 complete() 不会在释放的对象上调用。
在堆栈上使用幼稚的 DECLARE_COMPLETION()
会触发 lockdep 警告。
等待 completions:¶
为了使线程等待某些并发活动完成,它会在初始化的 completion 结构上调用 wait_for_completion()
void wait_for_completion(struct completion *done)
一个典型的使用场景是
CPU#1 CPU#2
struct completion setup_done;
init_completion(&setup_done);
initialize_work(...,&setup_done,...);
/* run non-dependent code */ /* do setup */
wait_for_completion(&setup_done); complete(&setup_done);
这并不意味着 wait_for_completion() 和 complete() 的调用之间存在任何特定的顺序 - 如果对 complete() 的调用发生在对 wait_for_completion() 的调用之前,那么等待方将立即继续,因为所有依赖关系都已满足;如果没有,它将阻塞直到 complete() 发出信号。
请注意,wait_for_completion() 调用 spin_lock_irq()/spin_unlock_irq(),因此只有在您知道中断已启用时才能安全地调用它。从禁用 IRQ 的原子上下文中调用它将导致难以检测的伪造中断启用。
默认行为是等待没有超时并将任务标记为不可中断。 wait_for_completion() 及其变体仅在进程上下文中是安全的(因为它们可以睡眠),但在原子上下文、中断上下文、禁用 IRQ 或禁用抢占的情况下则不安全 - 另请参阅下面的 try_wait_for_completion(),以处理原子/中断上下文中的 completion。
由于 wait_for_completion() 的所有变体都可能(显然)阻塞很长时间,具体取决于它们正在等待的活动的性质,因此在大多数情况下,您可能不想在持有互斥锁的情况下调用它。
可用的 wait_for_completion*() 变体:¶
以下变体都返回状态,并且在大多数(/所有)情况下都应检查此状态 - 在有意不检查状态的情况下,您可能需要做一个注释来解释这一点(例如,参见 arch/arm/kernel/smp.c:__cpu_up())。
一个常见的问题是返回值类型的不干净分配,因此请注意将返回值分配给正确类型的变量。
还发现对返回值的特定含义的检查非常不准确,例如构造
if (!wait_for_completion_interruptible_timeout(...))
... 对于成功完成和中断的情况,将执行相同的代码路径 - 这可能不是您想要的
int wait_for_completion_interruptible(struct completion *done)
此函数在等待时将任务标记为 TASK_INTERRUPTIBLE。如果在等待时收到信号,它将返回 -ERESTARTSYS;否则为 0
unsigned long wait_for_completion_timeout(struct completion *done, unsigned long timeout)
该任务被标记为 TASK_UNINTERRUPTIBLE 并且最多等待“timeout”jiffies。如果发生超时,它将返回 0,否则返回剩余时间(以 jiffies 为单位)(但至少为 1)。
超时最好使用 msecs_to_jiffies()
或 usecs_to_jiffies()
计算,以使代码在很大程度上与 HZ 无关。
如果返回的超时值被有意忽略,则注释可能应该解释原因(例如,参见 drivers/mfd/wm8350-core.c wm8350_read_auxadc())
long wait_for_completion_interruptible_timeout(struct completion *done, unsigned long timeout)
此函数以 jiffies 为单位传递超时,并将任务标记为 TASK_INTERRUPTIBLE。如果收到信号,它将返回 -ERESTARTSYS;否则,如果 completion 超时,则返回 0;如果 completion 发生,则返回剩余时间(以 jiffies 为单位)。
其他变体包括 _killable,它使用 TASK_KILLABLE 作为指定的任务状态,如果被中断,将返回 -ERESTARTSYS;如果实现 completion,则返回 0。还有一个 _timeout 变体
long wait_for_completion_killable(struct completion *done)
long wait_for_completion_killable_timeout(struct completion *done, unsigned long timeout)
_io 变体 wait_for_completion_io() 的行为与非 _io 变体相同,除了将等待时间计为“等待 IO”之外,这对任务在调度/IO 统计信息中的计算方式有影响
void wait_for_completion_io(struct completion *done)
unsigned long wait_for_completion_io_timeout(struct completion *done, unsigned long timeout)
信号发送 completions:¶
想要发出可以继续的条件已满足的信号的线程调用 complete(),以向正好一个等待者发出可以继续的信号
void complete(struct completion *done)
... 或者调用 complete_all() 以向所有当前和未来的等待者发出信号
void complete_all(struct completion *done)
即使在线程开始等待之前发出 completion 信号,信号发送也会按预期工作。这是通过等待者“消耗”(递减)“struct completion”的 done 字段来实现的。等待线程的唤醒顺序与它们排队的顺序相同(FIFO 顺序)。
如果多次调用 complete(),那么这将允许该数量的等待者继续 - 每次调用 complete() 都会简单地递增 done 字段。但是,多次调用 complete_all() 是一个 bug。 complete() 和 complete_all() 都可以安全地在 IRQ/原子上下文中调用。
在任何时候,只能有一个线程在特定的“struct completion”上调用 complete() 或 complete_all() - 通过等待队列自旋锁进行序列化。任何此类对 complete() 或 complete_all() 的并发调用都可能是一个设计 bug。
从 IRQ 上下文发送 completion 信号很好,因为它会适当地使用 spin_lock_irqsave()/spin_unlock_irqrestore() 进行锁定,并且它永远不会睡眠。
try_wait_for_completion()/completion_done():¶
try_wait_for_completion() 函数不会将线程放入等待队列,而是返回 false,如果需要将线程排队(阻塞),否则它会消耗一个已发布的 completion 并返回 true
bool try_wait_for_completion(struct completion *done)
最后,要检查 completion 的状态而不以任何方式更改它,请调用 completion_done(),如果没有尚未被等待者消耗的已发布 completions,则返回 false(意味着有等待者),否则返回 true
bool completion_done(struct completion *done)
try_wait_for_completion() 和 completion_done() 都可以安全地在 IRQ 或原子上下文中调用。