Linux内核抢占机制 - 实现

本文详细探讨了Linux内核抢占机制的实现,包括Scheduler Overview、触发抢占和执行抢占等部分。阐述了调度器核心、调度类以及preempt_count在抢占中的作用,涉及到Tick Preemption、Wakeup Preemption以及Kernel Preemption的触发和处理流程,同时讲解了不同调度类的接口和方法。文章还介绍了处理器相关的时钟中断处理和调度中断处理,以及在x86架构下的实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文首发于 http://oliveryang.net,转载时请包含原文或者作者网站链接。

本文主要围绕 Linux 内核调度器 Preemption 的相关实现进行讨论。其中涉及的一般操作系统和 x86 处理器和硬件概念,可能也适用于其它操作系统。

1. Scheduler Overview

Linux 调度器的实现实际上主要做了两部分事情,

  1. 任务上下文切换

    Preemption Overview 里,我们对任务上下文切换做了简单介绍。可以看到,任务上下文切换有两个层次的实现:公共层处理器架构相关层。任务运行状态的切换的实现最终与处理器架构密切关联。因此 Linux 做了很好的抽象。在不同的处理器架构上,处理器架构相关的代码和公共层的代码相互配合共同实现了任务上下文切换的功能。这也使得任务上下文切换代码可以很容易的移植到不同的处理器架构上。

  2. 任务调度策略

    同样的,为了满足不同类型应用场景的调度需求,Linux 调度器也做了模块化处理。调度策略的代码也可被定义两层 Scheduler Core (调度核心)Scheduling Class (调度类)。调度核心的代码实现了调度器任务调度的基本操作,所有具体的调度策略都被封装在具体调度类的实现中。这样,Linux 内核的调度策略就支持了模块化的扩展能力。Linux v3.19 支持以下调度类和调度策略,

    • Real Time (实时)调度类 - 支持 SCHED_FIFO 和 SCHED_RR 调度策略。
    • CFS (完全公平)调度类 - 支持 SCHED_OTHER(SCHED_NORMAL),SCHED_BATCH 和 SCHED_IDLE 调度策略。(注:SCHED_IDLE 是一种调度策略,与 CPU IDLE 进程无关)。
    • Deadline (最后期限)调度类 - 支持 SCHED_DEADLINE 调度策略。

    Linux 调度策略设置的系统调用 SCHED_SETATTR(2) 的手册有对内核支持的各种调度策略的详细说明。内核的调度类和 sched_setattr 支持的调度策略命名上不一致但是存在对应关系,而且调度策略的命名更一般化。这样做的一个好处是,同一种调度策略未来可能有不同的内核调度算法来实现。新的调度算法必然引入新的调度类。内核引入新调度类的时候,使用这个系统调用的应用不需要去为之修改。调度策略本身也是 POSIX 结构规范的一部分。上述调度策略中,SCHED_DEADLINE 是 Linux 独有的,POSIX 规范中并无此调度策略。SCHED(7) 对 Linux 调度 API 和历史发展提供了概览,值得参考。

1.1 Scheduler Core

调度器核心代码位于 kernel/sched/core.c 文件。主要包含了以下实现,

  • 调度器的初始化,调度域初始化。
  • 核心调度函数 __schedule 及上下文切换的通用层代码。
  • 时钟周期处理的通用层代码,包含 Tick Preemption 的代码。
  • 唤醒函数,Per-CPU Run Queue 操作的代码,包含 Wakeup Preemption 通用层的代码。
  • 基于高精度定时器中断实现的高精度调度,处理器间调度中断。
  • 处理器 IDLE 线程,调度负载均衡,迁移任务的代码。
  • 与调度器有关的系统调用的实现代码。

调度器核心代码的主要作用就是调度器的模块化实现,降低了跨处理器平台移植和实现新调度算法模块的重复代码和模块间的耦合度,提高了内核可移植性和可扩展性。

1.2 Scheduling Class

