[Linux]学习笔记系列 -- [kernel]completion
title: completion
categories:
- linux
- kernel
tags: - linux
- kernel
abbrlink: 38d6377e
date: 2025-10-03 09:01:49
文章目录
- kernel/sched/completion.c 内核同步原语(Kernel Synchronization Primitive) 简洁的任务完成信号量
- 历史与背景
- 这项技术是为了解决什么特定问题而诞生的?
- 它的发展经历了哪些重要的里程碑或版本迭代?
- 目前该技术的社区活跃度和主流应用情况如何?
- 核心原理与设计
- 它的核心工作原理是什么?
- 它的主要优势体现在哪些方面?
- 它存在哪些已知的劣势、局-限性或在特定场景下的不适用性?
- 使用场景
- 在哪些具体的业务或技术场景下,它是首选解决方案?
- 是否有不推荐使用该技术的场景?为什么?
- 对比分析
- 请将其 与 其他相似技术 进行详细对比。
- include/linux/completion.h
- init_completion 初始化完成量
- DECLARE_COMPLETION_ONSTACK 声明完成量
- kernel/sched/completion.c
- wait_for_common 等待完成量
- wait_for_completion 等待完成量 state TASK_UNINTERRUPTIBLE 不可被中断
- wait_for_completion_killable 等待完成量
- complete_with_flags 完成量完成
- complete_all 完成量释放所有线程
- complete 完成量释放单个线程
- completion_done 检查完成量是否有等待者

