Linux Kernel Complete

本文深入剖析了Linux内核中的`complete`函数和`wait_for_completion`系列函数。`complete`用于唤醒等待队列中的任务,`wait_for_completion`则用于挂起任务直到特定条件满足。文章详细解释了这两个函数的内部实现,包括等待队列的管理、超时处理和任务状态的切换,展示了Linux内核调度与同步机制的关键细节。
摘要由CSDN通过智能技术生成

1. Complete

1.1 Complete函数定义

文件路径:kernel/sched/completion.c

void complete(struct completion *x)
{
        unsigned long flags;

        raw_spin_lock_irqsave(&x->wait.lock, flags);

        if (x->done != UINT_MAX)
                x->done++;
        swake_up_locked(&x->wait);
        raw_spin_unlock_irqrestore(&x->wait.lock, flags);
}
EXPORT_SYMBOL(complete);

从代码分析,该函数的执行流程如下:

  • 获取传入completiom的等待队列锁,获取该锁的目的在于控制对等待队列增删的并发,并保存当前的中断状态,即代表此处可在未关中断的情况下调用,该语句为block函数。
  • 将x->done++
  • 调用swake_up_locked()函数,将x->wait链表中的等待队列的任务唤醒
  • 释放等待队列锁

1.2 completion 结构体定义

文件路径:include/linux/completion.h

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

其中,有两个成员变量,done代表信号量是否已满足,wait是一个链表的头,链表定义为swait_queue_head。

struct swait_queue_head {
        raw_spinlock_t          lock;
        struct list_head        task_list;
};

链表swait_queue_head有一个spinlock锁,在操作链表前需要先获取该锁。

1.2.1 补充

该链表是一个简单等待队列的实现。根据定义的注释,了解到所谓的简单等待队列正常等待队列的区别为:允许确定性行为
它通过两个方面来执行:
1)不允许在irqdisabled状态下调用swake_up_all,并在每次唤醒时放弃锁定,让更高优先级的任务有机会运行。
2)放弃很多正常等待队列支持的功能

  • 将 INTERRUPTIBLE 和 UNINTERRUPTIBLE 混合在同一个等待队列上休眠;所有唤醒都是 TASK_NORMAL,以避免 O(n) 查找正确的睡眠状态。
  • !exclusive;因为这会导致 O(n) 唤醒,所以一切都是排他的。因此 swake_up_one 每次只会唤醒一个waiter。
  • 自定义唤醒回调函数;因为您不能对随机代码做出任何保证。这也允许在 RT 中使用 swait,以便原始自旋锁可用于 swait 队列头。

2. 等待函数

等待函数向外封装了很多个函数,对应功能分别如下:

函数名称支持timeout设置可被中断可被Kill信号唤醒调用者被视为IO
wait_for_completionXXXX
wait_for_completion_timeoutXXX
wait_for_completion_interruptibleXXX
wait_for_completion_interruptible_timeoutXX
wait_for_completion_killableXXX
wait_for_completion_killable_timeoutXXX
wait_for_common_ioXXXY
wait_for_completion_io_timeoutYXXY

向下分别调用了两个函数,如下图所示,下面依次分析实现原理。

在这里插入图片描述

2.1 wait_for_common & wait_for_common_io

对比一下两个函数的实现,如下,他们都调用了相同的函数__wait_for_common,只不过传入的参数不同。

wait_for_common函数传入的参数为schedule_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_common_io函数传入的参数为io_schedule_timeout

static long __sched
wait_for_common_io(struct completion *x, long timeout, int state)
{
        return __wait_for_common(x, io_schedule_timeout, timeout, state);
}

两者的区别为是否设置current的in_iowait状态。详细解析如下:

2.1.1 schedule_timeout

将当前task调度出cpu,并根据传入的timeout决定何时重新调度。schedule_timeout函数的详细解析请查看文章:schedule_timeout

2.1.2 io_schedule_timeout

io_schedule_timeout函数根本上还是调用schedule_timeout函数,只不过在调用schedule_timeout函数的前后,设置current的in_iowait变量标志。在执行调度操作时,会将其设置为1,在指定task完成调度后,再将其设置为原始值。

long __sched io_schedule_timeout(long timeout)
{
        int token;
        long ret;

        token = io_schedule_prepare();
        ret = schedule_timeout(timeout); //将当前task调度出cpu,并根据传入的timeout决定何时重新调度。
        io_schedule_finish(token);

        return ret;
}
EXPORT_SYMBOL(io_schedule_timeout);

2.2 __wait_for_common

wait_for_common& wait_for_common_io函数中均调用了__wait_for_common函数。同样,首先看一下实现:

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;
}

该函数通过获取信号量的锁,判断是否可以执行action函数。同时提供了超时与调度参数功能。
具体参数的含义:

参数定义含义
struct completion *x等待的信号量
long (*action)(long)执行的操作
long timeout超时时长
int state调度标志

在拿到信号量的spinlock后,会调用do_wait_for_common函数,传入的参数与原函数基本相同,下面看一下do_wait_for_common函数的实现。

2.2 do_wait_for_common

实现如下,详细解析请参考注释。
该函数主要是在等待信号量,其中又包括了一些额外设置,例如,超时时间、等待时task的状态等等。

static inline long __sched
do_wait_for_common(struct completion *x,
                   long (*action)(long), long timeout, int state)
{
        if (!x->done) { //在x->done == 0 时,运行以下代码
                DECLARE_SWAITQUEUE(wait); //声明wait全局变量,其包含current(指向当前task)与task_list两个成员

                do {
                        if (signal_pending_state(state, current)) {
                                timeout = -ERESTARTSYS;
                                break;
                        }
                        __prepare_to_swait(&x->wait, &wait); //将wait设置为等待task,并将其放置到的等待队列中
                        __set_current_state(state); //设置current->__state状态为传入的state
                        raw_spin_unlock_irq(&x->wait.lock); // 释放信号量的锁,此时该信号量的等待task可以获取锁,并执行相应的action,多个task执行action的顺序由设置的task state与Linux 调度策略决定
                        timeout = action(timeout); // 执行action等待函数,并将timeout超时时间作为参数传入
                        raw_spin_lock_irq(&x->wait.lock); //在action执行完成,或者设置的时间过期后,重新拿信号量的锁,重复操作
                } while (!x->done && timeout); //若x->done == 0 (即,无其它task获取该complete); 或者timeout未超时时。则继续循环等待
                __finish_swait(&x->wait, &wait);
                if (!x->done)
                        return timeout;
        }
        if (x->done != UINT_MAX)
                x->done--;
        return timeout ?: 1;
}

3. 总结

至此,大家应该对complete有了一个基本的印象,这里总结一下:
1) complete的功能为唤醒下一个task,其实现为将done++;
2) wait_for_completion系列函数的功能为等待指定的信号量,等待信号量可以传入不同的参数,例如是否可被中断,超时时间设置等等;在等待的信号量的done数值为0时,代表等到了相应的信号量,继续执行相应操作;
3) 唤醒的顺序,根据调用wait_for_completion函数的先后顺序而定。在wait函数中会将等待task放入task_list

3.1 图示如下

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值