在 Linux 内核引入一种新调度算法,基本上就是实现一个新的 Scheduling Class (调度类)。调度类需要实现的所有借口定义在 struct sched_class 里。下面对其中最重要的一些调度类接口做简单的介绍,

  • enqueue_task

    将待运行的任务插入到 Per-CPU Run Queue。典型的场景就是内核里的唤醒函数,将被唤醒的任务插入 Run Queue 然后设置任务运行态为 TASK_RUNNING

    对 CFS 调度器来说,则是将任务插入红黑树,给 nr_running 增加计数。

  • dequeue_task

    将非运行态任务移除出 Per-CPU Run Queue。典型的场景就是任务调度引起阻塞的内核函数,把任务运行态设置成 TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE,然后调用 schedule 函数,最终触发本操作。

    对 CFS 调度器来说,则是将不在处于运行态的任务从红黑树中移除,给 nr_running 减少计数。

  • yield_task

    处于运行态的任务申请主动让出 CPU。典型的场景就是处于运行态的应用调用 sched_yield(2) 系统调用,直接让出 CPU。此时系统调用 sched_yield 系统调用先调用 yield_task 申请让出 CPU,然后调用 schedule 去做上下文切换。

    对 CFS 调度器来说,如果 nr_running 是 1,则直接返回,最终 schedule 函数也不产生上下文切换。否则,任务被标记为 skip 状态。调度器在红黑树上选择待运行任务时肯定会跳过该任务。之后,因为 schedule 函数被调用,pick_next_task 最终会被调用。其代码会从红黑树中最左侧选择一个任务,然后把要放弃运行的任务放回红黑树,然后调用上下文切换函数做任务上下文切换。

  • check_preempt_curr

    用于在待运行任务插入 Run Queue 后,检查是否应该 Preempt 正在 CPU 运行的当前任务。Wakeup Preemption 的实现逻辑主要在这里。

    对 CFS 调度器而言,主要是在是否能满足调度时延和是否能保证足够任务运行时间之间来取舍。CFS 调度器也提供了预定义的 Threshold 允许做 Wakeup Preemption 的调优。本文有专门章节对 Wakeup Preemption 做详细分析。

  • pick_next_task

    选择下一个最适合调度的任务,将其从 Run Queue 移除。并且如果前一个任务还保持在运行态,即没有从 Run Queue 移除,则将当前的任务重新放回到 Run Queue。内核 schedule 函数利用它来完成调度时任务的选择。

    对 CFS 调度器而言,大多数情况下,下一个调度任务是从红黑树的最左侧节点选择并移除。如果前一个任务是其它调度类,则调用该调度类的 put_prev_task 方法将前一个任务做正确的安置处理。但如果前一个任务如果也属于 CFS 调度类的话,为了效率,跳过调度类标准方法 put_prev_task,但核心逻辑仍旧是 put_prev_task_fair 的主要部分。关于 put_prev_task 的具体功能,请参考随后的说明。

  • put_prev_task

    将前一个正在 CPU 上运行的任务做拿下 CPU 的处理。如果任务还在运行态则将任务放回 Run Queue,否则,根据调度类要求做简单处理。此函数通常是 pick_next_task 的密切关联操作,是 schedule 实现的关键部分。

    如果前一个任务属于 CFS 调度类,则使用 CFS 调度类的具体实现 put_prev_task_fair。此时,如果任务还是 TASK_RUNNING 状态,则被重新插入到红黑树的最右侧。如果这个任务不是 TASK_RUNNING 状态,则已经从红黑树移除过了,只需要修改 CFS 当前任务指针 cfs_rq->curr 即可。

  • select_task_rq

    为给定的任务选择一个 Run Queue,返回 Run Queue 所属的 CPU 号。典型的使用场景是唤醒,fork/exec 进程时,给进程选择一个 Run Queue,这也给调度器一个 CPU 负载均衡的机会。

    对 CFS 调度器而言,主要是根据传入的参数要求找到符合亲和性要求的最空闲的 CPU 所属的 Run Queue。

  • set_curr_task

    当任务改变自己的调度类或者任务组时,该函数被调用。用户进程可以使用 sched_setscheduler系统调用,通过设置自己新的调度策略来修改自己的调度类。

    对 CFS 调度器而言,当任务把自己调度类从其它类型修改成 CFS 调度类,此时需要把该任务设置成正当前 CPU 正在运行的任务。例如把任务从红黑树上移除,设置 CFS 当前任务指针 cfs_rq->curr 和调度统计数据等。

  • task_tick

    这个函数通常在系统周期性 (Per-tick) 的时钟中断上下文调用,调度类可以把 Per-tick 处理的事务交给该方法执行。例如,调度器的统计数据更新,Tick Preemption 的实现逻辑主要在这里。Tick Preemption 主要判断是否当前运行任务需要 Preemption 来被强制剥夺运行。

    对 CFS 调度器而言,Tick Preemption 主要是在是否能满足调度时延和是否能保证足够任务运行时间之间来取舍。CFS 调度器也提供了预定义的 Threshold 允许做 Tick Preemption 的调优。需要进一步了解 Tick Preemption,请参考 2.1 章节。

Linux 内核的 CFS 调度算法就是通过实现该调度类结构来实现其主要逻辑的,CFS 的代码主要集中在 kernel/sched/fair.c 源文件。下面的 sched_class 结构初始化代码包含了本节介绍的所有方法在 CFS 调度器实现中的入口函数名称,

    const struct sched_class fair_sched_class = {

                    [...snipped...]

            .enqueue_task           = enqueue_task_fair,
            .dequeue_task           = dequeue_task_fair,
            .yield_task             = yield_task_fair,

                    [...snipped...]

            .check_preempt_curr     = check_preempt_wakeup,

                    [...snipped...]

            .pick_next_task         = pick_next_task_fair,
            .put_prev_task      = put_prev_task_fair,

                    [...snipped...]

            .select_task_rq     = select_task_rq_fair,

                    [...snipped...]

            .set_curr_task          = set_curr_task_fair,

                    [...snipped...]

            .task_tick              = task_tick_fair,

                    [...snipped...]
    };

1.3 preempt_count

Linux 内核为支持 Kernel Preemption 而引入了 preempt_count 计数器。如果 preempt_count 为 0,就允许 Kernel Preemption,否则就不允许。内核函数 preempt_disablepreempt_enable 用来内核代码的临界区动态关闭和打开 Kernel Preemption。其主要原理就是要通过对这个计数器的加和减来实现关闭和打开。

一般而言,打开 Kernel Preemption 特性的内核,在尽可能的情况下,允许在内核态通过 Tick Preemption 和 Wakeup Preemption 去触发和执行 Kernel Preemption。但在以下情形,Kernel Preemption 会有关闭和打开操作,

  • 内核显式调用 preempt_disable 关闭抢占期间,
  • 进入中断上下文时,preempt_count 计数器被加操作置为非零。退出中断时打开 Kernel Preemption。
  • 获取各种内核锁以后,preempt_disable 被间接调用。退出内核锁会有 preempt_enable 操作。

关于preempt_disablepreempt_enable 的用法,请参考 Proper Locking Under a Preemptible Kernel

早期 Linux 内核,preempt_count 是每个任务所属的 struct thread_info 里的一个成员,是 Per-thread 的。

而在 Linux 新内核,为了优化 Kernel Preemption 带来的频繁检查 preempt_count 的开销,Linus 和调度器的维护者决定对其做更多的优化。因此,Per-CPU preempt_count 的优化被集成到 3.13 版内核。所以,新内核的的 preempt_count 定义如

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值