Completions - "wait for completion" barrier APIs 【ChatGPT】

发布时间 2023-12-11 20:56:07作者: 摩斯电码

Completions - "wait for completion" barrier APIs

介绍:

如果您有一个或多个线程必须等待某些内核活动达到某个点或特定状态,完成(completions)可以为这个问题提供无竞争的解决方案。从语义上讲,它们有点像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;
    wait_queue_head_t 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()调用正在进行。

在静态声明和初始化时,可以使用宏。

对于文件范围的静态(或全局)声明,可以使用DECLARE_COMPLETION():

static DECLARE_COMPLETION(setup_done);
DECLARE_COMPLETION(setup_done);

请注意,在这种情况下,完成在引导时(或模块加载时)初始化为“未完成”,不需要init_completion()调用。

当完成被声明为函数内的局部变量时,初始化应始终显式使用DECLARE_COMPLETION_ONSTACK(),不仅是为了让lockdep满意,而且也是为了明确表明已考虑了有限的范围并且是有意的:

DECLARE_COMPLETION_ONSTACK(setup_done)

请注意,当使用完成对象作为局部变量时,您必须非常清楚地了解函数堆栈的短生命周期:函数必须在所有活动(如等待线程)停止并且完成对象完全未使用之前,才能返回到调用上下文。再次强调一下:特别是在使用一些等待API变体具有更复杂结果时,比如超时或信号(_timeout()、_killable()和_interruptible())变体,等待可能会在对象仍然被另一个线程使用时提前完成,而从wait_on_completion*()调用函数返回将会释放函数堆栈,并且如果在其他线程中执行了complete(),将会导致微妙的数据损坏。简单的测试可能不会触发这些种类的竞争。

如果不确定,请使用动态分配的完成对象,最好嵌入在一些其他寿命非常长的对象中,该对象的寿命超过了使用完成对象的任何辅助线程的寿命,或者具有锁或其他同步机制,以确保不会在释放的对象上调用complete()。

在堆栈上天真地DECLARE_COMPLETION()会触发lockdep警告。

等待完成:

要等待某个并发活动完成,线程会在初始化的完成结构上调用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(),因此只有在知道中断已启用时才能安全地调用它。在IRQs-off原子上下文中调用它将导致难以检测的意外中断使能。

默认行为是无超时等待,并将任务标记为不可中断。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'个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;否则,如果完成超时,则返回0,或者如果完成发生,则返回剩余的jiffies时间。

其他变体包括使用TASK_KILLABLE作为指定任务状态的_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()是一个bug。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或原子上下文中调用。