Linux内核任务调度时机总结

一、调度机制概述

Linux内核的任务调度主要基于两个核心机制:

  1. 直接调用schedule():主动让出CPU
  2. 设置TIF_NEED_RESCHED标志:延迟调度,在合适的时机检查该标志并调用schedule()

核心调度函数__schedule()(位于kernel/sched/core.c)的注释清楚地说明了驱动调度器进入的主要方式:

c 复制代码
/*
 * __schedule() is the main scheduler function.
 *
 * The main means of driving the scheduler and thus entering this function are:
 *
 *   1. Explicit blocking: mutex, semaphore, waitqueue, etc.
 *
 *   2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return
 *      paths. For example, see arch/x86/entry_64.S.
 *
 *      To drive preemption between tasks, the scheduler sets the flag in timer
 *      interrupt handler sched_tick().
 *
 *   3. Wakeups don't really cause entry into schedule(). They add a
 *      task to the run-queue and that's it.
 */

二、主动调度(Voluntary Scheduling)

1. schedule() 直接调用

位置 : kernel/sched/core.c

c 复制代码
asmlinkage __visible void __sched schedule(void)
{
    struct task_struct *tsk = current;

    sched_submit_work(tsk);
    do {
        preempt_disable();
        __schedule(SM_NONE);
        sched_preempt_enable_no_resched();
    } while (need_resched());
    sched_update_worker(tsk);
}

触发场景:

  • 任务主动阻塞(等待资源)
  • 内核同步原语(mutex, semaphore, completion等)
  • 等待队列操作

2. sched_yield() 系统调用

位置 : kernel/sched/syscalls.c

c 复制代码
SYSCALL_DEFINE0(sched_yield)
{
    do_sched_yield();
    return 0;
}

static void do_sched_yield(void)
{
    struct rq_flags rf;
    struct rq *rq;

    rq = this_rq_lock_irq(&rf);

    schedstat_inc(rq->yld_count);
    current->sched_class->yield_task(rq);

    preempt_disable();
    rq_unlock_irq(rq, &rf);
    sched_preempt_enable_no_resched();

    schedule();  // 直接调用调度器
}

触发条件 : 用户空间调用sched_yield()系统调用,主动让出CPU。

3. cond_resched() 条件调度

位置 : kernel/sched/core.c

c 复制代码
int __sched __cond_resched(void)
{
    if (should_resched(0) && !irqs_disabled()) {
        preempt_schedule_common();
        return 1;
    }
    ...
    return 0;
}

触发条件:

  • 仅在非抢占式内核(CONFIG_PREEMPTION=n)中有效
  • 内核代码显式调用cond_resched()检查是否需要调度
  • 常用于长循环中提供调度点

4. 睡眠/阻塞导致的调度

示例: mutex_lock
位置 : kernel/locking/mutex.c

c 复制代码
// 在mutex_lock中,当无法获取锁时
set_current_state(state);
for (;;) {
    if (__mutex_trylock(lock))
        goto acquired;
    ...
    schedule_preempt_disabled();  // 调用调度器
    ...
}

触发场景:

  • 互斥锁竞争
  • 信号量操作
  • 等待队列(waitqueue)
  • 完成量(completion)
  • IO等待

三、抢占式调度(Preemptive Scheduling)

1. 用户抢占(User Preemption)

触发时机: 从内核返回用户空间时检查TIF_NEED_RESCHED标志

位置 : kernel/entry/common.c

c 复制代码
__always_inline unsigned long exit_to_user_mode_loop(struct pt_regs *regs,
                                                     unsigned long ti_work)
{
    while (ti_work & EXIT_TO_USER_MODE_WORK) {
        local_irq_enable_exit_to_user(ti_work);

        if (ti_work & (_TIF_NEED_RESCHED | _TIF_NEED_RESCHED_LAZY))
            schedule();  // 返回用户空间前调度

        ...
        ti_work = read_thread_flags();
    }
    return ti_work;
}

调用路径:

