【linux kernel】一文总结linux内核的完成量机制

文章目录

        • 一、completion完成量
        • 二、使用方法
          • (2-1)初始化完成量
            • (2-1-1)动态初始化完成量
            • (2-1-2)静态声明和初始化完成量
          • (2-2)等待完成量
          • (2-3)发送信号量完成
          • (2-4) try_wait_for_completion()/completion_done()
        • 三、使用实例

一、completion完成量

linux内核中,completion是一种代码同步机制。如果有一个或多个线程必须等待某个内核活动操作达到某个点或某个特定状态,那么completion完成量可以提供一个无竞争的解决方案。当想使用yield()或msleep(1)循环来允许其他事情(线程)继续执行时,可以考虑使用wait_for_completion()和complete()来设计。

使用完成量的优点主要有以下两个:

(1)使代码更容易阅读。

(2)产生更高效的代码。因为所有线程都可以继续执行,直到实际需要结果为止,而且其使用低级别调度器sleep/wakeup,对完成量的等待和发送都非常高效。

二、使用方法

总的来说,使用完成量需要三个步骤:

(1)初始化struct completion同步对象。

(2)通过调用wait_for_completion()函数等待完成量。

(3)调用complete()complete_all发送完成信号。


注意,虽然首先必须进行完成量的初始化,但等待和发送信号是可以以任何顺序进行的。例如:一个线程在另一个线程中检查它是否需要等待它之前,将一个完成标记为“完成”是完全正常的。

如果需要使用完成量,需要#include 头文件并创建一个静态或动态的类型struct completion变量。该结构变量定义如下:

struct completion {
        unsigned int done;
        wait_queue_head_t wait;
};

wait等待队列用来放置任务等待(如果存在),done用来指示任务是否完成。完成量的命名应该引用正在同步的事件,例如:

wait_for_completion(&early_console_added);

complete(&early_console_added);

early_console_added这个完成量的命名就比较直观和意图清晰。

(2-1)初始化完成量
(2-1-1)动态初始化完成量

动态分配的完成对象最好将其嵌入到在函数/驱动程序的生命周期内是活的数据结构中,防止发生与异步complete()调用的竞争。

在使用wait_for_completion()的_timeout()或_killable()/_interruptible()变体时应特别注意,因为必须确保在所有相关活动(complete()或reinit_completion())发生之前不会发生内存回收。

通过调用init_completion()来初始化动态分配的完成对象,,如以下代码:

init_completion(&dynamic_object->done);

在这个调用中,初始化了等待队列,并将done设置为0,即“未完成”。

也可以使用reinit_completion来初始化完成量。但是reinit_completion()只是将done字段重置为0(“未完成”),而不设置等待队列。因此,这个使用这个函数时必须确保没有并发的、紧急wait_for_completion()调用。

在同一个完成对象上调用两次init_completion()很可能会出现bug,因为该操作将队列重新初始化为空队列,因此进入队列的任务可能会“丢失”。在这种情况下使用reinit_completion()比较合适,但要注意出现其他竞争的情况。

(2-1-2)静态声明和初始化完成量

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

static DECLARE_COMPLETION(setup_done);
DECLARE_COMPLETION(setup_done);

注意,在这种情况下,完成量是在linux内核启动时(或模块加载时)初始化为“未完成”的,不需要调用init_completion()函数。

当一个完成量被声明为函数中的局部变量时,应该总是显式地使用DECLARE_COMPLETION_ONSTACK()来初始化,这一点不仅仅是为了适配lockdep,也是为了表明我们考虑到了其有限的作用域:

DECLARE_COMPLETION_ONSTACK(setup_done)

【特别注意】
(1)当将完成对象作为局部变量时,必须敏感地意识到函数栈的生命周期很短:在所有活动(如等待线程)停止并且完成对象完全未使用之前,函数是不能返回到调用上下文。

(2)当使用一些具有复杂结果的等待API变体时,例如:超时或信号(_timeout(), _killable()和_interruptible())的变体,当对象仍被其他线程使用时,等待可能会提前完成—因此wait_on_completion()调用函数的返回将释放函数堆栈,如果在其他线程中完成complete(),则会导致数据损坏。在开发中,这类竞争问题将会很难发现和测试。

(2-2)等待完成量

为了让线程等待并发操作的完成,该线程应该对初始化的完成结构调用wait_for_completion():

void wait_for_completion(struct completion *done)

并不意味着wait_for_completion()和对complete()的调用有任何特定的顺序—如果对complete()的调用发生在对wait_for_completion()的调用之前,那么等待端将直接继续,因为所有依赖都满足了;如果没有,它将阻塞,直到complete()发出完成信号。

请注意,wait_for_completion()正在调用spin_lock_irq()/spin_unlock_irq(),所以只有当知道中断已启用时才能安全地调用它。从irq -off原子上下文调用它将导致难以检测的虚假中断启用。

默认行为是不超时地等待,并将任务标记为不可中断,wait_for_completion()及其变体只在进程上下文中是安全的(因为它们可以睡眠),但在原子上下文中、中断上下文中则不是安全的,在禁用IRQs或禁用抢占的情况下也是不安全的。这时候使用try_wait_for_completion()函数来处理原子/中断上下文中的完成。

(2-3)发送信号量完成

一个线程想要通知某个条件已经达到,就会调用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()都可以在中断/原子上下文中安全调用。

在任何时候都只能有一个线程对特定的 struct completion 调用complete()或complete_all()——可通过等待队列/自旋锁序来列化。对complete()或complete_all()的任何此类并发调用都可能导致错误。

从中断上下文发送完成信号是可以的,因为它将使用spin_lock_irqsave()/spin_unlock_irqrestore()进行锁定,并且永远不会睡眠。

(2-4) 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或原子上下文中调用都是安全的。

三、使用实例

完成量的典型使用场景是:linux内核启动过程中的1号和2号线程创建过程。

笔者对其做了一个测试,步骤和结果如下:

1、在kernel_init()函数中加了一行代码,如下图所示:

【linux kernel】一文总结linux内核的完成量机制_第1张图片

2、在kthreadd函数中加了一行打印输出代码,如下图所示:

【linux kernel】一文总结linux内核的完成量机制_第2张图片

3、在rest_init()函数中加了打印输出代码,如下图所示:

【linux kernel】一文总结linux内核的完成量机制_第3张图片

最后编译构建,运行结果如下图所示:

在这里插入图片描述

在启动后期,打印如下图所示信息:

在这里插入图片描述

从该实际测试可见,在linux内核启动过程中1号init线程和2号kthreadd线程之间的创建过程中,当kthreadd创建完成后,kernel_init代表的init线程才开始执行,该处则依赖于完成量机制而得以实现。

你可能感兴趣的:(小生聊【linux,kernel】,linux,kernel,linux,C语言,completion,完成量)