完成 - “等待完成” 屏障 API¶
简介:¶
如果您有一个或多个线程必须等待某些内核活动达到某个点或特定状态,那么完成可以为这个问题提供一个无竞争的解决方案。从语义上讲,它们有点像 pthread_barrier(),并且具有类似的用例。
完成是一种代码同步机制,它优于任何对锁/信号量和忙等待循环的误用。任何时候您想到使用 yield() 或一些奇怪的 msleep(1) 循环来允许其他内容继续进行时,您可能需要考虑使用 wait_for_completion*() 调用和 complete() 代替。
使用完成的优点是它们具有明确的、集中的目的,这使得很容易看到代码的意图,但它们也产生更高效的代码,因为所有线程都可以继续执行,直到实际需要结果,并且等待和信号发送都使用低级调度器睡眠/唤醒工具非常高效。
完成是建立在 Linux 调度器的等待队列和唤醒基础设施之上的。等待队列上的线程正在等待的事件被简化为“struct completion”中的一个简单标志,恰当地称为“done”。
由于完成与调度相关,因此代码可以在 kernel/sched/completion.c 中找到。
用法:¶
使用完成有三个主要部分
“struct completion”同步对象的初始化
通过调用 wait_for_completion() 的变体之一进行的等待部分,
通过调用 complete() 或 complete_all() 进行的信号发送部分。
还有一些辅助函数用于检查完成的状态。请注意,虽然必须首先进行初始化,但等待和信号发送部分可以以任何顺序发生。也就是说,一个线程在另一个线程检查是否需要等待完成之前,就将完成标记为“完成”是完全正常的。
要使用完成,您需要 #include <linux/completion.h> 并创建一个类型为“struct completion”的静态或动态变量,该变量只有两个字段
struct completion {
unsigned int done;
struct swait_queue_head wait;
};
这提供了 ->wait 等待队列,以便将任务放置在等待(如果有)上,以及 ->done 完成标志,用于指示是否已完成。
完成应该被命名为引用正在同步的事件。一个很好的例子是
wait_for_completion(&early_console_added);
complete(&early_console_added);
良好、直观的命名(一如既往)有助于代码的可读性。将完成命名为“complete”并没有帮助,除非目的非常明显……
初始化完成:¶
动态分配的完成对象最好嵌入到数据结构中,这些数据结构保证在函数/驱动程序的生命周期内保持活动状态,以防止与异步 complete() 调用发生竞争。
当使用 wait_for_completion() 的 _timeout() 或 _killable()/_interruptible() 变体时,应特别注意,因为它必须确保在所有相关活动(complete() 或 reinit_completion()
)发生之前不会发生内存释放,即使这些等待函数由于超时或信号触发而过早返回。
动态分配的完成对象的初始化是通过调用 init_completion()
完成的
init_completion(&dynamic_object->done);
在此调用中,我们初始化等待队列并将 ->done 设置为 0,即“未完成”或“未完成”。
重新初始化函数 reinit_completion()
只是将 ->done 字段重置为 0(“未完成”),而不触及等待队列。此函数的调用者必须确保没有并行发生的竞争的 wait_for_completion() 调用。
在同一个完成对象上两次调用 init_completion()
很可能是一个错误,因为它会将队列重新初始化为空队列,并且排队的任务可能会“丢失” - 在这种情况下使用 reinit_completion()
,但要注意其他竞争。
对于静态声明和初始化,可以使用宏。
对于文件范围内的静态(或全局)声明,您可以使用 DECLARE_COMPLETION()
static DECLARE_COMPLETION(setup_done);
DECLARE_COMPLETION(setup_done);
请注意,在这种情况下,完成在启动时(或模块加载时)初始化为“未完成”,并且不需要 init_completion()
调用。
当完成被声明为函数内的局部变量时,则初始化应始终显式使用 DECLARE_COMPLETION_ONSTACK()
,不仅是为了使锁依赖项满意,还要清楚地表明已考虑有限的范围并且是故意的
DECLARE_COMPLETION_ONSTACK(setup_done)
请注意,当将完成对象用作局部变量时,您必须清楚地了解函数堆栈的短暂生命周期:在所有活动(例如等待线程)停止并且完成对象完全未使用之前,该函数不得返回到调用上下文。
再次强调:特别是当使用一些具有更复杂结果的等待 API 变体(例如超时或信号 (_timeout()、_killable() 和 _interruptible()) 变体)时,等待可能会过早完成,而该对象可能仍被另一个线程使用 - 并且从 wait_on_completion*() 调用者函数返回将释放函数堆栈,并且如果 complete() 在其他线程中完成,则会导致细微的数据损坏。简单的测试可能不会触发这些类型的竞争。
如果不确定,请使用动态分配的完成对象,最好嵌入到其他一些具有很长生命周期的对象中,该对象的生命周期超过任何使用完成对象的辅助线程的生命周期,或者具有锁或其他同步机制以确保不会在已释放的对象上调用 complete()。
在堆栈上使用一个简单的 DECLARE_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() 以处理原子/中断上下文中的完成。
由于 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’ 个节拍。如果发生超时,则返回 0,否则返回剩余的节拍时间(但至少为 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)
此函数传递一个以节拍为单位的超时时间,并将任务标记为 TASK_INTERRUPTIBLE。如果收到信号,它将返回 -ERESTARTSYS;否则,如果完成超时,则返回 0,如果完成发生,则返回剩余的节拍时间。
其他变体包括 _killable,它使用 TASK_KILLABLE 作为指定的任务状态,如果被中断,将返回 -ERESTARTSYS,如果完成,则返回 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)
发出完成信号:¶
一个想要发出继续条件的线程调用 complete() 来精确地向一个等待者发出可以继续的信号。
void complete(struct completion *done)
... 或调用 complete_all() 向所有当前和未来的等待者发出信号。
void complete_all(struct completion *done)
即使在线程开始等待之前发出完成信号,信号也会按预期工作。这是通过等待者“消耗”(递减)‘struct completion’ 的 done 字段来实现的。等待线程的唤醒顺序与它们入队的顺序相同(FIFO 顺序)。
如果 complete() 被多次调用,那么这将允许该数量的等待者继续 - 每次调用 complete() 都会简单地增加 done 字段。但是,多次调用 complete_all() 是一个错误。complete() 和 complete_all() 都可以安全地在 IRQ/原子上下文中调用。
在任何时候,只能有一个线程在特定的 ‘struct completion’ 上调用 complete() 或 complete_all() - 通过等待队列自旋锁进行序列化。任何此类对 complete() 或 complete_all() 的并发调用都可能是设计错误。
从 IRQ 上下文发出完成信号是没问题的,因为它会适当地使用 spin_lock_irqsave()/spin_unlock_irqrestore() 进行锁定,并且永远不会休眠。
try_wait_for_completion()/completion_done():¶
如果 try_wait_for_completion() 函数需要将线程入队(阻塞),则不会将线程放入等待队列,而是返回 false,否则它会消耗一个已发布的完成并返回 true。
bool try_wait_for_completion(struct completion *done)
最后,要检查完成状态而不以任何方式更改它,请调用 completion_done(),如果没有任何等待者尚未消耗的已发布完成(暗示存在等待者),则返回 false,否则返回 true。
bool completion_done(struct completion *done)
try_wait_for_completion() 和 completion_done() 都可以安全地在 IRQ 或原子上下文中调用。