复制代码
syscall_exit_to_user_mode()
  -> __syscall_exit_to_user_mode_work()
    -> exit_to_user_mode_prepare()
      -> exit_to_user_mode_loop()  // 检查并调度

2. 内核抢占(Kernel Preemption)

前提: 内核配置了CONFIG_PREEMPTION

2.1 preempt_schedule() - preempt_enable时抢占

位置 : kernel/sched/core.c

c 复制代码
/*
 * This is the entry point to schedule() from in-kernel preemption
 * off of preempt_enable.
 */
asmlinkage __visible void __sched notrace preempt_schedule(void)
{
    /*
     * If there is a non-zero preempt_count or interrupts are disabled,
     * we do not want to preempt the current task. Just return..
     */
    if (likely(!preemptible()))
        return;
    preempt_schedule_common();
}

触发机制 :
位置 : include/linux/preempt.h

c 复制代码
#define preempt_enable() \
do { \
    barrier(); \
    if (unlikely(preempt_count_dec_and_test())) \
        __preempt_schedule(); \  // 抢占点
    barrier(); \
} while (0)

preempt_count减为0且TIF_NEED_RESCHED被设置时,触发抢占调度。

2.2 preempt_schedule_irq() - 中断返回内核态时抢占

位置 : kernel/sched/core.c

c 复制代码
asmlinkage __visible void __sched preempt_schedule_irq(void)
{
    enum ctx_state prev_state;

    /* Catch callers which need to be fixed */
    BUG_ON(preempt_count() || !irqs_disabled());

    prev_state = exception_enter();

    do {
        preempt_disable();
        local_irq_enable();
        __schedule(SM_PREEMPT);
        local_irq_disable();
        sched_preempt_enable_no_resched();
    } while (need_resched());

    exception_exit(prev_state);
}

调用位置 : kernel/entry/common.c

c 复制代码
void raw_irqentry_exit_cond_resched(void)
{
    if (!preempt_count()) {
        rcu_irq_exit_check_preempt();
        if (need_resched())
            preempt_schedule_irq();  // 中断返回时抢占
    }
}

触发条件:

  • 从中断返回内核态
  • preempt_count为0
  • TIF_NEED_RESCHED标志被设置

四、时钟中断触发的调度

1. sched_tick() - 时钟滴答处理

位置 : kernel/sched/core.c

c 复制代码
void sched_tick(void)
{
    int cpu = smp_processor_id();
    struct rq *rq = cpu_rq(cpu);
    struct task_struct *donor;
    struct rq_flags rf;

    ...
    rq_lock(rq, &rf);
    donor = rq->donor;

    update_rq_clock(rq);
    ...

    // 调用调度类的task_tick方法
    donor->sched_class->task_tick(rq, donor, 0);
    ...
}

调用路径:

复制代码
timer_interrupt
  -> update_process_times()  // kernel/time/timer.c
    -> sched_tick()

2. CFS调度类的task_tick_fair()

位置 : kernel/sched/fair.c

c 复制代码
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
    struct cfs_rq *cfs_rq;
    struct sched_entity *se = &curr->se;

    for_each_sched_entity(se) {
        cfs_rq = cfs_rq_of(se);
        entity_tick(cfs_rq, se, queued);
    }
    ...
}

entity_tick()调用update_curr()检查时间片 :
位置 : kernel/sched/fair.c

c 复制代码
static void update_curr(struct cfs_rq *cfs_rq)
{
    ...
    if (cfs_rq->nr_queued == 1)
        return;

    if (resched || did_preempt_short(cfs_rq, curr)) {
        resched_curr_lazy(rq);  // 设置重调度标志
        clear_buddies(cfs_rq, curr);
    }
}

3. RT调度类的task_tick_rt()

位置 : kernel/sched/rt.c