https://github.com/wdfk-prog/linux-study
kernel/sched/completion.c 内核同步原语(Kernel Synchronization Primitive) 简洁的任务完成信号量
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术以及它所实现的“完成量”(Completion),是为了提供一个高度简化的、专门用于解决“一个执行单元等待另一个执行单元完成某项工作”这一特定同步场景的内核原语。
- 简化样板代码:在
completion出现之前,要实现这种“等待-通知”的模式,开发者必须手动组合更底层的原语。这通常意味着:- 定义一个
wait_queue_head_t(等待队列头)。 - 定义一个
bool或int类型的标志位来表示任务是否完成。 - 等待者需要在一个循环中调用
wait_event_interruptible(),这个宏会自动处理将任务加入等待队列、睡眠、被唤醒后检查标志位、处理伪唤醒等一系列复杂操作。 - 完成者需要设置标志位,然后调用
wake_up()来唤醒等待者。
这个过程虽然强大,但对于一个非常常见的模式来说,显得过于繁琐且容易出错(例如,忘记处理竞态条件)。
- 定义一个
- 解决竞态条件:一个经典的竞态条件是“完成发生在等待之前”(Completion-before-wait)。如果任务在等待者调用
wait_event之前就已经完成并发出信号,那么这个信号就会丢失,导致等待者永久睡眠。completion机制从设计上就完美地解决了这个问题。
completion的诞生,就是为了将上述所有复杂性封装成一个极其简单、健壮且易于使用的API,让开发者可以专注于业务逻辑,而不是同步细节。
它的发展经历了哪些重要的里程碑或版本迭代?
completion作为一个基础的同步原语,其核心API非常稳定,其发展主要体现在内部实现的优化上。
- 引入:
completion被引入内核,提供了init_completion,wait_for_completion,complete这三个核心API,极大地简化了内核中的同步代码。 - 性能优化:
completion内部实现的一个关键优化是针对“完成发生在等待之前”的快速路径。它内部有一个done计数器。当wait_for_completion被调用时,它会首先检查这个计数器。如果计数器大于0,意味着任务已经完成,等待者根本无需睡眠,可以直接递减计数器并立即返回。这避免了进入调度器、上下文切换等昂贵的操作。 - API扩展:为了支持更广泛的场景,后续加入了
complete_all()(唤醒所有等待者,而不仅仅是一个)、reinit_completion()(允许一个completion对象被重用)以及各种可中断、可超时的等待变体(如wait_for_completion_timeout,wait_for_completion_interruptible)。
目前该技术的社区活跃度和主流应用情况如何?
completion是内核中最基础、最稳定、使用最广泛的同步原语之一。
- 社区活跃度:其代码库和API极其稳定,处于纯维护状态。它被认为是内核同步工具箱中一个“已解决的问题”。
- 主流应用:它的应用遍布内核的每一个角落。
- 驱动程序:一个驱动向硬件发送一个命令后,可以调用
wait_for_completion来睡眠,直到硬件完成操作并通过中断处理程序调用complete。 - 内核线程同步:一个内核线程在启动后,可以调用
complete来通知其创建者自己已经完成初始化。反之,在线程退出时,也可以用它来等待所有子任务完成。 - 核心内核:
vfork()系统调用的实现就用到了completion,父进程用它来等待子进程执行exec()或exit()。
- 驱动程序:一个驱动向硬件发送一个命令后,可以调用
核心原理与设计
它的核心工作原理是什么?
completion的实现巧妙地封装了wait_queue_head_t,并增加了一个状态计数器来解决竞态问题。
struct completion结构体主要包含两个成员:
unsigned int done;:一个计数器,表示“完成”事件发生的次数。wait_queue_head_t wait;:一个标准的等待队列头,用于让任务在此睡眠。
工作流程:
-
初始化 (
init_completion):将done计数器清零,并初始化等待队列头。 -
等待 (
wait_for_completion):- 快速路径:检查
done计数器。如果done > 0,说明complete()已经被调用过了。此时,函数会原子地将done减1,然后立即返回,根本不会睡眠。 - 慢速路径:如果
done == 0,说明任务尚未完成。函数会将当前任务加入到wait队列中,并调用调度器使其进入睡眠状态。
- 快速路径:检查
-
完成 (
complete):- 原子地将
done计数器加1。 - 检查等待队列
wait是否为空。如果不为空,就调用wake_up()唤醒一个正在睡眠的任务。
- 原子地将
这个设计的精妙之处在于done计数器。它使得wait和complete的调用顺序无关紧要。
- 如果
wait先调用,任务会睡眠,等待complete来唤醒它。 - 如果
complete先调用,done会变为1,之后wait被调用时会看到done > 0,直接从快速路径返回。信号绝不会丢失。
它的主要优势体现在哪些方面?
- API极其简洁:完美地抽象了“等待-通知”模型,隐藏了所有底层复杂性。
- 健壮:从设计上就避免了“完成先于等待”的竞态条件。
- 高效:为“完成先于等待”的常见场景提供了无上下文切换的快速路径。
它存在哪些已知的劣势、局-限性或在特定场景下的不适用性?
- 功能专一:它是一个高度特化的工具,只适用于“任务完成”这种一次性的、单向的信令。
- 不用于互斥:它绝对不能被用来保护临界区或共享数据。这是互斥锁(mutex)或自旋锁(spinlock)的工作。
- 不用于资源计数:它不适合用来管理对一组资源的访问。这是信号量(semaphore)的工作。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
当一个执行流需要阻塞自己,直到另一个执行流(或中断上下文)显式地通知它某个一次性事件已经发生时,completion是简单、安全且首选的解决方案。
- 模块卸载:当一个模块被卸载时,它可能需要等待所有正在使用其资源的内核线程都安全退出。卸载代码可以为每个线程调用
wait_for_completion,而每个线程在退出前调用complete。 - 同步I/O操作:一个块设备驱动在处理一个同步的
read请求时,会将请求加入硬件的命令队列,然后wait_for_completion。当硬件完成读取并通过中断通知驱动时,中断处理程序会调用complete,唤醒等待的进程,使其可以拷贝数据并返回给用户。
是否有不推荐使用该技术的场景?为什么?
- 保护共享数据:当多个线程需要互斥地访问一个共享变量时,应该使用
mutex_lock/mutex_unlock。completion无法提供互斥保证。 - 有条件的等待:当一个线程需要等待一个复杂的条件变为真(例如,等待一个队列既不为空也不为满)时,应该使用底层的
wait_event()宏,因为它天生就是与条件判断结合在一起的。 - 生产者-消费者队列:虽然可以用
completion来实现简单的生产者-消费者信令,但信号量(ksem)通常是更通用、更合适的工具,因为它能自然地处理队列中的多个“槽位”。
对比分析
请将其 与 其他相似技术 进行详细对比。
| 特性 | Completion (完成量) | Mutex (互斥锁) | Semaphore (信号量) | Wait Queue (等待队列) |
|---|---|---|---|---|
| 主要目的 | 信令/同步 (Signaling)。表示一个任务已完成。 | 互斥 (Mutual Exclusion)。保护临界区,保证只有一个执行者能进入。 | 资源计数 (Resource Counting)。控制对N个资源的并发访问。 | 底层等待机制 (Waiting Mechanism)。让任务在某个条件上睡眠。 |
| 核心操作 | wait_for_completion, complete | mutex_lock, mutex_unlock | down, up | wait_event, wake_up |
| 解决的问题 | “等待工作完成” | “保护这段代码不被并发执行” | “最多只允许N个线程通过” | “当这个条件不满足时,就睡吧” |
| 抽象层级 | 高层。是对Wait Queue的封装。 | 高层。 | 高层。 | 底层。是其他同步原语的基础。 |
| 是否可重入 | complete可以被多次调用,done计数器会累加。 | 不可重入(常规mutex)。 | 可重入(多次up会增加计数值)。 | N/A |
| 竞态处理 | 内置。完美处理“完成先于等待”的竞态。 | N/A (用于解决竞态) | 需要小心使用以避免竞态。 | 需要手动处理。必须与一个条件变量和正确的锁配合使用。 |
include/linux/completion.h
init_completion 初始化完成量
/*** init_completion - 初始化动态分配的完成* @x:指向要初始化的完成结构的指针** 此内联函数将初始化动态创建的完成结构。*/
static inline void init_completion(struct completion *x)
{x->done = 0;init_swait_queue_head(&x->wait); // 初始化等待队列头
}
DECLARE_COMPLETION_ONSTACK 声明完成量
#define COMPLETION_INITIALIZER(work) \{ 0, __SWAIT_QUEUE_HEAD_INITIALIZER((work).wait) }/** * DECLARE_COMPLETION - 声明和初始化一个完成结构 * @work: 完成结构的标识符 * * 此宏声明并初始化一个完成结构。一般用于静态声明。对于自动变量,您应该使用_ONSTACK变体。
*/
#define DECLARE_COMPLETION(work) \struct completion work = COMPLETION_INITIALIZER(work)# define DECLARE_COMPLETION_ONSTACK(work) DECLARE_COMPLETION(work)
/*** reinit_completion - reinitialize a completion structure* @x: pointer to completion structure that is to be reinitialized** This inline function should be used to reinitialize a completion structure so it can* be reused. This is especially important after complete_all() is used.*/
static inline void reinit_completion(struct completion *x)
{x->done = 0;
}
kernel/sched/completion.c
wait_for_common 等待完成量
static inline long __sched
do_wait_for_common(struct completion *x,long (*action)(long), long timeout, int state)
{/* x->done 为 0,表示条件尚未完成,需要进入等待逻辑 */if (!x->done) {/* 使用 DECLARE_SWAITQUEUE(wait) 声明一个等待队列条目 wait,用于将当前线程加入到 x->wait 的等待队列中 */DECLARE_SWAITQUEUE(wait);do {/* 检查当前线程是否有挂起的信号 */if (signal_pending_state(state, current)) {timeout = -ERESTARTSYS;break;}/* 将当前线程添加到 x->wait 的等待队列中,并设置线程的状态为 state */__prepare_to_swait(&x->wait, &wait);__set_current_state(state);raw_spin_unlock_irq(&x->wait.lock);/* 执行传入的回调函数(通常是一个等待操作,如 schedule_timeout),进入睡眠并更新超时时间 */timeout = action(timeout);raw_spin_lock_irq(&x->wait.lock);} while (!x->done && timeout);/* 调用 __finish_swait 将当前线程从等待队列中移除 */__finish_swait(&x->wait, &wait);if (!x->done)return timeout;}/* 如果 x->done 不等于 UINT_MAX(表示条件未被永久完成),则将其递减,表示一个等待线程已完成 有多个线程在等待同一个完成事件,x->done 的值可以大于 1,表示该事件可以唤醒多个线程。每当一个线程被唤醒并处理完成事件时,x->done 会递减,直到其值为 0,表示事件已被完全消耗。*/if (x->done != UINT_MAX)x->done--;return timeout ?: 1;
}static inline long __sched
__wait_for_common(struct completion *x,long (*action)(long), long timeout, int state)
{might_sleep();complete_acquire(x);raw_spin_lock_irq(&x->wait.lock);timeout = do_wait_for_common(x, action, timeout, state);raw_spin_unlock_irq(&x->wait.lock);complete_release(x);return timeout;
}static long __sched
wait_for_common(struct completion *x, long timeout, int state)
{return __wait_for_common(x, schedule_timeout, timeout, state);
}
wait_for_completion 等待完成量 state TASK_UNINTERRUPTIBLE 不可被中断
/*** 等待完成: - 等待任务的完成 * * @x: 保存此特定完成的状态 * * 这将等待特定任务完成的信号。它不可被打断,并且没有超时。 * * 另见类似例程(即 wait_for_completion_timeout()),它具有超时和可中断能力。另见 complete()。*/
void __sched wait_for_completion(struct completion *x)
{wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
}
EXPORT_SYMBOL(wait_for_completion);
wait_for_completion_killable 等待完成量
/* wait_for_completion_killable: - 等待一个任务的完成(可被杀死) * @x: 保存此特定完成的状态 * * 这会等待特定任务完成的信号。可以被杀死信号中断。 * * 返回:-ERESTARTSYS 如果被中断,0 如果已完成。
*/
int __sched wait_for_completion_killable(struct completion *x)
{/* #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) */long t = wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_KILLABLE);if (t == -ERESTARTSYS)return t;return 0;
}
EXPORT_SYMBOL(wait_for_completion_killable);
complete_with_flags 完成量完成
/* * 通用等待完成的处理程序; ** 它与信号量不同,信号量的默认情况相反,wait_for_completion默认是阻塞的,而信号量默认是非阻塞的。这个接口还使得‘完成’多个等待线程变得简单,这对于信号量并不是完全自然的。 ** 但更重要的是,这个原语记录了用法。信号量通常用于互斥,这会导致优先级反转。等待完成通常是一个同步点,而不是一个互斥点。 */static void complete_with_flags(struct completion *x, int wake_flags)
{unsigned long flags;raw_spin_lock_irqsave(&x->wait.lock, flags);if (x->done != UINT_MAX)x->done++;/* swake 链表为空不执行唤醒,有数据尝试唤醒第一个任务,之后删除该任务 */swake_up_locked(&x->wait, wake_flags);raw_spin_unlock_irqrestore(&x->wait.lock, flags);
}
complete_all 完成量释放所有线程
/** * complete_all: - 信号所有等待这个完成的线程 * @x: 持有这个特定完成的状态 * * 这将唤醒所有等待这个特定完成事件的线程。 * * 如果这个函数唤醒了一个任务,它将在访问任务状态之前执行完整的内存屏障。 * * 由于 complete_all() 将 @x 的完成状态永久设为完成,以允许多个等待者结束,如果 @x 需要再次使用,则必须在 @x 上调用 reinit_completion()。 代码必须确保在重新初始化 @x 之前,所有的等待者都已经被唤醒并完成。同时需要注意,函数 completion_done() 不能用来判断在调用 complete_all() 后是否还有等待者。
*/
void complete_all(struct completion *x)
{unsigned long flags;lockdep_assert_RT_in_threaded_ctx();raw_spin_lock_irqsave(&x->wait.lock, flags);x->done = UINT_MAX;swake_up_all_locked(&x->wait);raw_spin_unlock_irqrestore(&x->wait.lock, flags);
}
EXPORT_SYMBOL(complete_all);
complete 完成量释放单个线程
/** * complete: - 表示有一个线程在等待这个完成 * @x: 保存这个特定完成的状态 * * 这将唤醒一个在此完成上等待的线程。线程将按照它们被排队的顺序被唤醒。 * * 另见 complete_all()、wait_for_completion() 和相关的例程。 * * 如果此函数唤醒了一个任务,它在访问任务状态之前会执行完整的内存屏障。 */
void complete(struct completion *x)
{complete_with_flags(x, 0);
}
EXPORT_SYMBOL(complete);
completion_done 检查完成量是否有等待者
/*** completion_done - 测试完成操作是否有等待者* @x: 完成结构体** 返回值: 如果有等待者(正在执行 wait_for_completion()),返回 0;* 如果没有等待者,返回 1。** 注意,如果对 @x 调用了 complete_all(),此函数将始终返回 true。*/
bool completion_done(struct completion *x)
{unsigned long flags;if (!READ_ONCE(x->done))return false;/** 如果 ->done,我们需要等待 complete() 释放 ->wait.lock,* 否则可能会在 complete() 完成引用之前释放该完成对象。*/raw_spin_lock_irqsave(&x->wait.lock, flags);raw_spin_unlock_irqrestore(&x->wait.lock, flags);return true;
}
EXPORT_SYMBOL(completion_done);