Linux 内核 completion 机制深度实战
一、从问题出发:驱动为何需要「完成」语义
在设备驱动开发中,CPU 启动某个异步操作(如 SPI 传输、固件加载、硬件校准),随后需要等待该操作完成才能继续。这是经典的「事件等待」场景。Linux 内核为此提供了struct completion机制,本质上是一个轻量级同步原语,专门用于「我已经准备好了,你可以继续了」这类单向通知。
二、数据结构剖析
打开include/linux/completion.h可以看到:
struct completion {
unsigned int done;
struct wait_queue_head_t wait;
};
关键在于done是一个一次性发射的单调计数器。每次complete()调用会让done += 1;wait_for_completion()只消费一次自增,设计上保证了多 waiter 竞争 single event 场景的正确性。
核心 API 体系
DECLARE_COMPLETION(my_comp); // 静态声明
init_completion(&my_comp); // 动态初始化(必须调用)
reinit_completion(&my_comp); // 重置允许重复使用
// 等待侧 API
wait_for_completion(&comp); // 不可中断等待
wait_for_completion_interruptible(&comp); // 可中断
wait_for_completion_timeout(&comp, HZ); // 带超时
// 通知侧 API
complete(&comp); // 唤醒一个 waiter
complete_all(&comp); // 唤醒所有 waiter
三、内核实现分析
complete()在kernel/sched/completion.c中实现,使用raw_spinlock而非spinlock,因为在hardirq上下文(如 DMA 完成中断 handler)中也可安全调用。done自增+唤醒原子操作在锁内完成,保证 event 不丢失。
wait_for_completion()逻辑清晰:获取 spinlock 检查是否 done > 0,若成立直接消费(done--),否则进入 cirque_wait_wait 设置 task state 并调度出去,信号或超时到达后退出。
四、与信号量、条件变量对比
- completion:阻塞 wait,单次 event,唤醒 1 个或 N 个,开销极低
- semaphore:阻塞 P 操作,V 操作可累加,唤醒 1 个,开销中
- mutex:阻塞 lock,仅 owner unlock,唤醒 1 个,公平锁开销高
- wait_event:阻塞 condition,condition 可反复触发,唤醒所有 wakers,开销中
核心区别:completion 强调「事件发生」,done单调性保证不会被假唤醒消费掉;wait_event()只检查 condition 是否为真,没有状态记录。
五、驱动实战场景
SPI / I2C 异步传输完成
struct spi_dev {
struct completion xfer_done;
struct spi_device *spi;
};
static irqreturn_t spi_complete_handler(int irq, void *dev_id) {
struct spi_dev *dev = dev_id;
complete(&dev->xfer_done);
return IRQ_HANDLED;
}
int spi_sync_xfer(struct spi_dev *dev, struct spi_transfer *xfer) {
init_completion(&dev->xfer_done);
struct spi_message msg;
spi_message_init(&msg);
spi_message_add_tail(xfer, &msg);
spi_async(dev->spi, &msg);
wait_for_completion(&dev->xfer_done);
return xfer->len;
}
固件加载
static void firmware_loaded(const struct firmware *fw, void *context, int err) {
struct firmware_loader *ldr = context;
ldr->data = err ? NULL : (void*)fw->data;
complete_all(&ldr->fw_loaded);
}
int load_fw_sync(struct firmware_loader *ldr, const char *name) {
reinit_completion(&ldr->fw_loaded);
request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT, name,
ldr->dev, GFP_KERNEL, ldr, firmware_loaded);
wait_for_completion_interruptible_timeout(&ldr->fw_loaded, 5*HZ);
return ldr->data ? 0 : -ETIMEDOUT;
}
使用complete_all()是因为可能多个组件同时等待同一固件。reinit_completion()确保每次加载前重置状态。
六、Completion 链与多阶段事件
复杂设备可能需要多阶段初始化,每个阶段的 completion 作为该阶段完成信号量:
struct multi_dev {
struct completion stage1; // 硬件上电
struct completion stage2; // 固件加载
struct completion stage3; // 自检完成
};
// 每个阶段的 irq 或回调中调用 complete(&dev->stageN)
// 通过 reinit_completion() 支持重复初始化流程
七、与 Workqueue 集成
static void work_handler(struct work_struct *work) { do_time_consuming_work(); complete(&wc->done); } int submit_and_wait(struct work_completion *wc) { reinit_completion(&wc->done); queue_work(system_unbound_wq, &wc->work); return wait_for_completion_interruptible(&wc->done); }区别:
flush_work()等待 worker 函数退出;wait_for_completion()等待某个事件点,在 worker 函数内任意位置触发。八、性能与注意事项
- Hot path(complete 侧):raw_spinlock + swake_up,约 ~50 个指令周期
- Cold path(wait 侧):schedule() 触发上下文切换,约 2-5μs
- 多 waiter 场景:complete_all() 遍历整个 wait queue,复杂度 O(n)
常见问题规避:必须调用 init_completion;多 waiter 使用 complete_all;中断上下文不能 wait;重复触发需 reinit;wait_for_completion_interruptible 需检查返回值处理重试。
九、总结
completion 是 Linux 内核中最轻量、最高效的事件完成同步原语,适合「启动→等待完成」的单向事件模式、硬件异步操作到进程上下文桥接、驱动模块初始化顺序依赖等场景。理解 completion 的关键是抓住「done 单调计数器」这一本质——它不保护任何资源,仅仅记录「事件发生过多少次」。
「完成」不是「拥有」,而是「知晓」。completion 让 CPU 高效地让出时间片,直到世界准备好了再醒来。

发表评论 取消回复