c 复制代码
static void task_tick_rt(struct rq *rq, struct task_struct *p, int queued)
{
    ...
    // RR任务的时间片管理
    if (p->policy != SCHED_RR)
        return;

    if (--p->rt.time_slice)
        return;

    p->rt.time_slice = sched_rr_timeslice;

    // 时间片用完,重新排队并设置重调度标志
    for_each_sched_rt_entity(rt_se) {
        if (rt_se->run_list.prev != rt_se->run_list.next) {
            requeue_task_rt(rq, p, 0);
            resched_curr(rq);  // 设置重调度标志
            return;
        }
    }
}

五、睡眠/唤醒导致的调度

1. 进程唤醒 - try_to_wake_up()

位置 : kernel/sched/core.c

c 复制代码
int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
    ...
    // 将任务加入运行队列
    // 如果被唤醒的任务优先级更高,触发抢占
    if (p->sched_class->task_woken)
        p->sched_class->task_woken(rq, p);
    ...
}

wake_up_process() :
位置 : kernel/sched/core.c

c 复制代码
int wake_up_process(struct task_struct *p)
{
    return try_to_wake_up(p, TASK_NORMAL, 0);
}

2. 唤醒抢占检查 - wakeup_preempt()

位置 : kernel/sched/core.c

c 复制代码
void wakeup_preempt(struct rq *rq, struct task_struct *p, int flags)
{
    struct task_struct *donor = rq->donor;

    if (p->sched_class == donor->sched_class)
        donor->sched_class->wakeup_preempt(rq, p, flags);
    else if (sched_class_above(p->sched_class, donor->sched_class))
        resched_curr(rq);  // 高优先级调度类,立即抢占
    ...
}

3. CFS唤醒抢占检查 - check_preempt_wakeup_fair()

位置 : kernel/sched/fair.c

c 复制代码
static void check_preempt_wakeup_fair(struct rq *rq, struct task_struct *p, int flags)
{
    ...
    // 检查新唤醒的任务是否应该抢占当前任务
    if (pick_eevdf(cfs_rq) == pse)
        goto preempt;

    return;

preempt:
    resched_curr_lazy(rq);  // 设置重调度标志
}

六、resched_curr() - 设置重调度标志

位置 : kernel/sched/core.c

c 复制代码
/*
 * reshed_curr - mark rq's current task 'to be rescheduled now'.
 *
 * On UP this means the setting of the need_resched flag, on SMP it
 * might also involve a cross-CPU call to trigger the scheduler on
 * the target CPU.
 */
static void __resched_curr(struct rq *rq, int tif)
{
    struct task_struct *curr = rq->curr;
    struct thread_info *cti = task_thread_info(curr);
    int cpu;

    ...
    if (cpu == smp_processor_id()) {
        set_ti_thread_flag(cti, tif);  // 本地CPU直接设置标志
        if (tif == TIF_NEED_RESCHED)
            set_preempt_need_resched();
        return;
    }

    if (set_nr_and_not_polling(cti, tif)) {
        if (tif == TIF_NEED_RESCHED)
            smp_send_reschedule(cpu);  // 远程CPU发送IPI
    }
    ...
}

void resched_curr(struct rq *rq)
{
    __resched_curr(rq, TIF_NEED_RESCHED);
}

七、其他调度时机

1. 优先级变更

当任务的优先级改变时,可能需要重新调度:

位置 : kernel/sched/fair.c

c 复制代码
static void prio_changed_fair(struct rq *rq, struct task_struct *p, int oldprio)
{
    ...
    if (task_current_donor(rq, p)) {
        if (p->prio > oldprio)
            resched_curr(rq);  // 优先级降低,需要调度
    } else
        wakeup_preempt(rq, p, 0);
}

2. 负载均衡

当进行CPU间负载均衡时,可能触发调度:

位置 : kernel/sched/core.c

c 复制代码
// 在sched_tick()中
#ifdef CONFIG_SMP
    if (!scx_switched_all()) {
        rq->idle_balance = idle_cpu(cpu);
        sched_balance_trigger(rq);  // 触发负载均衡
    }
#endif

3. 进程创建

新进程创建后唤醒时:

位置 : kernel/sched/core.c

c 复制代码
// wake_up_new_task()中
wakeup_preempt(rq, p, 0);  // 检查是否需要抢占

