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 高效地让出时间片,直到世界准备好了再醒来。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部