4. cgroup调度组变更

当任务在cgroup调度组之间移动时,可能触发调度。


八、调度时机总结表

调度类型 触发机制 调用路径 配置依赖
主动调度 schedule()直接调用 阻塞、等待、yield
条件调度 cond_resched() 内核长循环中 非抢占内核
用户抢占 返回用户空间 exit_to_user_mode_loop()
内核抢占 preempt_enable() __preempt_schedule() CONFIG_PREEMPTION
中断返回抢占 中断返回内核态 preempt_schedule_irq() CONFIG_PREEMPTION
时钟中断调度 sched_tick() task_tick_xxx()
唤醒调度 try_to_wake_up() wakeup_preempt()
优先级变更 __sched_setscheduler() prio_changed_xxx()

九、关键标志位

位置 : arch/x86/include/asm/thread_info.h

c 复制代码
#define TIF_NEED_RESCHED      3    /* rescheduling necessary */
#define TIF_NEED_RESCHED_LAZY 4    /* Lazy rescheduling needed */

这些标志位存储在thread_info->flags中,用于标识是否需要进行调度。TIF_NEED_RESCHED_LAZY用于延迟抢占模式。


十、调度检查点总结

Linux内核在以下位置检查TIF_NEED_RESCHED标志:

  1. 返回用户空间时: 系统调用返回、异常返回、中断返回用户态
  2. preempt_enable()时: 抢占计数器归零且开启内核抢占
  3. 中断返回内核态时: preempt_schedule_irq()
  4. 显式调用cond_resched()时: 非抢占内核中的显式调度点

十一、核心设计理念

调度器的核心设计理念是:在需要调度时设置标志位,在安全的检查点进行实际的上下文切换,这既保证了系统的响应性,又确保了内核数据结构的一致性。

这种设计确保了:

  • 安全性: 只在内核数据结构一致的点进行上下文切换
  • 响应性: 通过抢占机制保证高优先级任务及时响应
  • 灵活性: 支持多种调度策略和配置选项

总结

Linux内核任务调度的主要时机总结如下:

  1. 主动调度 - 进程直接调用schedule()让出CPU,包括:
    • 阻塞操作(mutex、semaphore、waitqueue等)
    • sched_yield()系统调用
    • cond_resched()条件调度
  2. 用户抢占 - 从内核返回用户空间时检查TIF_NEED_RESCHED标志
  3. 内核抢占 - 需配置CONFIG_PREEMPTION:
    • preempt_enable()时检查抢占
    • 中断返回内核态时抢占
  4. 时钟中断调度 - sched_tick()检测时间片耗尽
  5. 唤醒调度 - 高优先级进程被唤醒时触发抢占
  6. 其他时机 - 优先级变更、负载均衡、进程创建等

End

相关推荐
草莓熊Lotso1 小时前
Ext 系列文件系统核心:块、分区、inode 与块组结构详解
android·linux·c语言·开发语言·c++·人工智能·文件
开开心心_Every1 小时前
系统清理工具清理缓存日志,启动卸载管理
运维·服务器·网络·数学建模·电脑·excel·抽象代数
qq_454245032 小时前
开源GraphMindStudio工作流引擎:自动化与AI智能体的理想核心
运维·人工智能·开源·c#·自动化
Vincent_Zhang2332 小时前
第五周:传输层(上)
网络
小鸡食米4 小时前
Nginx基础
运维·服务器·nginx
~黄夫人~10 小时前
Linux 权限管理:用户组 + 特殊权限 + ACL 解析
linux·运维·计算机·学习笔记·权限管理
Godspeed Zhao10 小时前
现代智能汽车中的无线技术106——ETC(0)
网络·人工智能·汽车
2501_9071368211 小时前
手搓仓库管理系统Senbar-1.0.4(附带财务管理板块)
运维·服务器·软件需求
枷锁—sha11 小时前
【pwn系列】Pwndbg 汇编调试实操教程
网络·汇编·笔记·安全·网络安全