[TOC]

kernel/sched 调度器核心(Scheduler Core) 进程调度与上下文切换的决策中心

历史与背景

这项技术是为了解决什么特定问题而诞生的?

kernel/sched/core.c 及其关联文件(如fair.c, rt.c)共同构成了Linux内核的进程调度器(Process Scheduler)。这项技术是为了解决在分时多任务操作系统中一个最根本、最核心的问题:在多个可运行的进程(或线程)之间,如何以及何时分配有限的CPU时间,以达到系统设定的目标。

这些目标通常是相互冲突的,调度器的设计就是在它们之间做出权衡和取舍:

  1. 公平性(Fairness):确保每个普通进程都能获得合理的CPU时间份额,防止任何进程被“饿死”。
  2. 吞吐量(Throughput):最大化系统在单位时间内完成的总工作量。这通常有利于长时间运行的计算密集型任务。
  3. 响应性(Responsiveness):最小化交互式任务(如GUI应用、文本编辑器)的响应延迟,让用户感觉系统“流畅”。这要求任务能被快速唤醒并执行。
  4. 实时性(Real-time):确保需要满足严格截止时间(deadline)的实时任务能够在规定时间内运行。

kernel/sched/core.c 扮演的角色是调度框架的中枢,它负责实现与具体调度策略无关的通用逻辑,如进程的唤醒、睡眠、上下文切换的触发,以及周期性的调度决策点(tick)。

它的发展经历了哪些重要的里程碑或版本迭代?

Linux调度器的发展史是一部不断追求更高性能、更好公平性和更低延迟的优化史。

  • 早期调度器(O(N) Scheduler):非常简单,每次调度决策都需要遍历系统中所有的可运行进程,其复杂度与进程数成正比(O(N)),在进程数多时性能极差。
  • O(1) 调度器:在2.5/2.6内核中引入,是一个重大的进步。它使用基于优先级的运行队列,使得选择下一个要运行的进程的时间复杂度变为常数(O(1))。但它在判断交互式任务方面过于复杂和启发式,公平性也不理想。
  • 完全公平调度器(Completely Fair Scheduler, CFS):由Ingo Molnar开发,在2.6.23内核中引入,是革命性的里程碑。CFS彻底抛弃了复杂的启发式算法,转而追求一种理想化的、极其简单的“公平”模型:它模拟一个“理想的多任务CPU”,在这个CPU上,每个进程都以同等的速度并行执行。CFS的核心是维护一个以“虚拟运行时间”(vruntime)为key的红黑树,每次都选择树中vruntime最小的进程来运行。这使得它在提供出色公平性的同时,也保持了高效的进程选择性能(O(log N))。
  • 实时调度策略的增强:内核始终支持SCHED_FIFOSCHED_RR这两种POSIX标准的实时调度策略。后来又引入了SCHED_DEADLINE,这是一种基于最早截止时间优先(EDF)算法的调度策略,为硬实时任务提供了更强的可预测性保证。

目前该技术的社区活跃度和主流应用情况如何?

进程调度器是Linux内核中最核心、最活跃、被研究和优化得最深入的子系统之一。

  • 主流应用:它是所有Linux系统的脉搏。从Android手机(对响应性要求极高)到大型服务器(对吞吐量要求高),再到嵌入式实时系统(对可预测性要求高),Linux调度器通过其模块化的调度类(Scheduling Classes)框架和可调参数,适应着各种截然不同的工作负载。CFS是目前所有通用Linux系统(桌面、服务器)的默认调度器。

核心原理与设计

它的核心工作原理是什么?

现代Linux调度器是一个基于**调度类(Scheduling Classes)**的模块化框架。core.c 负责顶层协调,而具体的调度决策则委托给不同的调度类。

  1. 运行队列(Runqueue):每个CPU核心都有一个自己的运行队列(struct rq)。这是一个核心数据结构,包含了该CPU上所有可运行的进程,以及与调度相关的各种状态。

  2. 调度类(Scheduling Classes):调度器按优先级从高到低分为几个类,如:

    • StopTask:最高优先级,用于停止CPU等内部任务。
    • Deadline (SCHED_DEADLINE)
    • Real-Time (SCHED_FIFO, SCHED_RR)
    • Fair (SCHED_NORMAL, SCHED_BATCH,即CFS)
    • IdleTask:最低优先级,当没有其他任务可运行时执行。
  3. 核心调度函数 schedule()

    • 当内核需要在当前进程上进行重新调度时(例如,当前进程阻塞、时间片用完或被更高优先级的任务抢占),会调用 schedule()
    • schedule() 的核心逻辑是调用 pick_next_task() 来选择下一个要运行的进程。
    • pick_next_task()按优先级顺序遍历各个调度类。它会询问Deadline类:“你有可运行的进程吗?”,如果有,就选择它;如果没有,就去问Real-Time类,以此类推。
    • CFS(Fair类)是普通进程的归宿。只要系统中没有更高优先级的实时任务,调度决策就会落到CFS手中。
  4. CFS的核心原理

    • 虚拟运行时间 (vruntime):CFS为每个进程维护一个vruntime。进程运行得越久,它的vruntime增长得越快。被赋予更高nice值的进程,其vruntime增长得更慢。
    • 红黑树:每个CPU的CFS运行队列 (cfs_rq) 内部是一棵红黑树,所有可运行的普通进程都按其vruntime值在这棵树中排序。
    • 决策:当CFS需要选择下一个进程时,它总是选择红黑树最左边的节点,因为这个节点的vruntime最小,意味着它是被“亏欠”CPU时间最多的进程。
  5. 上下文切换(Context Switch)

    • pick_next_task() 选出下一个要运行的进程(next)后,schedule() 会调用 context_switch()
    • 这个函数负责执行底层的、与CPU架构相关的操作:保存当前进程(prev)的CPU寄存器状态到其内核栈,然后加载next进程的寄存器状态到CPU中。执行完这条指令后,CPU就开始执行next进程的代码了。

它的主要优势体现在哪些方面?

  • 模块化与可扩展性:调度类框架使得添加新的调度策略变得相对容易。
  • CFS的公平性与简洁性:CFS用一个简单而优雅的模型取代了过去复杂的启发式算法,提供了出色的公平性和对交互式任务的良好支持。
  • per-CPU 运行队列:避免了全局锁,提供了极佳的多核扩展性。
  • 可调优性:通过sysctl和cgroup,提供了丰富的参数供系统管理员根据具体负载进行调优。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 通用性与特定负载的矛盾:作为一个通用调度器,它试图在所有目标(吞吐量、延迟、公平性)之间取得平衡,这意味着它在任何单一目标上可能都不是“最优”的。例如,一个专为HPC设计的调度器可能会牺牲公平性来换取极致的吞-吐量。
  • 唤醒延迟:当一个任务在一个CPU上被唤醒,但它需要的数据却在另一个CPU的缓存中时,如何高效地进行任务迁移以获得最佳性能(缓存亲和性 vs 唤醒延迟),是一个非常复杂且持续在优化的难题。
  • 实时性能:虽然Linux提供了实时调度策略,但要实现真正的硬实时,还需要配合PREEMPT_RT补丁集,该补丁集对内核中大量的自旋锁等进行了修改,以确保内核的可抢占性。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

Linux调度器是所有基于Linux的系统的默认和首选解决方案。不同的调度策略适用于不同的场景:

  • SCHED_NORMAL (CFS):桌面系统、Web服务器、数据库等绝大多数通用计算负载。
  • SCHED_BATCH (CFS):用于非交互式的、计算密集型的批处理任务。它告诉CFS可以稍微牺牲一点延迟来换取更好的缓存利用率和吞吐量。
  • SCHED_FIFO / SCHED_RR:软实时应用,如音频/视频处理、工业控制中对延迟有要求但能容忍微小抖动的任务。
  • SCHED_DEADLINE:硬实时应用,如高频交易、机器人控制等,这些任务的正确性不仅取决于计算结果,还取决于计算完成的时间。

是否有不推荐使用该技术的场景?为什么?

对于绝大多数场景,Linux调度器都是适用的。只有在一些极端专用的领域,可能会选择其他操作系统:

  • 极端的安全关键和硬实时系统:例如航空电子、医疗生命支持系统。这些领域通常使用经过严格形式化验证的、更简单的实时操作系统(RTOS)。虽然PREEMPT_RT Linux正在越来越多地进入这些领域,但传统的RTOS仍占有一席之地。

对比分析

请将其 与 其他相似技术 进行详细对比。

最有趣的对比是Linux调度器(以CFS为代表)与其他操作系统或调度思想的比较。

特性 Linux CFS 传统优先级调度器 (如 O(1), Windows NT) 实时操作系统 (RTOS) 调度器
核心思想 公平性。模拟理想并行执行,基于“虚拟运行时间”。 优先级。严格按照离散的优先级级别进行调度。 可预测性。通常是严格的、基于优先级的抢占式调度。
进程选择 O(log N),从红黑树中选择vruntime最小的。 O(1),从优先级位图中找到最高优先级的队列。 O(1)O(N),取决于实现。
公平性 非常好,是其核心设计目标。 较差。低优先级任务可能被持续饿死。 不保证。公平性不是其主要目标。
对交互任务的处理 内生地好。交互任务因睡眠时间长,vruntime增长慢,被唤醒后能很快被调度。 启发式。需要复杂的算法(如睡眠时间、CPU使用率)来“猜测”谁是交互任务并提升其优先级。 不区分。只看优先级。
实时支持 通过独立的、更高优先级的调度类来实现。 通常也是通过高优先级来实现。 核心设计目标。整个系统的设计都为保证可预测性服务。
复杂性 模型简单,实现中等。 模型复杂(启发式),实现复杂。 模型简单,实现相对简单。

include/linux/sched/task.h

task_lock task_unlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 保护 ->fs、->files、->mm、->group_info、->comm、keyring
* 订阅,并与 wait4() 同步。也用于 procfs。同时
* 固定任务 io_context 的最终释放。同时保护 ->cpuset 和
* ->cgroup.subsys[]。以及 ->vfork_done 和 ->sysvshm.shm_clist。
*
* 在 read_lock(&tasklist_lock) 内外均嵌套。
* 它不能与 write_lock_irq(&tasklist_lock) 嵌套,
* 无论是在内部还是外部。
*/
static inline void task_lock(struct task_struct *p)
{
spin_lock(&p->alloc_lock);
}

static inline void task_unlock(struct task_struct *p)
{
spin_unlock(&p->alloc_lock);
}

DEFINE_GUARD(task_lock, struct task_struct *, task_lock(_T), task_unlock(_T))

include/uapi/linux/sched.h

调度策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*
* 注释:调度策略
*/
/*
* SCHED_NORMAL (普通调度策略):
* 这是Linux默认的分时调度策略,由完全公平调度器(CFS)实现。
* 它旨在为系统中的所有普通任务提供公平的CPU时间分配,追求高吞g吐量和交互性。
* 它的优先级由nice值(-20到+19)动态调整。
*/
#define SCHED_NORMAL 0

/*
* SCHED_FIFO (先入先出调度策略):
* 这是一种静态优先级的实时调度策略。它不使用时间片。
* 一个SCHED_FIFO任务会一直运行,直到它主动放弃CPU(如睡眠、等待I/O)、
* 被更高优先级的实时任务抢占、或者它自己调用sched_yield()。
* 相同优先级的任务遵循先到先服务的原则。
*/
#define SCHED_FIFO 1

/*
* SCHED_RR (轮转调度策略):
* 这也是一种静态优先级的实时调度策略,是SCHED_FIFO的变体。
* 它为每个任务分配一个固定的时间片。当一个任务的时间片用完,
* 但它还未完成时,它会被放到其所在优先级队列的末尾,让同优先级的
* 其他任务有机会运行。这为相同优先级的实时任务提供了公平性。
*/
#define SCHED_RR 2

/*
* SCHED_BATCH (批处理调度策略):
* 由CFS调度器实现,是SCHED_NORMAL的变体。
* 它告诉调度器,这个任务是一个非交互式的、CPU密集型的批处理任务。
* 调度器在决策时,会倾向于让它长时间运行而不被打断,以提高CPU缓存利用率,
* 但会降低其抢占其他任务的倾向,从而不影响系统交互性。
*/
#define SCHED_BATCH 3

/*
* SCHED_ISO (等时调度策略):
* 注释:已预留但尚未实现。
* 这是一个为未来的实时应用(如专业音频/视频处理)预留的策略,
* 旨在提供有保障的、周期性的CPU时间分配,但目前在主线内核中并未实现。
*/
#define SCHED_IDLE 5

/*
* SCHED_IDLE (空闲优先级调度策略):
* 由CFS调度器实现。被设置为此策略的任务,其优先级甚至低于nice值为+19的
* 普通任务。只有在系统绝对没有任何其他事情可做时,这些任务才会运行。
* 它适用于执行最低优先级后台工作的场景,如日志清理、索引建立等。
*/
#define SCHED_DEADLINE 6

/*
* SCHED_DEADLINE (截止时间调度策略):
* 这是一种基于参数(runtime, period, deadline)的实时调度策略。
* 它允许任务声明“我需要在每个period周期内,获得runtime的执行时间,
* 并且必须在deadline之前完成”。调度器会基于最早截止时间优先(EDF)
* 算法,通过接纳控制来判断是否能满足所有任务的约束。
*/
#define SCHED_EXT 7
/*
* SCHED_EXT (可扩展调度器策略):
* 这提供了一个接口,允许加载外部的、第三方开发的调度器模块,
* 而无需修改内核主线代码。这是一个用于调度器研究和定制的框架。
*/

include/linux/sched.h

cond_resched 条件调度

1
2
3
4
5
6
7
8
9
static inline int _cond_resched(void)
{
return 0;
}

#define cond_resched() ({ \
__might_resched(__FILE__, __LINE__, 0); \
_cond_resched(); \
})

kernel/sched/sched.h

global_rt_runtime global_rt_period

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* period over which we measure -rt task CPU usage in us.
* default: 1s
*/
int sysctl_sched_rt_period = 1000000;

/*
* part of the period that we allow rt tasks to run in us.
* default: 0.95s
*/
int sysctl_sched_rt_runtime = 950000;

static inline u64 global_rt_period(void)
{
return (u64)sysctl_sched_rt_period * NSEC_PER_USEC;
}

static inline u64 global_rt_runtime(void)
{
if (sysctl_sched_rt_runtime < 0)
return RUNTIME_INF;

return (u64)sysctl_sched_rt_runtime * NSEC_PER_USEC;
}

raw_spin_rq_lock 运行队列的原子自旋锁

这段代码和注释详细描述了调度器中任务状态的序列化规则,以及如何通过锁机制确保多线程环境下的线程安全性。以下是对主要内容的分段解析:


锁的顺序

注释中首先定义了锁的获取顺序,以避免死锁问题:

  1. p->pi_lock 是任务的优先级继承锁,用于保护任务的优先级相关数据。
  2. rq->lock 是调度队列的锁,用于保护调度队列的状态。
  3. hrtimer_cpu_base->lock 是高精度定时器的锁,通常用于带宽控制。

此外,当涉及多个调度队列时,锁的顺序必须遵循 rq1->lockrq2->lock 之前获取的规则,其中 rq1 < rq2


常规状态(Regular State)

在正常的调度状态下,rq->lock 是主要的同步机制:

  • 调度器的核心函数 __schedule() 会获取当前 CPU 的 rq->lock,以确保安全地操作本地调度队列。它可能会从运行队列中移除任务,并选择下一个最合适的任务运行。
  • 任务的入队操作(enqueue)也需要在 rq->lock 下进行,即使操作来自其他 CPU。
  • 对于跨 NUMA 域的任务唤醒,可能会使用中断(IPI)将任务的入队操作转移到本地 CPU,以减少运行队列状态的频繁切换。

任务唤醒(wakeups)尤其是涉及迁移的唤醒操作非常复杂,为了避免同时获取两个 rq->lock,需要特殊的处理。


特殊状态(Special State)

对于系统调用或外部操作,会使用 task_rq_lock() 同时获取 p->pi_lockrq->lock。这确保了在持有任一锁时,任务的状态是稳定的。例如:

  • 修改任务的 CPU 亲和性(sched_setaffinity)。
  • 调整任务的优先级(set_user_nice)。
  • 更改调度策略(__sched_setscheduler)。
  • 更新 NUMA 节点或任务组。

这些操作需要对任务的状态进行一致性保护,因此需要同时持有两个锁。


任务状态字段

  1. p->state
    任务的状态(TASK_*)可以通过无锁方式修改,例如 set_current_state()try_to_wake_up()。其中 try_to_wake_up() 使用 p->pi_lock 来防止并发问题。

  2. p->on_rq
    表示任务是否在运行队列中:

    • 0:不在运行队列中。
    • 1:任务已入队。
    • 2:任务正在迁移(TASK_ON_RQ_MIGRATING)。此状态用于在不同时持有两个 rq->lock 的情况下进行迁移。

    特殊情况下,任务可能在运行队列中但不可运行(例如 p->se.sched_delayedtrue)。

  3. p->on_cpu
    表示任务是否正在运行:

    • 1:任务正在其 CPU 上运行。
    • 0:任务未运行。

    这一字段在任务被调度入 CPU 前设置,在任务被调度出 CPU 后清除。

  4. task_cpu(p)
    任务的 CPU 分配由 set_task_cpu() 修改,但有以下规则:

    • 不应对阻塞任务调用 set_task_cpu()
    • 唤醒任务时在 p->pi_lock 下调用。
    • 迁移任务时在 rq->lock 或双队列锁(double_rq_lock())下调用。

函数 raw_spin_rq_lock_nested

该函数实现了对调度队列锁的嵌套加锁逻辑:

  • 如果调度核心功能被禁用,直接对 rq->__lock 加锁。
  • 如果调度核心功能启用,进入循环尝试获取锁,确保锁指针在加锁期间未发生变化。

通过这种设计,函数能够在不同的调度模式下安全地获取锁,保护调度队列的状态。


总结

这段代码和注释详细描述了调度器中任务状态的管理规则,以及如何通过锁机制确保多线程环境下的安全性和一致性。它涵盖了正常调度状态、特殊操作以及任务状态字段的修改规则,是调度器实现中关键的同步逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void raw_spin_rq_lock_nested(struct rq *rq, int subclass)
{
raw_spinlock_t *lock;

/* 匹配 __sched_core_enable() 中的 synchronize_rcu() */
preempt_disable();
if (sched_core_disabled()) {
//调度队列的基础锁(rq->__lock)进行加锁
raw_spin_lock_nested(&rq->__lock, subclass);
/* preempt_count *MUST* be > 1 */
preempt_enable_no_resched(); //重新启用抢占
return;
}

for (;;) {
lock = __rq_lockp(rq); //获取当前调度队列的锁指针
raw_spin_lock_nested(lock, subclass);
//检查锁指针是否在加锁过程中发生了变化(通过再次调用 __rq_lockp(rq) 比较)。如果锁指针未变,说明锁定成功,退出循环并重新启用抢占
if (likely(lock == __rq_lockp(rq))) {
/* preempt_count *MUST* be > 1 */
preempt_enable_no_resched();
return;
}
raw_spin_unlock(lock);
}
}

static inline void raw_spin_rq_lock(struct rq *rq)
{
raw_spin_rq_lock_nested(rq, 0);
}

rq::clock_update_flags bits

这段代码定义了运行队列(rq)的 clock_update_flags 字段的三个标志位(RQCF_REQ_SKIPRQCF_ACT_SKIPRQCF_UPDATED),它们用于优化和调试调度器中运行队列的时钟更新逻辑。这些标志位通过位操作进行管理,帮助调度器高效地处理任务调度中的时钟更新。

标志位说明

  1. RQCF_REQ_SKIP (0x01):

    • 表示请求在下一次调用 __schedule() 时跳过运行队列时钟的更新。
    • 这是一个优化标志,用于避免对相邻运行队列的时钟进行不必要的更新,从而减少开销。
  2. RQCF_ACT_SKIP (0x02):

    • __schedule() 内部设置,表示当前正在跳过运行队列时钟的更新。
    • 当此标志生效时,对 update_rq_clock() 的调用将被忽略。
  3. RQCF_UPDATED (0x04):

    • 这是一个调试标志,用于指示自上次固定运行队列锁(rq::lock)以来,是否调用过 update_rq_clock()
    • 该标志帮助开发者检查时钟更新是否按预期发生。

标志位的使用

  • __schedule() 中,clock_update_flags 的值可能会左移一位(通过位移操作将 RQCF_REQ_SKIP 提升为 RQCF_ACT_SKIP),以便快速切换到跳过时钟更新的状态。

  • 为了检查 RQCF_UPDATED 是否被设置,必须使用以下条件:

    1
    if (rq->clock_update_flags >= RQCF_UPDATED)

    这是因为标志位可能被左移,但不会超过一个位置。

  • 在调用 rq_unpin_lock() 时,clock_update_flags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
* rq::clock_update_flags bits
*
* %RQCF_REQ_SKIP - will request skipping of clock update on the next
* call to __schedule(). This is an optimisation to avoid
* neighbouring rq clock updates.
*
* %RQCF_ACT_SKIP - is set from inside of __schedule() when skipping is
* in effect and calls to update_rq_clock() are being ignored.
*
* %RQCF_UPDATED - is a debug flag that indicates whether a call has been
* made to update_rq_clock() since the last time rq::lock was pinned.
*
* If inside of __schedule(), clock_update_flags will have been
* shifted left (a left shift is a cheap operation for the fast path
* to promote %RQCF_REQ_SKIP to %RQCF_ACT_SKIP), so you must use,
*
* if (rq-clock_update_flags >= RQCF_UPDATED)
*
* to check if %RQCF_UPDATED is set. It'll never be shifted more than
* one position though, because the next rq_unpin_lock() will shift it
* back.
*/
#define RQCF_REQ_SKIP 0x01
#define RQCF_ACT_SKIP 0x02
#define RQCF_UPDATED 0x04

rq_pin_lock 将运行队列锁定到当前 CPU

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* Lockdep 注释,避免意外解锁;这就像一个粘性/连续的 lockdep_assert_held()。 *
* 这可以避免对 'struct rq *rq'(实际上是调度器中的所有代码)具有访问权限的代码在没有同时拥有(在栈上)'struct rq_flags rf' 的副本时意外解锁 rq。 *
* 另请参见 Documentation/locking/lockdep-design.rst.
*/
static inline void rq_pin_lock(struct rq *rq, struct rq_flags *rf)
{
rf->cookie = lockdep_pin_lock(__rq_lockp(rq));
/* 清除运行队列的时钟更新标志,保留特定的标志位 */
rq->clock_update_flags &= (RQCF_REQ_SKIP|RQCF_ACT_SKIP);
rf->clock_update_flags = 0;
#ifdef CONFIG_SMP
WARN_ON_ONCE(rq->balance_callback && rq->balance_callback != &balance_push_callback);
#endif
}

task_rq_lock task_rq_unlock 任务运行队列锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
DEFINE_LOCK_GUARD_1(task_rq_lock, struct task_struct,
_T->rq = task_rq_lock(_T->lock, &_T->rf),
task_rq_unlock(_T->rq, _T->lock, &_T->rf),
struct rq *rq; struct rq_flags rf)

#define cpu_rq(cpu) (&per_cpu(runqueues, (cpu)))
#define task_rq(p) cpu_rq(task_cpu(p))

static inline void rq_unpin_lock(struct rq *rq, struct rq_flags *rf)
{
if (rq->clock_update_flags > RQCF_ACT_SKIP)
rf->clock_update_flags = RQCF_UPDATED;

scx_rq_clock_invalidate(rq);
lockdep_unpin_lock(__rq_lockp(rq), rf->cookie);
}

static inline void
task_rq_unlock(struct rq *rq, struct task_struct *p, struct rq_flags *rf)
__releases(rq->lock)
__releases(p->pi_lock)
{
rq_unpin_lock(rq, rf);
raw_spin_rq_unlock(rq);
raw_spin_unlock_irqrestore(&p->pi_lock, rf->flags);
}

/*
* task_rq_lock - lock p->pi_lock and lock the rq @p resides on.
*/
struct rq *task_rq_lock(struct task_struct *p, struct rq_flags *rf)
__acquires(p->pi_lock)
__acquires(rq->lock)
{
struct rq *rq;

for (;;) {
/* 锁定任务的优先级继承锁(pi_lock */
raw_spin_lock_irqsave(&p->pi_lock, rf->flags);
/* 获取任务的运行队列 */
rq = task_rq(p);
/* 锁定运行队列的锁 */
raw_spin_rq_lock(rq);
/*
* move_queued_task() task_rq_lock()
*
* ACQUIRE (rq->lock)
* [S] ->on_rq = MIGRATING [L] rq = task_rq()
* WMB (__set_task_cpu()) ACQUIRE (rq->lock);
* [S] ->cpu = new_cpu [L] task_rq()
* [L] ->on_rq
* RELEASE (rq->lock)
*
* If we observe the old CPU in task_rq_lock(), the acquire of
* the old rq->lock will fully serialize against the stores.
*
* If we observe the new CPU in task_rq_lock(), the address
* dependency headed by '[L] rq = task_rq()' and the acquire
* will pair with the WMB to ensure we then also see migrating.
*/
/* 确保任务的运行队列没有在锁定过程中发生变化 确保任务没有处于迁移状态*/
if (likely(rq == task_rq(p) && !task_on_rq_migrating(p))) {
/* 调用 rq_pin_lock 将运行队列锁定到当前 CPU */
rq_pin_lock(rq, rf);
return rq;
}
raw_spin_rq_unlock(rq);
raw_spin_unlock_irqrestore(&p->pi_lock, rf->flags);
/* 通过 cpu_relax 进入一个短暂的等待循环,直到任务迁移完成。随后重新尝试锁定。 */
while (unlikely(task_on_rq_migrating(p)))
cpu_relax();
}
}

update_rq_clock_task 更新运行队列的时钟任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* RQ-clock updating methods:
*/

static void update_rq_clock_task(struct rq *rq, s64 delta)
{
/*
* In theory, the compile should just see 0 here, and optimize out the call
* to sched_rt_avg_update. But I don't trust it...
*/
s64 __maybe_unused steal = 0, irq_delta = 0;

rq->clock_task += delta;

update_rq_clock_pelt(rq, delta);
}

update_rq_clock 更新运行队列(rq)的时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void update_rq_clock(struct rq *rq)
{
s64 delta;
u64 clock;

lockdep_assert_rq_held(rq);

if (rq->clock_update_flags & RQCF_ACT_SKIP)
return;
/* 启用了调度特性 WARN_DOUBLE_CLOCK,函数会检查 RQCF_UPDATED 标志是否已被设置。
如果标志已设置,说明时钟可能被重复更新,函数会触发一次警告 */
if (sched_feat(WARN_DOUBLE_CLOCK))
WARN_ON_ONCE(rq->clock_update_flags & RQCF_UPDATED);
rq->clock_update_flags |= RQCF_UPDATED;
/* 调用 sched_clock_cpu(cpu_of(rq)) 获取当前 CPU 的调度时钟值 */
clock = sched_clock_cpu(cpu_of(rq));
/* 更新运行队列的时钟状态 */
scx_rq_clock_update(rq, clock);
/* 计算时间增量 */
delta = clock - rq->clock;
/* 如果 delta 为负值,说明时钟未前进,函数直接返回 */
if (delta < 0)
return;
rq->clock += delta;
/* 通知与运行队列相关的任务更新其时钟信息。这通常用于调整任务的调度参数 */
update_rq_clock_task(rq, delta);
}

task_rq 获取任务所在的运行队列

1
2
#define cpu_rq(cpu)		(&per_cpu(runqueues, (cpu)))
#define task_rq(p) cpu_rq(task_cpu(p))

DEFINE_SCHED_CLASS 定义调度类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 用于定义 sched_class 实例的帮助程序;每个 API 都放置在单独的
* 部分,该部分由链接器脚本排序:
*
* include/asm-generic/vmlinux.lds.h
*
* *小心* 他们是按*相反* 的顺序排列的!!
*
* 还要对实例(而不是类型)强制对齐,以保证布局。
*/
#define DEFINE_SCHED_CLASS(name) \
const struct sched_class name##_sched_class \
__aligned(__alignof__(struct sched_class)) \
__section("__" #name "_sched_class")

put_prev_task 将前一个任务从运行队列中移除

1
2
3
4
5
static inline void put_prev_task(struct rq *rq, struct task_struct *prev)
{
WARN_ON_ONCE(rq->donor != prev);
prev->sched_class->put_prev_task(rq, prev, NULL);
}

set_next_task 设置下一个任务

1
2
3
4
static inline void set_next_task(struct rq *rq, struct task_struct *next)
{
next->sched_class->set_next_task(rq, next, false);
}

rt_effective_prio 计算实时任务的有效优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifdef CONFIG_RT_MUTEXES
// 计算实时任务的有效优先级, 返回值为当前任务的最高优先级
// linux优先级越低,优先级越高
// 例如,优先级 0 是最高的实时优先级,而优先级 139 是最低的实时优先级
static inline int __rt_effective_prio(struct task_struct *pi_task, int prio)
{
if (pi_task)
prio = min(prio, pi_task->prio);

return prio;
}

static inline int rt_effective_prio(struct task_struct *p, int prio)
{
// 获取实时任务的优先级继承任务(PI task)
// 该任务是当前任务的最高优先级任务
struct task_struct *pi_task = rt_mutex_get_top_task(p);

return __rt_effective_prio(pi_task, prio); // 计算实时任务的有效优先级
}
#endif

entity_is_task 判断实体是否为任务

1
#define entity_is_task(se)	1

task_on_rq_queued 判断任务是否在运行队列中排队

1
2
3
4
static inline int task_on_rq_queued(struct task_struct *p)
{
return READ_ONCE(p->on_rq) == TASK_ON_RQ_QUEUED;
}

task_current_donor 判断任务是否为当前的捐赠者

1
2
3
4
5
6
7
8
9
10
/*
* p 是当前的调度上下文吗?
*
* 请注意,如果 rq->curr == rq->donor == p,
* 它可能同时是当前的执行上下文。
*/
static inline int task_current_donor(struct rq *rq, struct task_struct *p)
{
return rq->donor == p;
}

task_has_idle_policy 判断任务是否具有空闲策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* Single value that denotes runtime == period, ie unlimited time.
*/
#define RUNTIME_INF ((u64)~0ULL)

static inline int idle_policy(int policy)
{
return policy == SCHED_IDLE;
}

static inline int task_has_idle_policy(struct task_struct *p)
{
return idle_policy(p->policy);
}

task_on_cpu 判断任务是否在 CPU 上运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* Is p the current execution context?
*/
static inline int task_current(struct rq *rq, struct task_struct *p)
{
return rq->curr == p;
}

static inline int task_on_cpu(struct rq *rq, struct task_struct *p)
{
#ifdef CONFIG_SMP
return p->on_cpu;
#else
return task_current(rq, p);
#endif
}

include/linux/sched/mm.h

mmgrab_lazy_tlb 懒惰的 转译后备缓冲区(Translation Lookaside Buffer)内存管理单元(MMU)引用计数

  • 该函数适用于需要长时间或不确定时间内保持对 mm_struct 的引用的场景。
  • 例如,当某些内核子系统需要访问进程的内存管理信息时,可以调用 mmgrab 来确保 mm_struct 在操作期间不会被释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* mmgrab() - 将 &struct mm_struct 固定。
* @mm:mm_struct 的 &struct 到 pin。
*
* 确保@mm即使在拥有任务退出后也不会被释放。这并不能保证关联的地址空间以后仍然存在,并且在访问它之前必须使用 mmget_not_zero()。
*
* 这是将 @mm固定更长时间/无限期的首选方法。
*
* 使用 mmdrop() 释放 mmgrab() 获取的引用。
*
* 另请参阅 <Documentation/mm/active_mm.rst> 以深入了解 &mm_struct.mm_count 与 &mm_struct.mm_users。
*/
static inline void mmgrab(struct mm_struct *mm)
{
atomic_inc(&mm->mm_count);
}

/* Helpers for lazy TLB mm refcounting */
static inline void mmgrab_lazy_tlb(struct mm_struct *mm)
{
if (IS_ENABLED(CONFIG_MMU_LAZY_TLB_REFCOUNT))
mmgrab(mm);
}

mmdrop_lazy_tlb_sched Mmdrop 懒惰 TLB 调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static inline void mmdrop(struct mm_struct *mm)
{
/*
* atomic_dec_and_test() 隐含的完整屏障是
* 在存储到 rq->curr 后返回用户空间之前,
* membarrier 系统调用所需的。
*/
if (unlikely(atomic_dec_and_test(&mm->mm_count)))
__mmdrop(mm);
}

static inline void mmdrop_sched(struct mm_struct *mm)
{
mmdrop(mm);
}

static inline void mmdrop_lazy_tlb_sched(struct mm_struct *mm)
{
if (IS_ENABLED(CONFIG_MMU_LAZY_TLB_REFCOUNT))
mmdrop_sched(mm);
else
smp_mb(); /* see mmdrop_lazy_tlb() above */
}

include/uapi/linux/sched/types.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
* struct sched_attr - 扩展的调度参数数据结构。
*
* 需要这个结构体是因为,原始的struct sched_param在不引入与遗留应用程序
* (例如,在sched_getparam()中)的ABI问题的情况下,无法被修改。
*
* 然而,能够为任务指定比优先级更多的参数,可能对多种应用领域都很有用,
* 例如:多媒体、流媒体、自动化与控制,以及许多其他领域。
*
* 这个变体(sched_attr)允许定义额外的属性,以增强调度器对任务需求的了解。
*
* 调度类属性 (Scheduling Class Attributes)
* ===========================
*
* sched_attr属性的一个子集,用于指定调度策略和相关的POSIX属性:
*
* @size: 结构体的大小,用于向前/向后兼容性。
*
* @sched_policy: 任务的调度策略 (例如 SCHED_NORMAL, SCHED_FIFO)。
* @sched_nice: 任务的nice值 (用于SCHED_NORMAL/BATCH)。
* @sched_priority: 任务的静态优先级 (用于SCHED_FIFO/RR)。
*
* 某些更高级的调度特性,可以通过一个预定义的标志集合来控制:
*
* @sched_flags: 用于定制调度器行为的标志。
*
* 零星时间约束任务属性 (Sporadic Time-Constrained Task Attributes)
* =========================================
*
* sched_attr属性的一个子集,允许描述所谓的“零星时间约束任务”。
*
* 在这种模型中,一个任务由以下参数指定:
* - 激活周期 或 最小实例到达间隔时间;
* - 所有实例的最大(或平均,取决于实际调度策略)计算时间,也称为运行时;
* - 每个实例的截止时间(相对于实际激活时间)。
* 简而言之,一个周期性(零星)任务要求(至多)每隔一个周期,执行一些
* 特定的计算——通常称为一个实例。此外,每个实例的持续时间通常不超过
* “运行时”,并且必须在等于“实例激活时间 + 截止时间”的时刻之前完成。
*
* 这由sched_attr结构中的以下字段反映:
*
* @sched_deadline: 代表任务截止时间的纳秒值。
* @sched_runtime: 代表任务运行时的纳秒值。
* @sched_period: 代表任务周期的纳秒值。
*
* 基于这个任务模型,存在多种调度算法和策略,可用于确保所有任务
* 都能满足其时间约束。
*
* 截至目前,SCHED_DEADLINE策略(sched_dl调度类)是这个新接口
* 的唯一用户。更多关于该算法的信息可在调度类文件或文档中找到。
*
* 任务利用率属性 (Task Utilization Attributes)
* ===========================
*
* sched_attr属性的一个子集,允许指定一个任务的预期利用率。
* 这些属性允许告知调度器,它应该在哪个利用率边界内调度该任务。
* 这些边界是支持调度器在任务放置和频率选择上做出决策的宝贵提示。
*
* @sched_util_min: 代表最小利用率。
* @sched_util_max: 代表最大利用率。
*
* 利用率是一个在[0..SCHED_CAPACITY_SCALE]范围内的值。它代表了
* 一个任务在系统的最高容量CPU上以最大频率运行时所使用的CPU时间百分比。
* 例如,一个20%利用率的任务,是在最大频率下每10ms运行2ms的任务。
*
* 一个最小利用率值大于0的任务,更有可能被调度到一个容量足以满足
* 该指定值的CPU上。
* 一个最大利用率值小于1024的任务,更有可能被调度到一个容量不超过
* 该指定值的CPU上。
*
* 可以通过将属性设置为-1来重置任务的利用率边界。
*/
struct sched_attr {
/* 结构体大小,必须由用户空间填充。*/
__u32 size;

/* 调度策略,如SCHED_NORMAL, SCHED_FIFO, SCHED_RR, SCHED_DEADLINE等。*/
__u32 sched_policy;
/* 调度标志,如SCHED_FLAG_RESET_ON_FORK。*/
__u64 sched_flags;

/* 用于SCHED_NORMAL, SCHED_BATCH策略,范围从-20到19。*/
__s32 sched_nice;

/* 用于SCHED_FIFO, SCHED_RR策略,范围从1到99。*/
__u32 sched_priority;

/* 以下三个字段用于SCHED_DEADLINE策略。*/
__u64 sched_runtime;
__u64 sched_deadline;
__u64 sched_period;

/* 以下两个字段是给调度器的利用率提示。*/
__u32 sched_util_min;
__u32 sched_util_max;

};

kernel/sched/clock.c

sched_clock_cpu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
notrace unsigned long long __weak sched_clock(void)
{
return (unsigned long long)(jiffies - INITIAL_JIFFIES)
* (NSEC_PER_SEC / HZ);
}
EXPORT_SYMBOL_GPL(sched_clock);

notrace u64 sched_clock_cpu(int cpu)
{
if (!static_branch_likely(&sched_clock_running))
return 0;

return sched_clock();
}

sched_clock_init

1
2
3
4
5
6
7
void __init sched_clock_init(void)
{
static_branch_inc(&sched_clock_running);
local_irq_disable();
generic_sched_clock_init();
local_irq_enable();
}

kernel/sched/core.c

hrtick_rq_init 高分辨率定时器初始化

1
2
3
4
5
6
7
static void hrtick_rq_init(struct rq *rq)
{
#ifdef CONFIG_SMP
INIT_CSD(&rq->hrtick_csd, __hrtick_start, rq);
#endif
hrtimer_setup(&rq->hrtick_timer, hrtick, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);
}

set_load_weight 设置负载权重

  • 该函数用于设置任务的负载权重(load weight),根据任务的调度策略和优先级来计算负载权重。
  • 负载权重是调度器用来衡量任务对 CPU 资源需求的重要指标,直接影响任务的调度优先级。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# define scale_load(w)		(w)

/*
* nice级别是乘法效应的,每改变一个nice级别,就有大约10%的平滑变化。
* 也就是说,当一个CPU密集型任务从nice 0变到nice 1,它将比另一个
* 保持在nice 0的CPU密集型任务少获得约10%的CPU时间。
*
* “10%效应”是相对的、累积的:从“任何”nice级别,如果你提升1级,
* CPU使用率就减少约10%;如果你降低1级,CPU使用率就增加约10%。
* (为了达到这个效果,我们使用了一个1.25的乘数。如果一个任务增加了约10%,
* 另一个任务减少了约10%,那么它们之间的相对距离就是约25%。)
*/
/*
* 这是一个预计算的查找表,将nice值(-20到+19)映射到调度权重。
* 数组索引是通过 static_prio 计算得出的。
*/
const int sched_prio_to_weight[40] = {
/* nice -20 */ 88761, 71755, 56483, 46273, 36291,
/* nice -15 */ 29154, 23254, 18705, 14949, 11916,
/* nice -10 */ 9548, 7620, 6100, 4904, 3906,
/* nice -5 */ 3121, 2501, 1991, 1586, 1277,
/* nice 0 */ 1024, 820, 655, 526, 423, /* 1024是NICE_0_LOAD基准 */
/* nice 5 */ 335, 272, 215, 172, 137,
/* nice 10 */ 110, 87, 70, 56, 45,
/* nice 15 */ 36, 29, 23, 18, 15,
};

/*
* sched_prio_to_weight[]数组的反转值(2^32/x),已预先计算。
*
* 在权重不经常改变的情况下,我们可以使用预计算的反转值来加速算术运算,
* 通过将除法变为乘法。
*/
/*
* 这是一个预计算的查找表,存储每个权重对应的反转权重值,用于定点数乘法。
*/
const u32 sched_prio_to_wmult[40] = {
/* -20 */ 48388, 59856, 76040, 92818, 118348,
/* -15 */ 147320, 184698, 229616, 287308, 360437,
/* -10 */ 449829, 563644, 704093, 875809, 1099582,
/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326,
/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587,
/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126,
/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717,
/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};

/*
* 这是一个宏,用于对权重进行可能的缩放。在基础实现中,它是一个空操作。
*/
# define scale_load(w) (w)

/*
* 这个函数在一个任务的静态优先级改变后被调用,用于更新其负载权重。
* @p: 指向目标任务的task_struct。
* @update_load: 一个布尔值,指示是否需要同步更新任务的平均负载统计。
*/
void set_load_weight(struct task_struct *p, bool update_load)
{
/* 将任务的内核内部标准优先级(static_prio)转换为数组索引。
* MAX_RT_PRIO是100,static_prio范围是100-139,所以prio范围是0-39。*/
int prio = p->static_prio - MAX_RT_PRIO;
/* 声明一个临时的load_weight结构体,用于存放查询到的新权重。*/
struct load_weight lw;

/* 检查任务是否为SCHED_IDLE策略,它有特殊的、最低的权重。*/
if (task_has_idle_policy(p)) {
/* 为IDLE任务设置固定的、最低的权重和反转权重。*/
lw.weight = scale_load(WEIGHT_IDLEPRIO);
lw.inv_weight = WMULT_IDLEPRIO;
} else {
/* 对于普通任务,使用prio作为索引,从预计算的数组中查找权重和反转权重。*/
lw.weight = scale_load(sched_prio_to_weight[prio]);
lw.inv_weight = sched_prio_to_wmult[prio];
}

/*
* SCHED_OTHER任务在改变其权重时,必须更新它们的负载。
*/
/* 如果需要更新负载统计,并且任务所属的调度类实现了reweight_task回调。*/
if (update_load && p->sched_class->reweight_task)
/* 调用该回调函数,它会原子性地更新任务的权重并同步更新cfs_rq的平均负载。*/
p->sched_class->reweight_task(task_rq(p), p, &lw);
else
/* 否则,只简单地将新的权重结构体赋给任务的调度实体。*/
p->se.load = lw;
}

dequeue_task 任务在运行队列中 将其从队列中移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 只有在 DEQUEUE_SLEEP 时才返回假。
*/
inline bool dequeue_task(struct rq *rq, struct task_struct *p, int flags)
{
/* 启用了核心调度 */
// if (sched_core_enabled(rq))
// /* 执行核心调度相关的任务移除操作 */
// sched_core_dequeue(rq, p, flags);

if (!(flags & DEQUEUE_NOCLOCK))
update_rq_clock(rq);

if (!(flags & DEQUEUE_SAVE))
sched_info_dequeue(rq, p);

// psi_dequeue(p, flags);

/*
* 必须在 ->dequeue_task() 之前,因为 ->dequeue_task() 可能会 '失败' 并将任务标记为 ->sched_delayed。
*/
// uclamp_rq_dec(rq, p);
return p->sched_class->dequeue_task(rq, p, flags);
}

__sched_fork 为新创建的进程(task_struct)执行与调度器相关的初始化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
* 默认时间片为 100 毫秒(仅用于 SCHED_RR 任务)。
* Timeslice 在过期后会重新填充。
*/
#define RR_TIMESLICE (100 * HZ / 1000)
/* 定义实时调度策略(SCHED_RR,即 Round-Robin 调度)的时间片长度
时间片是指一个任务在被调度器切换之前可以连续运行的时间长度。
在 SCHED_RR 策略中,多个具有相同优先级的任务会按照时间片轮流执行*/
int sched_rr_timeslice = RR_TIMESLICE;

/*
* 为新fork的进程 p 执行与调度程序相关的设置。
* p 由 current frok。
*
* __sched_fork() 是基本设置,sched_init() 也使用它来初始化启动 CPU 的空闲任务。
*/
static void __sched_fork(unsigned long clone_flags, struct task_struct *p)
{
/* p->on_rq 和 p->se.on_rq 表示进程是否在运行队列中,初始化为 0,表示新进程尚未加入调度队列 */
p->on_rq = 0;
p->se.on_rq = 0;
//调度实体
p->se.exec_start = 0; //跟踪进程的执行时间
p->se.sum_exec_runtime = 0; //跟踪进程的总执行时间
p->se.prev_sum_exec_runtime = 0; //跟踪进程的上次执行时间
p->se.nr_migrations = 0; //迁移次数
p->se.vruntime = 0; //虚拟运行时间
p->se.vlag = 0; //虚拟运行时间标志
INIT_LIST_HEAD(&p->se.group_node);

/* 延迟任务不能位于 clone() 中。*/
WARN_ON_ONCE(p->se.sched_delayed);

init_dl_entity(&p->dl);

INIT_LIST_HEAD(&p->rt.run_list);
p->rt.timeout = 0;
p->rt.time_slice = sched_rr_timeslice;
p->rt.on_rq = 0;
p->rt.on_list = 0;

// init_numa_balancing(clone_flags, p); //NUMA 平衡和多核支持

// init_sched_mm_cid(p); //初始化调度器的内存上下文 ID,用于跟踪进程的内存管理上下文
}

sched_fork 为新创建的进程执行调度器相关的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
* fork()/clone()-时间设置:
*/
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
__sched_fork(clone_flags, p);
/*
* 将新任务的状态设置为 TASK_NEW,表示任务尚未准备好运行。
* 这确保任务不会被调度器运行,也不会因信号或其他事件被插入运行队列。
*/
p->__state = TASK_NEW;

/*
* 新任务的优先级被设置为当前任务(父任务)的正常优先级。
*/
p->prio = current->normal_prio;
/* 调用 uclamp_fork 函数进一步调整任务的优先级限制,确保优先级继承符合调度策略 */
// uclamp_fork(p);

/*
* sched_reset_on_fork 标志被设置,表示需要重置任务的调度策略和优先级
*/
if (unlikely(p->sched_reset_on_fork)) {
if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
p->policy = SCHED_NORMAL;
p->static_prio = NICE_TO_PRIO(0);
p->rt_priority = 0;
} else if (PRIO_TO_NICE(p->static_prio) < 0)
p->static_prio = NICE_TO_PRIO(0);

p->prio = p->normal_prio = p->static_prio;
set_load_weight(p, false);
p->se.custom_slice = 0;
p->se.slice = sysctl_sched_base_slice;

/*
* We don't need the reset flag anymore after the fork. It has
* fulfilled its duty:
*/
p->sched_reset_on_fork = 0;
}

/* 截止任务(dl_prio) */
if (dl_prio(p->prio))
return -EAGAIN;

// scx_pre_fork(p);

/* 实时任务(rt_prio) */
if (rt_prio(p->prio)) {
p->sched_class = &rt_sched_class;
// #ifdef CONFIG_SCHED_CLASS_EXT
// } else if (task_should_scx(p->policy)) {
// p->sched_class = &ext_sched_class;
// #endif
} else {
/* 其他任务使用公平调度类 */
p->sched_class = &fair_sched_class;
}

// init_entity_runnable_average(&p->se);

init_task_preempt_count(p);
return 0;
}

init_idle 空闲任务初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* init_idle - 为给定 CPU 设置空闲线程
* @idle:有问题的任务
* @cpu:空闲任务所属的 CPU
*
* 注意:此函数不设置空闲线程的 NEED_RESCHED 标志,以使启动更加健壮。
*/
void __init init_idle(struct task_struct *idle, int cpu)
{

struct rq *rq = cpu_rq(cpu);
unsigned long flags;
/* 用于保护与优先级继承(Priority Inheritance, PI)相关的数据结构 */
raw_spin_lock_irqsave(&idle->pi_lock, flags);
/* 对调度队列(rq,即 runqueue)进行加锁操作 */
raw_spin_rq_lock(rq);

idle->__state = TASK_RUNNING;
idle->se.exec_start = sched_clock(); //初始化调度实体的执行开始时间
/*
* 设置线程标志为内核线程(PF_KTHREAD)和禁止亲和性设置(PF_NO_SETAFFINITY)
* 在操作系统中,“亲和性”(Affinity)是指将特定的任务(线程或进程)绑定到特定的 CPU 核心上运行的能力。
* 通过设置亲和性,可以控制任务在哪些 CPU 核心上运行,从而优化性能或满足特定的调度需求。
*/
idle->flags |= PF_KTHREAD | PF_NO_SETAFFINITY;
kthread_set_per_cpu(idle, cpu); //将线程标记为属于特定 CPU 的内核线程

/*
* 使用 RCU(Read-Copy-Update)机制设置空闲线程的 CPU,避免锁依赖问题
*/
rcu_read_lock();
// __set_task_cpu(idle, cpu);
rcu_read_unlock();

//将空闲线程设置为运行队列的空闲任务
rq->idle = idle;
// rq_set_donor(rq, idle);
rcu_assign_pointer(rq->curr, idle);
idle->on_rq = TASK_ON_RQ_QUEUED; //表示该任务已经被加入调度队列

raw_spin_rq_unlock(rq);
raw_spin_unlock_irqrestore(&idle->pi_lock, flags);
/* #define init_idle_preempt_count(p, cpu) do { \
task_thread_info(p)->preempt_count = PREEMPT_DISABLED; \
} while (0)
*/
/*设置抢占计数 _ outside_ 自旋锁! */
init_idle_preempt_count(idle, cpu);

/*
*空闲任务有自己的简单调度类:
*/
idle->sched_class = &idle_sched_class;
}

sched_init 调度器初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
void __init sched_init(void)
{
unsigned long ptr = 0;
int i;

/* 确保 linker 没有搞砸 */
BUG_ON(!sched_class_above(&dl_sched_class, &rt_sched_class));
BUG_ON(!sched_class_above(&rt_sched_class, &fair_sched_class));
BUG_ON(!sched_class_above(&fair_sched_class, &idle_sched_class));
wait_bit_init();
//遍历所有可能的 CPU 并初始化运行队列
for_each_possible_cpu(i) {
struct rq *rq;
//运行队列是调度器的核心数据结构,用于跟踪每个 CPU 上的任务状态
rq = cpu_rq(i); //初始化运行队列(rq)
raw_spin_lock_init(&rq->__lock);
rq->nr_running = 0;
rq->calc_load_active = 0;
rq->calc_load_update = jiffies + LOAD_FREQ;
init_cfs_rq(&rq->cfs); //初始化完全公平调度器运行队列
init_rt_rq(&rq->rt); //初始化实时运行队列
init_dl_rq(&rq->dl); //初始化截止运行队列

hrtick_rq_init(rq); //初始化高分辨率定时器
atomic_set(&rq->nr_iowait, 0); //初始化 I/O 等待计数器
fair_server_init(rq); //初始化公平服务器

zalloc_cpumask_var_node(&rq->scratch_mask, GFP_KERNEL, cpu_to_node(i));//分配掩码清零
}
//设置初始任务的负载权重
/* MAX_PRIO - 20意味着nice=-20,最高优先级 */
set_load_weight(&init_task, false);
init_task.se.slice = sysctl_sched_base_slice, //设置时间片长度

/*
* 初始化空闲任务的 MMU 切换
*/
mmgrab_lazy_tlb(&init_mm);
// enter_lazy_tlb(&init_mm, current);

/*
* 空闲任务被包装成每个 CPU 的内核线程(kthread)。
* set_kthread_struct 函数确保当前任务的线程结构正确,如果失败会触发警告。
*/
WARN_ON(!set_kthread_struct(current));

/*
* 初始化空闲任务和调度器状态
* 从技术上讲,schedule() 不应该从这个线程中调用,但是它可能在它下面的某个地方,但是因为我们是空闲线程,所以当这个 runqueue 变为 “idle” 时,我们只需再次开始运行。
*/
__sched_fork(0, current);
/* 将init_task变成idle进程 */
init_idle(current, smp_processor_id());

calc_load_update = jiffies + LOAD_FREQ;
/* SMP中启用
多核系统需要额外的调度逻辑
在多核系统中,任务调度需要考虑多个 CPU 核心之间的负载均衡和任务迁移,而这些功能在单核系统中是没有意义的:

负载均衡:多核系统中,任务需要在多个核心之间分配,以确保所有核心的负载尽可能均衡。
任务迁移:多核系统中,任务可能需要从一个核心迁移到另一个核心,以优化性能或响应系统状态的变化。这种逻辑在单核系统中不需要,因为任务只能运行在唯一的核心上。 */
// init_sched_fair_class(); //初始化完全公平调度器类
// init_sched_ext_class(); //初始化扩展调度器类

// psi_init(); //初始化 PSI(压力指标)数据结构

// init_uclamp(); //初始化 uclamp(用户空间调度器)数据结构

// preempt_dynamic_init(); //初始化动态抢占数据结构

scheduler_running = 1; //设置调度器运行标志
}

sched_submit_work 提交任务的工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static inline void sched_submit_work(struct task_struct *tsk)
{
static DEFINE_WAIT_OVERRIDE_MAP(sched_map, LD_WAIT_CONFIG);
unsigned int task_flags;

/*
* 建立LD_WAIT_CONFIG上下文以确保不会调用任何代码
* 将使用阻塞原语 —— 这将导致递归。
*/
lock_map_acquire_try(&sched_map);

task_flags = tsk->flags;
/*
* 如果 worker 进入睡眠状态,通知并询问 workqueue 是否
* 想要唤醒一个任务以保持并发。
*/
if (task_flags & PF_WQ_WORKER)
wq_worker_sleeping(tsk);
else if (task_flags & PF_IO_WORKER)
// io_wq_worker_sleeping(tsk);

/*
* 检查当前任务是否处于实时锁等待状态(TASK_RTLOCK_WAIT)。
* 如果任务处于该状态,发出警告,因为实时锁和读写锁不应触发阻塞请求,这可能导致死锁
*/
WARN_ON_ONCE(current->__state & TASK_RTLOCK_WAIT);

/*
* 如果我们要进入睡眠状态,并且已插入 IO 队列,
* 请务必提交它以避免死锁。
*/
blk_flush_plug(tsk->plug, true);

lock_map_release(&sched_map);
}

pick_next_task 选择下一个任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/*
* 选择最高优先级的任务:
*/
static inline struct task_struct *
__pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
const struct sched_class *class;
struct task_struct *p;

rq->dl_server = NULL;

/* 启用了 SCX(BPF 调度器扩展) */
if (scx_enabled())
goto restart;

/*
* 优化:我们知道,如果所有任务都在 fair 类中,我们可以
* 直接调用该函数,但前提是@prev任务不是
* 更高的调度类,否则那些会丢失
* 从其他 CPU 中提取更多工作的机会。
*/
/* 所有任务都属于公平调度类(fair_sched_class) */
if (likely(!sched_class_above(prev->sched_class, &fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_queued)) {

p = pick_next_task_fair(rq, prev, rf);
/* 返回 RETRY_TASK,表示需要重新评估任务选择逻辑 */
if (unlikely(p == RETRY_TASK))
goto restart;

/* 假设下一个优先类是 idle_sched_class */
if (!p) {
/* 没有找到任务,则选择空闲任务 */
p = pick_task_idle(rq);
put_prev_set_next_task(rq, prev, p);
}

return p;
}

restart:
/* 处理运行队列的负载均衡
CONFIG_SMP 才需要 */
prev_balance(rq, prev, rf);

/* 遍历所有活动的调度类(sched_class),尝试从每个调度类中选择任务 */
for_each_active_class(class) {
if (class->pick_next_task) { //fair
p = class->pick_next_task(rq, prev);
if (p)
return p;
} else {
p = class->pick_task(rq);
if (p) {
put_prev_set_next_task(rq, prev, p);
return p;
}
}
}

/* 如果没有找到任务,触发 BUG(),因为空闲调度类(idle_sched_class)应该始终有一个可运行的任务 */
BUG(); /* The idle class should always have a runnable task. */
}

static struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
return __pick_next_task(rq, prev, rf);
}

prepare_lock_switch 准备切换任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* prepare_task_switch - 准备切换任务
* @rq:准备切换的 runqueue
* @prev:当前正在切换的任务
* @next:我们将要切换到的任务。
*
* 这是在 rq lock hold 和 interrupt off 的情况下调用的。它必须
* 与上下文后的后续finish_task_switch配对
*开关。
*
* prepare_task_switch 设置锁定并调用特定于架构的
*钩。
*/
static inline void
prepare_task_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
// kcov_prepare_switch(prev);
// sched_info_switch(rq, prev, next);
// perf_event_task_sched_out(prev, next);
//要求禁用抢占
rseq_preempt(prev);
// fire_sched_out_preempt_notifiers(prev, next);
// kmap_local_sched_out();
// prepare_task(next);
// prepare_arch_switch(next);
}


static inline void
prepare_lock_switch(struct rq *rq, struct task_struct *next, struct rq_flags *rf)
{
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler it's an obvious special-case), so we
* do an early lockdep release here:
*/
rq_unpin_lock(rq, rf);
spin_release(&__rq_lockp(rq)->dep_map, _THIS_IP_);
#ifdef CONFIG_DEBUG_SPINLOCK
/* this is a valid case when another task releases the spinlock */
rq_lockp(rq)->owner = next;
#endif
}

context_switch 上下文切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*
* context_switch - 切换到新的内存管理(MM)结构和新线程的寄存器状态。
*/
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
/* 为任务切换做准备工作。这会调用一些追踪(tracing)和RCU(读-复制-更新)相关的钩子函数,
* 通知内核的其他子系统,一个任务切换即将发生。*/
prepare_task_switch(rq, prev, next);

/*
* 对于半虚拟化(paravirt)环境,这个调用与switch_to中的一个退出操作相配合,
* 目的是将页表重载和后端切换合并成一个单一的hypercall(超级调用),以提高效率。
*/
arch_start_context_switch(prev);

/*
* 这里的注释描述了不同切换路径下的内存管理(MM)行为:
*
* 内核线程 -> 内核线程:使用懒惰TLB模式,只传递active_mm指针。
* 用户进程 -> 内核线程:使用懒惰TLB模式,并增加active_mm的引用计数。
*
* 内核线程 -> 用户进程:执行完整的MM切换,并在完成切换后减少懒惰TLB的引用计数。
* 用户进程 -> 用户进程:执行完整的MM切换。
*
* 如果context_switch()提供的内存屏障被修改,switch_mm_cid()函数也需要相应更新。
*/

/* 检查目标任务'next'是否拥有自己的地址空间(mm_struct)。*/
if (!next->mm) { // 如果!next->mm为真,说明'next'是一个内核线程。
/* 进入懒惰TLB模式。内核线程没有自己的地址空间,所以它会临时“借用”前一个任务的地址空间,
* 以避免昂贵的地址空间切换和TLB刷新。*/
// enter_lazy_tlb(prev->active_mm, next);

/* 让'next'任务的active_mm指针指向前一个任务'prev'的active_mm。*/
next->active_mm = prev->active_mm;
/* 检查前一个任务'prev'是否是用户进程(有自己的mm)。*/
if (prev->mm) // 如果是,说明我们是从一个用户进程切换过来的。
/* 增加'prev->active_mm'的引用计数,防止在'next'内核线程借用期间,这个地址空间被释放。*/
// mmgrab_lazy_tlb(prev->active_mm);
else
/* 如果'prev'也是一个内核线程,它自己也在借用,所以我们只需清空它的active_mm指针。*/
prev->active_mm = NULL;
} else { // 否则,说明'next'是一个用户进程,有自己的地址空间。
/* 为membarrier系统调用切换MM。这确保了在切换前后内存操作的可见性。*/
membarrier_switch_mm(rq, prev->active_mm, next->mm);
/*
* sys_membarrier()系统调用要求在设置rq->curr和返回用户空间之间有一个smp_mb()内存屏障。
* 下面的代码通过switch_mm()或者在'prev->active_mm == next->mm'情况下的
* finish_task_switch()中的mmdrop()来提供这个屏障。
*/
/*
* 这是核心的地址空间切换函数。它会把'next->mm'的页表基地址(如x86的CR3寄存器)加载到CPU中。
* 'irqs_off'后缀表明此操作在本地中断关闭的情况下进行,以防被中断打扰。
*/
switch_mm_irqs_off(prev->active_mm, next->mm, next);
/* 通知多代LRU页面置换算法,next->mm这个地址空间现在开始被使用了。*/
lru_gen_use_mm(next->mm);

/* 检查前一个任务'prev'是否是内核线程。*/
if (!prev->mm) { // 如果是,说明我们是从一个内核线程切换过来的。
/* 我们需要在finish_task_switch()中为'prev'借用的地址空间调用mmdrop_lazy_tlb()。
* 因此,先把这个借来的mm保存到运行队列的'prev_mm'字段中。*/
rq->prev_mm = prev->active_mm;
/* 清空'prev'内核线程的active_mm指针。*/
prev->active_mm = NULL;
}
}

/* switch_mm_cid()依赖于上面的内存屏障。
* 这个函数用于切换内存控制组(cgroup)的上下文ID,用于内存资源统计。*/
// switch_mm_cid(rq, prev, next);

/* 为锁切换做准备,主要用于锁依赖分析工具(lockdep)。*/
prepare_lock_switch(rq, next, rf);

/*
* 到这里,我们只切换寄存器状态和栈。
* 这是实际进行CPU状态切换的地方。它是一个宏,通常由汇编实现。
* 它会保存'prev'任务的寄存器到其栈上,然后从'next'任务的栈上加载它的寄存器。
* 当此宏返回时,代码已经是在'next'任务的上下文中执行了。
* 第三个参数'prev'是一个技巧,它使得'next'任务在恢复执行后,能拿到'prev'任务的指针。
*/
switch_to(prev, next, prev);
/* 这是一个编译器屏障。它告诉编译器不要将此屏障前后的指令进行重排序。
* 这是必需的,因为编译器无法理解'switch_to'宏(汇编代码)的副作用。*/
barrier();

/*
* 这行代码是由新任务'next'来执行的。
* 它调用finish_task_switch来完成切换的收尾工作,主要是清理'prev'任务的状态,
* 比如释放锁、处理其被借用的mm等。函数返回的是上一个任务所在的运行队列。
*/
return finish_task_switch(prev);
}

__schedule 主调度器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/*
* __schedule() 是主调度器函数。
*
* 驱动调度器从而进入此功能的主要方法是:
*
* 1.显式阻塞:互斥锁、信号量、waitqueue 等。
*
* 2.TIF_NEED_RESCHED 在中断和用户空间返回时检查
*路径。例如,请参阅 arch/x86/entry_64.S。
*
* 为了在任务之间驱动抢占,调度器在 timer 中设置了标志
* 中断处理程序 sched_tick()。
*
* 3.唤醒并不真正导致进入 schedule()。他们添加了一个
* task 添加到运行队列中,就是这样。
*
* 现在,如果添加到 run-queue 的新任务抢占了当前的
* task,则 wakeup 会设置 TIF_NEED_RESCHED,schedule() 会获取
* 在最近的可能场合调用:
*
* - 如果内核是抢占式的 (CONFIG_PREEMPTION=y):
*
* - 在 syscall 或异常上下文中,在下一个最外侧
* preempt_enable() 的(这可能在 wake_up() 的
* spin_unlock()!
*
* - 在 IRQ 上下文中,从 interrupt-handler 返回给
* 抢占式上下文
*
* - 如果内核不是抢占式的(未设置 CONFIG_PREEMPTION)
* 然后在下一个:
*
* - cond_resched() 调用
* - 显式 schedule() 调用
* - 从 syscall 或 exception 返回到用户空间
* - 从中断处理程序返回到用户空间
*
* 警告:必须在禁用抢占的情况下调用!
*/
static void __sched notrace __schedule(int sched_mode)
{
struct task_struct *prev, *next;
/*
* 在 PREEMPT_RT 内核上,会记录 SM_RTLOCK_WAIT
* 作为 schedule_debug() 和 RCU 的抢占。
*/
bool preempt = sched_mode > SM_NONE;
bool is_switch = false;
unsigned long *switch_count;
unsigned long prev_state;
struct rq_flags rf;
struct rq *rq;
int cpu;

trace_sched_entry_tp(preempt, CALLER_ADDR0);

cpu = smp_processor_id();
rq = cpu_rq(cpu);
/* 当前正在运行的任务(prev) */
prev = rq->curr;

schedule_debug(prev, preempt);

if (sched_feat(HRTICK) || sched_feat(HRTICK_DL))
hrtick_clear(rq);

// klp_sched_try_switch(prev);

local_irq_disable();
/* 通知 RCU(Read-Copy-Update)上下文切换 */
rcu_note_context_switch(preempt);

/*
* 确保下面的 signal_pending_state()->signal_pending()
* 不能用 __set_current_state (TASK_INTERRUPTIBLE) 重新排序
* 由调用方完成以避免与 signal_wake_up() 的竞争:
*
* __set_current_state(@state) signal_wake_up()
* schedule() set_tsk_thread_flag(p, TIF_SIGPENDING)
* wake_up_state(p, state)
* LOCK rq->lock LOCK p->pi_state
* smp_mb__after_spinlock() smp_mb__after_spinlock()
* if (signal_pending_state()) if (p->state & @state)
*
* 此外,membarrier 系统调用需要一个完整的内存屏障
* 从用户空间 出来后,存储到 rq->curr 之前;这
* barrier 匹配 membarrier 附近的完整 barrier
* 系统调用 Exit。
*/
rq_lock(rq, &rf);
smp_mb__after_spinlock();

/* Promote REQ to ACT */
rq->clock_update_flags <<= 1;
update_rq_clock(rq);
rq->clock_update_flags = RQCF_UPDATED;

switch_count = &prev->nivcsw;

/* 任务状态更改仅将 SM_PREEMPT 视为抢占 */
preempt = sched_mode == SM_PREEMPT;

/*
* 我们必须加载一次 prev->state (task_struct::state 是 volatile的),例如
* 我们形成一个控制依赖关系,而不是下面的 deactivate_task()。
*/
prev_state = READ_ONCE(prev->__state);
if (sched_mode == SM_IDLE) {
/* SCX 必须咨询 BPF 调度程序以判断 rq 是否为空 */
if (!rq->nr_running && !scx_enabled()) {
next = prev;
goto picked;
}
} else if (!preempt && prev_state) {
/* #define TASK_RUNNING 0x00000000
非抢占 且 任务不为运行中*/
/* 尝试阻塞当前任务 */
try_to_block_task(rq, prev, &prev_state);
/* 更新任务的上下文切换计数(switch_count),指向非自愿上下文切换计数(prev->nvcsw) */
switch_count = &prev->nvcsw;
}

/* 调用 pick_next_task 根据调度策略(rt,dl,fl)选择下一个要运行的任务 */
/* idle 的下一个任务永远是自己 */
/* init_task 启动时copy了两个线程,执行了wake操作入队了rq运行队列,所以这里的next将会是pid1,接着是pid2 */
next = pick_next_task(rq, prev, &rf);
// rq_set_donor(rq, next);
picked:
/* 清除当前任务和运行队列的调度需求标志 */
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
/* 最近一次需要调度的时间点已被清除 */
rq->last_seen_need_resched_ns = 0;

/* 检查当前任务(prev)是否与下一个任务(next)不同。如果不同,则需要进行任务切换 */
is_switch = prev != next;
if (likely(is_switch)) {
/* 增加运行队列的任务切换计数(nr_switches */
rq->nr_switches++;
/*
* rcu_dereference(rq->curr) 的 RCU 用户可能无法看到
* 对 pick_next_task() 对 task_struct 所做的更改。
*/
RCU_INIT_POINTER(rq->curr, next);
/*
* membarrier 系统调用需要每个架构
* 更新后具有完整的内存屏障
* rq->curr,然后返回到用户空间。
*
* 以下是在
* 各种架构:
* - mm ? switch_mm() : mmdrop() for x86, s390, sparc, PowerPC,
* RISC-V. switch_mm() relies on membarrier_arch_switch_mm()
* on PowerPC and on RISC-V.
* - finish_lock_switch() for weakly-ordered
* architectures where spin_unlock is a full barrier,
* - switch_to() for arm64 (weakly-ordered, spin_unlock
* is a RELEASE barrier),
*
* 屏障与附近的完整屏障匹配
* membarrier 系统调用 entry。
*
* 在 RISC-V 上,这个 barrier 配对也是
* SYNC_CORE 在进程之间切换时的命令,cf.
* membarrier_arch_switch_mm() 中的内联注释。
*/
++*switch_count;

/* 禁用任务迁移。 */
// migrate_disable_switch(rq, prev);
/* 更新任务的调度统计信息 */
// psi_account_irqtime(rq, prev, next);
// psi_sched_switch(prev, next, !task_on_rq_queued(prev) ||
// prev->se.sched_delayed);

// trace_sched_switch(preempt, prev, next, prev_state);

/* Also unlocks the rq: */
/* 执行实际的上下文切换,包括切换任务的堆栈和寄存 */
rq = context_switch(rq, prev, next, &rf);
} else {
rq_unpin_lock(rq, &rf);
// __balance_callbacks(rq);
raw_spin_rq_unlock_irq(rq);
}
trace_sched_exit_tp(is_switch, CALLER_ADDR0);
}

__schedule_loop 实现任务调度的循环逻辑

1
2
3
4
5
6
7
8
static __always_inline void __schedule_loop(int sched_mode)
{
do {
preempt_disable();
__schedule(sched_mode);
sched_preempt_enable_no_resched();
} while (need_resched());
}

schedule 实现任务的切换和调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;

#ifdef CONFIG_RT_MUTEXES
lockdep_assert(!tsk->sched_rt_mutex);
#endif

if (!task_is_running(tsk))
/* 提交任务的工作。这通常用于处理任务的挂起或等待状态 */
sched_submit_work(tsk);
/* 进入调度循环,选择下一个要运行的任务 */
__schedule_loop(SM_NONE);
/* 更新当前任务的状态 */
sched_update_worker(tsk);
}
EXPORT_SYMBOL(schedule);

do_task_dead 任务退出的最后一步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*
* 这是一个静态的、永不返回的函数,是任务退出的最后一步。
*/
void __noreturn do_task_dead(void)
{
/*
* 注释:在finish_task_switch()中导致最终的put_task_struct()。
*
* 原理:调用set_special_state,原子地将当前任务的状态设置为TASK_DEAD。
* 这是一个明确的信号,告诉在下一个任务的上下文中运行的
* finish_task_switch(),可以安全地对这个任务的task_struct
* 执行最终的引用计数减少和资源回收了。
*/
set_special_state(TASK_DEAD);

/*
* 注释:告诉freezer(内核冻结机制)忽略我们。
*
* 原理:设置PF_NOFREEZE标志,确保即使系统恰好进入休眠状态,
* 这个正在退出的任务也不会被冻结,其退出流程必须完成。
*/
current->flags |= PF_NOFREEZE;

/*
* 调用核心调度函数,进行最后一次上下文切换。
* SM_NONE表示这是一次常规的、非抢占的调度。
* 对于当前任务来说,这个函数调用将永远不会返回。
*/
__schedule(SM_NONE);

/*
* 这是一个断言。代码的执行流理论上永远不应该到达这里。
* 如果到达了,说明__schedule()意外返回,这是内核的一个严重错误,
* BUG()会立即触发一次内核panic。
*/
BUG();

/*
* 注释:避免“noreturn函数却返回了”的警告 - 但如果BUG()是空操作,不要继续。
*
* 原理:在某些配置下,BUG()可能是空操作(NOP)。为了防止CPU在BUG()之后
* 继续执行内存中的无效指令,这里加入一个无限循环。
* cpu_relax()是一个提示指令,告诉CPU可以在此空转,降低功-耗。
*/
for (;;)
cpu_relax();
}

finish_lock_switch 完成锁切换

1
2
3
4
5
6
7
8
9
10
static inline void finish_lock_switch(struct rq *rq)
{
/*
* 如果我们正在跟踪自旋锁依赖关系,那么我们必须
* 修复运行队列锁——它从前一个线程“继承”到当前线程:
*/
// spin_acquire(&__rq_lockp(rq)->dep_map, 0, 0, _THIS_IP_);
// __balance_callbacks(rq);
raw_spin_rq_unlock_irq(rq);
}

finish_task_switch 完成任务切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/*
* finish_task_switch - 在一次任务切换后进行清理
* @prev: 我们刚刚从其切换开的那个线程。
*/
static struct rq *finish_task_switch(struct task_struct *prev)
/* 这是一个锁注解,告诉静态分析工具,此函数会释放rq->lock。*/
__releases(rq->lock)
{
/* 获取当前CPU的运行队列。*/
struct rq *rq = this_rq();
/* 获取在上下文切换时可能被“延迟释放”的内存描述符。*/
struct mm_struct *mm = rq->prev_mm;
unsigned int prev_state;

/*
* 注释解释:前一个任务离开时,会给我们留下一个为2的抢占计数值,
* 因为它是在执行了preempt_disable()和raw_spin_lock_irq()之后离开的。
*/
/* 这是一个健壮性检查,确保抢占计数值是我们预期的。如果不是,
* 说明内核状态可能已损坏,打印警告并强制重置。*/
if (WARN_ONCE(preempt_count() != 2*PREEMPT_DISABLE_OFFSET,
"corrupted preempt_count: %s/%d/0x%x\n",
current->comm, current->pid, preempt_count()))
preempt_count_set(FORK_PREEMPT_COUNT);

/* 清除运行队列中保存的延迟释放的mm指针。*/
rq->prev_mm = NULL;

/*
* 注释解释:必须在finish_task()清除prev->on_cpu标志之前读取prev->__state,
* 否则,并发的唤醒可能让prev在另一个CPU上运行,我们会与它的
* RUNNING -> DEAD状态转换产生竞争,可能导致双重释放。
*/
/* 原子地读取前一个任务的状态,为后续的僵尸清理做准备。*/
prev_state = READ_ONCE(prev->__state);

/* 调用finish_lock_switch,它会释放运行队列锁(rq->lock),并降低抢占计数。
* 这是本函数最关键的操作之一。*/
finish_lock_switch(rq);

/*
* 注释解释:处理membarrier系统调用的同步要求。如果在用户->内核->用户
* 的切换路径中没有经过switch_mm(),就需要在这里提供内存屏障。
*/
/*
context_switch()函数判断了是否要切换到用户线程
// 检查前一个任务'prev'是否是内核线程。
if (!prev->mm) { // 如果是,说明我们是从一个内核线程切换过来的。
//我们需要在finish_task_switch()中为'prev'借用的地址空间调用mmdrop_lazy_tlb()。
// 因此,先把这个借来的mm保存到运行队列的'prev_mm'字段中
rq->prev_mm = prev->active_mm;
// 清空'prev'内核线程的active_mm指针。
prev->active_mm = NULL;
}
*/
if (mm) {
/* 为SYNC_CORE类型的membarrier提供核心同步。*/
// membarrier_mm_sync_core_before_usermode(mm);
/* 释放对延迟TLB的mm的引用,这隐式地提供了全局内存屏障。*/
mmdrop_lazy_tlb_sched(mm);
}

/*
* 如果前一个任务已经死亡(僵尸状态),在这里执行最终的资源回收。
* unlikely()是一个编译器提示,表示这种情况不常发生。
*/
if (unlikely(prev_state == TASK_DEAD)) {
/* 通知调度类,该任务已死亡。
只有fair调度策略需要
为什么非SMP不需要?在单处理器(UP)系统中,根本不存在“其他CPU”,也就不存在跨CPU的负载均衡。
所有任务都在同一个CPU上,其运行队列的负载统计永远是实时的、准确的。
因此,task_dead这个用于跨核同步的回调就完全没有存在的必要了。*/
if (prev->sched_class->task_dead)
prev->sched_class->task_dead(prev);

/* 任务的内核栈不再需要,释放它。*/
put_task_stack(prev);

/* 释放task_struct结构体本身。*/
put_task_struct_rcu_user(prev);
}

/* 返回当前运行队列的指针。*/
return rq;
}

schedule_tail 完成上下文切换的后半部分清理工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* schedule_tail - 一个刚被fork出来的线程必须调用的第一个函数。
* @prev: 我们刚刚从其切换开的那个线程(即父进程或创建者)。
*/
/* asmlinkage告诉编译器从栈上获取参数,__visible确保函数不被优化掉。
* __releases(rq->lock)是一个锁注解,告诉静态分析工具,这个函数
* 的执行路径中会释放掉运行队列锁。*/
asmlinkage __visible void schedule_tail(struct task_struct *prev)
__releases(rq->lock)
{
/*
* 注释:新任务以FORK_PREEMPT_COUNT的抢占计数值开始,
* 详见该宏和finish_task_switch()的说明。
*
* 注释:finish_task_switch()会释放rq->lock(),并降低抢占计数值,
* 而preempt_enable()最终会(在支持抢占的内核上)使能抢占。
*/wake_up_process

/*
* 调用finish_task_switch,这是完成任务切换最核心的收尾函数。
* 它会做几件重要的事情:
* 1. 释放前一个任务所在运行队列的锁(rq->lock)。
* 2. 如果前一个任务已经死亡(变为僵尸进程),在这里进行清理。
* 3. 降低当前任务的抢占计数值(preempt_count)。
*/
finish_task_switch(prev);
/*
* 注释:这是一个特殊情况:新创建的任务刚刚完成了第一次上下文切换。
* 它在这条路径上是第一次从调度器返回。
*/
/*
* 这是一个追踪点(tracepoint),用于内核的性能分析和调试工具(如ftrace)。
* 它记录下“调度器退出”这一事件,`true`表示这是一个特殊的、非典型的返回路径。
*/
// trace_sched_exit_tp(true, CALLER_ADDR0);

/*
* 使能抢占。这个函数会检查抢占计数值,如果计数值降为0,
* 并且有更高优先级的任务在等待,就会立刻触发一次新的调度。
* 对于新任务来说,这是它第一次变得“可被抢占”的时刻。
*/
preempt_enable();

/*
* 如果当前任务(即子进程)被要求在创建后,将其TID(线程ID)
* 写入父进程指定的一个用户空间地址(通常由clone系统调用的
* CLONE_CHILD_SETTID标志指定)。
*/
if (current->set_child_tid)
/* put_user是一个安全的函数,用于将内核空间的数据(当前任务的TID)
* 写入到用户空间指定的地址(current->set_child_tid)。*/
put_user(task_pid_vnr(current), current->set_child_tid);

/*
* 检查并计算是否有挂起的信号需要处理。
* 尽管任务是新创建的,但在它能够运行之前,可能已经有信号被发送给它。
* 在任务正式开始执行其用户代码或内核逻辑前,必须检查这一点。
*/
/* 计算挂起的信号并清除 */
calculate_sigpending();
}

__task_rq_lock 锁定任务所在的运行队列(rq)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
*__task_rq_lock - 锁定@p所在的 RQ。
*/
struct rq *__task_rq_lock(struct task_struct *p, struct rq_flags *rf)
__acquires(rq->lock)
{
struct rq *rq;

lockdep_assert_held(&p->pi_lock);

for (;;) {
/* 获取任务 p 当前所在的运行队列 */
rq = task_rq(p);
raw_spin_rq_lock(rq);
/* 检查运行队列是否仍然是任务的当前队列,并且任务没有处于迁移状态(task_on_rq_migrating) */
if (likely(rq == task_rq(p) && !task_on_rq_migrating(p))) {
rq_pin_lock(rq, rf);
return rq;
}
raw_spin_rq_unlock(rq);

while (unlikely(task_on_rq_migrating(p)))
/* 如果任务正在迁移,调用 cpu_relax 等待迁移完成 */
cpu_relax();
}
}

__task_rq_unlock 解锁任务所在的运行队列(rq)

1
2
3
4
5
6
static inline void __task_rq_unlock(struct rq *rq, struct rq_flags *rf)
__releases(rq->lock)
{
rq_unpin_lock(rq, rf);
raw_spin_rq_unlock(rq);
}

update_rq_clock_task 更新运行队列的时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* RQ-clock updating methods:
*/

static void update_rq_clock_task(struct rq *rq, s64 delta)
{
/*
* 理论上,编译应该只看到这里的 0,并优化调用
* 更改为 sched_rt_avg_update。但我不相信它......
*/
s64 __maybe_unused steal = 0, irq_delta = 0;

rq->clock_task += delta;

// update_rq_clock_pelt(rq, delta);
}

update_rq_clock 更新运行队列的时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void update_rq_clock(struct rq *rq)
{
s64 delta;
u64 clock;

lockdep_assert_rq_held(rq);

if (rq->clock_update_flags & RQCF_ACT_SKIP)
return;

if (sched_feat(WARN_DOUBLE_CLOCK))
WARN_ON_ONCE(rq->clock_update_flags & RQCF_UPDATED);
rq->clock_update_flags |= RQCF_UPDATED;

clock = sched_clock_cpu(cpu_of(rq));
// scx_rq_clock_update(rq, clock);

delta = clock - rq->clock;
if (delta < 0)
return;
rq->clock += delta;

update_rq_clock_task(rq, delta);
}

enqueue_task 将任务添加到运行队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
/* 如果未设置 ENQUEUE_NOCLOCK 标志,调用 update_rq_clock 更新运行队列的时钟 */
if (!(flags & ENQUEUE_NOCLOCK))
update_rq_clock(rq);

/*
* 可以在 ->enqueue_task() 之前,因为 uclamp 认为
* 在清除 ->sched_delayed 之前ENQUEUE_DELAYED任务
* 在 ->enqueue_task() 中。
*/
// uclamp_rq_inc(rq, p, flags);

/* 调用任务所属调度类的 enqueue_task 方法,将任务加入运行队列。
调度类(如 CFS、RT、DL)定义了任务的具体调度行为 */
/* 初始化由idle执行user_mode_thread了kthread_init线程
在clone过程中,新创建的线程执行了sched_fork(),将sched_class重新赋值为适合的调度策略
idle优先级为MAX_PRIO - 20,所以新建的线程执行策略为rt */
p->sched_class->enqueue_task(rq, p, flags);

// psi_enqueue(p, flags);

/* 未设置 ENQUEUE_RESTORE 标志,调用 sched_info_enqueue 更新调度器的统计信息 */
// if (!(flags & ENQUEUE_RESTORE))
// sched_info_enqueue(rq, p);

/* 启用了核心调度功能(sched_core_enabled),调用 sched_core_enqueue 执行核心调度相关的入队操作。
核心调度是一种机制,用于在 SMT(超线程)环境中隔离任务,确保安全性和性能 */
// if (sched_core_enabled(rq))
// sched_core_enqueue(rq, p);
}

activate_task 将任务从休眠状态激活为可运行状态

  • 将task入队调度器队列中,并设置任务状态为 TASK_ON_RQ_QUEUED
1
2
3
4
5
6
7
8
9
10
11
12
void activate_task(struct rq *rq, struct task_struct *p, int flags)
{
if (task_on_rq_migrating(p))
flags |= ENQUEUE_MIGRATED;
if (flags & ENQUEUE_MIGRATED)
sched_mm_cid_migrate_to(rq, p);

enqueue_task(rq, p, flags);

WRITE_ONCE(p->on_rq, TASK_ON_RQ_QUEUED);
ASSERT_EXCLUSIVE_WRITER(p->on_rq);
}

ttwu_state_match 检查给定任务的状态是否与指定的状态匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static __always_inline
int __task_state_match(struct task_struct *p, unsigned int state)
{
if (READ_ONCE(p->__state) & state)
return 1;

if (READ_ONCE(p->saved_state) & state)
return -1;

return 0;
}

/*
* 从 try_to_wake_up() 调用,用于检查任务是否可以被唤醒。
*
* 如果 p != current,则调用者持有 p::pi_lock;如果 p == current,则调用者禁用抢占。
*
* saved_state 的规则:
*
* 相关的锁定代码在更新 p::saved_state 时总是持有 p::pi_lock,
* 这意味着在两种情况下代码都是完全序列化的。
*
* 对于 PREEMPT_RT,锁等待和锁唤醒通过 TASK_RTLOCK_WAIT 发生。
* 没有设置其他位。这允许区分所有唤醒场景。
*
* 对于 FREEZER,唤醒通过 TASK_FROZEN 发生。没有设置其他位。
* 这使我们能够防止任务在可以运行于非对称 ISA 架构(例如 ARMv9)之前被过早唤醒。
*/
static __always_inline
bool ttwu_state_match(struct task_struct *p, unsigned int state, int *success)
{
int match;

if (IS_ENABLED(CONFIG_DEBUG_PREEMPT)) {
WARN_ON_ONCE((state & TASK_RTLOCK_WAIT) &&
state != TASK_RTLOCK_WAIT);
}

*success = !!(match = __task_state_match(p, state));

/*
* 保存状态在阻塞于 RT 锁或可冻结任务的情况下保留任务状态。如果状态匹配,将 p::saved_state 设置为 TASK_RUNNING,但不要唤醒任务,因为它在等待锁唤醒或 __thaw_task()。还要表明成功,因为从常规唤醒者的角度来看,这已经成功。
* 在获取锁之后,任务将从 p::saved_state 恢复 p::__state,这确保常规唤醒不会丢失。恢复还将 p::saved_state 设置为 TASK_RUNNING,因此任何进一步的测试都不会导致与 @success 的假阳性结果。
*/
if (match < 0)
p->saved_state = TASK_RUNNING;

return match > 0;
}

ttwu_runnable 检查任务是否仍然处于可运行状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
* 考虑@p处于等待循环中:
*
* for (;;) {
* set_current_state(TASK_UNINTERRUPTIBLE);
*
* if (CONDITION)
* break;
*
* schedule();
* }
* __set_current_state(TASK_RUNNING);
*
* 在 set_current_state() 和 schedule() 之间。在这种情况下,@p 仍然是
* runnable,因此需要做的就是将 p->state 改回 TASK_RUNNING
* 一种原子方式。
*
* 通过采用 task_rq(p)->lock,如果 @p->on_rq,则针对 schedule() 进行序列化
* 则 schedule() 仍必须发生,并且 p->state 可以更改为
* TASK_RUNNING。否则我们输掉了比赛,schedule() 已经发生,我们
* 需要用 enqueue 做一个完全唤醒。
*
* 唤醒完成后返回: %true,
* 否则为 LSE。
*/
static int ttwu_runnable(struct task_struct *p, int wake_flags)
{
struct rq_flags rf;
struct rq *rq;
int ret = 0;

rq = __task_rq_lock(p, &rf);
if (task_on_rq_queued(p)) {
update_rq_clock(rq);
if (p->se.sched_delayed)
enqueue_task(rq, p, ENQUEUE_NOCLOCK | ENQUEUE_DELAYED);
if (!task_on_cpu(rq, p)) {
/* 任务不在 CPU 上运行(!task_on_cpu),
调用 wakeup_preempt 检查是否需要抢占当前运行的任务 */
wakeup_preempt(rq, p, wake_flags);
}
/* 调用 ttwu_do_wakeup 执行任务的唤醒操作 */
ttwu_do_wakeup(p);
ret = 1;
}
__task_rq_unlock(rq, &rf);

return ret;
}

ttwu_do_activate 将任务从休眠状态激活为可运行状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static void
ttwu_do_activate(struct rq *rq, struct task_struct *p, int wake_flags,
struct rq_flags *rf)
{
int en_flags = ENQUEUE_WAKEUP | ENQUEUE_NOCLOCK;

lockdep_assert_rq_held(rq);

/* 如果任务 p 对调度器的负载统计有贡献(sched_contributes_to_load),减少运行队列中不可中断任务的计数 */
if (p->sched_contributes_to_load)
rq->nr_uninterruptible--;

#ifdef CONFIG_SMP
if (wake_flags & WF_RQ_SELECTED)
en_flags |= ENQUEUE_RQ_SELECTED;
if (wake_flags & WF_MIGRATED)
en_flags |= ENQUEUE_MIGRATED;
else
#endif
/* 任务处于 I/O 等待状态(in_iowait) */
if (p->in_iowait) {
/* 结束任务的块 I/O 计数 */
delayacct_blkio_end(p);
/* 减少运行队列中等待 I/O 的任务计数 */
atomic_dec(&task_rq(p)->nr_iowait);
}

/* 调用 activate_task 将任务 p 激活并加入运行队列 */
activate_task(rq, p, en_flags);
/* 检查任务是否需要抢占当前运行的任务 */
wakeup_preempt(rq, p, wake_flags);

ttwu_do_wakeup(p);

#ifdef CONFIG_SMP
if (p->sched_class->task_woken) {
/*
* Our task @p is fully woken up and running; so it's safe to
* drop the rq->lock, hereafter rq is only used for statistics.
*/
rq_unpin_lock(rq, rf);
p->sched_class->task_woken(rq, p);
rq_repin_lock(rq, rf);
}

if (rq->idle_stamp) {
u64 delta = rq_clock(rq) - rq->idle_stamp;
u64 max = 2*rq->max_idle_balance_cost;

update_avg(&rq->avg_idle, delta);

if (rq->avg_idle > max)
rq->avg_idle = max;

rq->idle_stamp = 0;
}
#endif
}

ttwu_queue 检查任务是否可以直接加入唤醒列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static inline bool ttwu_queue_wakelist(struct task_struct *p, int cpu, int wake_flags)
{
return false;
}

static void ttwu_queue(struct task_struct *p, int cpu, int wake_flags)
{
struct rq *rq = cpu_rq(cpu);
struct rq_flags rf;

if (ttwu_queue_wakelist(p, cpu, wake_flags))
return;

rq_lock(rq, &rf);
update_rq_clock(rq);
ttwu_do_activate(rq, p, wake_flags, &rf);
rq_unlock(rq, &rf);
}

ttwu_do_wakeup 将任务标记为可运行

1
2
3
4
5
6
7
8
/*
* 将任务标记为可运行.
*/
static inline void ttwu_do_wakeup(struct task_struct *p)
{
WRITE_ONCE(p->__state, TASK_RUNNING);
trace_sched_wakeup(p);
}

try_to_wake_up 唤醒一个线程

  • 任务不在运行队列中 ttwu_queue 将任务加入运行队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* try_to_wake_up -唤醒一个线程
* @p: 需要被唤醒的线
* @state: 任务状态的掩码可以被唤醒
* @wake_flags: 唤醒修饰符标志 (WF_*)
*/
int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
/* 使用 guard(preempt)() 确保在唤醒过程中不会被抢占
#define guard(_name) \
CLASS(_name, __UNIQUE_ID(guard)
*/
guard(preempt)();
int cpu, success = 0;

wake_flags |= WF_TTWU;

if (p == current) {
/*
* 我们正在唤醒当前,这意味着 'p->on_rq' 和 'task_cpu(p) == smp_processor_id()'。综上所述,这意味着我们可以在不获取任何锁的情况下,特别处理下面整个 'p->on_rq && ttwu_runnable()' 的情况。
*
* 具体来说,考虑到当前的运行 ttwu(),我们必须在 schedule() 的 block_task() 之前,因为这样必须不观察 sched_delayed。
*
* 特别是:
* - 我们依赖程序顺序保证来进行所有的排序,
* -我们通过 set_special_state() 使能中断(这允许不获取 ->pi_lock)而被序列化。
*/
WARN_ON_ONCE(p->se.sched_delayed);
/* 查任务的状态是否与 state 匹配 */
if (!ttwu_state_match(p, state, &success))
goto out;
/* WRITE_ONCE(p->__state, TASK_RUNNING); */
trace_sched_waking(p);
/* 直接调用 ttwu_do_wakeup 完成唤醒 */
ttwu_do_wakeup(p);
goto out;
}

/*
* 如果我们要唤醒等待 CONDITION 的线程,
* 需要确保调用者完成的 CONDITION=1 操作不会与下面的 p->state 检查发生重排序。
* 这与等待线程在 set_current_state() 中的 smp_store_mb() 配对。
*/
scoped_guard (raw_spinlock_irqsave, &p->pi_lock) {
smp_mb__after_spinlock();
if (!ttwu_state_match(p, state, &success))
break;

trace_sched_waking(p);

/*
* Ensure we load p->on_rq _after_ p->state, otherwise it would
* be possible to, falsely, observe p->on_rq == 0 and get stuck
* in smp_cond_load_acquire() below.
*
* sched_ttwu_pending() try_to_wake_up()
* STORE p->on_rq = 1 LOAD p->state
* UNLOCK rq->lock
*
* __schedule() (switch to task 'p')
* LOCK rq->lock smp_rmb();
* smp_mb__after_spinlock();
* UNLOCK rq->lock
*
* [task p]
* STORE p->state = UNINTERRUPTIBLE LOAD p->on_rq
*
* Pairs with the LOCK+smp_mb__after_spinlock() on rq->lock in
* __schedule(). See the comment for smp_mb__after_spinlock().
*
* A similar smp_rmb() lives in __task_needs_rq_lock().
*/
smp_rmb();
/* 如果任务已经在运行队列中, 检查任务是否可以直接运行 */
if (READ_ONCE(p->on_rq) && ttwu_runnable(p, wake_flags))
break;

#ifdef CONFIG_SMP
#else
cpu = task_cpu(p);
#endif /* CONFIG_SMP */
/* 任务不在运行队列中 ttwu_queue 将任务加入运行队列 */
ttwu_queue(p, cpu, wake_flags);
}
out:
if (success)
ttwu_stat(p, task_cpu(p), wake_flags);

return success;
}

wake_up_process 唤醒一个特定的进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* wake_up_process - 唤醒一个特定的进程
* @p: 被唤醒的过程。
*
* 尝试唤醒被提名的进程,并将其移动到可运行进程集合中。
*
* Return: 如果进程被唤醒则为1,如果它已经在运行则为0。
*
* 该函数在访问任务状态之前执行完全内存屏障。
*/
int wake_up_process(struct task_struct *p)
{
return try_to_wake_up(p, TASK_NORMAL, 0);
}
EXPORT_SYMBOL(wake_up_process);

wake_q_add 将唤醒排队等待“稍后”唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* wake_q_add() - 将唤醒排队等待“稍后”唤醒。
* @head:要添加@task的wake_q_head
* @task:排队等待“稍后”唤醒的任务
*
* 将任务排队以供稍后唤醒,很可能通过同一上下文中的 wake_up_q() 调用,_但是_这并不能保证,唤醒可以立即到来。
*
* 此函数必须像 wake_up_process() 一样使用;也就是说,任务必须准备好在此位置唤醒。
*/
void wake_q_add(struct wake_q_head *head, struct task_struct *task)
{
if (__wake_q_add(head, task))
get_task_struct(task);
}

wake_q_add_safe 安全地将唤醒操作排队以“稍后”唤醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* wake_q_add_safe() - 安全地将唤醒操作排队以“稍后”唤醒。
* @head: 要添加 @task 的 wake_q_head
* @task: 排队等待“稍后”唤醒的任务
*
* 将任务排队以供稍后唤醒,很可能通过同一上下文中的 wake_up_q() 调用,_但是_这并不能保证,唤醒也可能会立即发生。
*
* 此函数的使用方式必须与 wake_up_process() 一致;也就是说,任务必须在此位置已准备好被唤醒。
*
* 该函数本质上是 wake_q_add() 的任务安全版本。已经持有 @task 引用的调用者可以调用该“安全”版本,并信任 wake_q 根据 @task 是否已被排队唤醒来做正确的处理。
*/
void wake_q_add_safe(struct wake_q_head *head, struct task_struct *task)
{
if (!__wake_q_add(head, task))
put_task_struct(task);
}

__wake_q_add 将任务安全地加入唤醒队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* __wake_q_add - 将任务安全地加入唤醒队列
*
* @head: 唤醒队列头指针
* @task: 需要加入唤醒队列的任务
*
* 该函数用于将指定任务原子性地加入到唤醒队列中,确保同一个任务不会被重复加入队列。
* 如果任务已经在唤醒队列中(即 task->wake_q.next 非空),则不会重复加入,并返回 false。
* 否则,将其加入队列并返回 true。
*
* 为了保证并发安全,在原子操作前使用 smp_mb__before_atomic() 内存屏障,确保任务的唤醒状态对其他处理器可见。
* head->lastp 指向队列的最后一个节点的 next 指针,便于高效地追加新节点。
*/
static bool __wake_q_add(struct wake_q_head *head, struct task_struct *task)
{
struct wake_q_node *node = &task->wake_q;

/*
* 原子性地获取任务,如果 ->wake_q 已经非空,说明已经被加入队列(可能由当前或其他上下文),
* 此时无需重复加入,后续的唤醒操作会处理该任务。
*
* 为确保挂起的唤醒操作能够观察到我们设置的挂起状态,即使在失败的情况下,也需要显式的 smp_mb() 内存屏障。
*/
smp_mb__before_atomic();
/* 如果 wake_q.next 非空,说明任务已经在唤醒队列中,函数返回 false,避免重复加入。
如果 wake_q.next 为 NULL,将任务标记为队列尾部(WAKE_Q_TAIL),表示成功加入队列 */
if (unlikely(cmpxchg_relaxed(&node->next, NULL, WAKE_Q_TAIL)))
return false;

/*
* head 是上下文本地的,不存在并发问题。
*/
/* 更新唤醒队列头指针(head->lastp),将新任务追加到队列尾部 */
*head->lastp = node;
head->lastp = &node->next;
return true;
}

wake_up_q 唤醒队列中的所有任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* wake_up_q - 唤醒唤醒队列中的所有任务
* @head: 唤醒队列头指针
*
* 遍历唤醒队列,将其中的每个任务唤醒,并释放其引用计数。
* 该函数通常与 wake_q_add() 配合使用,实现批量唤醒多个任务。
*/
void wake_up_q(struct wake_q_head *head)
{
struct wake_q_node *node = head->first;

while (node != WAKE_Q_TAIL) {
struct task_struct *task;

task = container_of(node, struct task_struct, wake_q);
node = node->next;
/* 与 __wake_q_add() 中的 cmpxchg_relaxed() 配对 */
WRITE_ONCE(task->wake_q.next, NULL);
/* 现在可以安全地重新插入任务到唤醒队列。 */

/*
* wake_up_process() 会执行一次完整的内存屏障,
* 这与 wake_q_add() 中的排队操作配对,确保不会遗漏唤醒操作。
*/
wake_up_process(task);
put_task_struct(task);
}
}

wake_up_new_task 首次唤醒新创建的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* wake_up_new_task - 首次唤醒新创建的任务。
*
* 此功能将进行一些初始调度程序统计内务管理
* 必须为每个新创建的上下文执行此作,然后将任务
* 并唤醒它。
*/
void wake_up_new_task(struct task_struct *p)
{
struct rq_flags rf;
struct rq *rq;
int wake_flags = WF_FORK;

raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
WRITE_ONCE(p->__state, TASK_RUNNING);

/* 锁定任务所属的运行队列 */
rq = __task_rq_lock(p, &rf);
/* 更新运行队列的时钟,确保调度器的时间信息是最新的 */
update_rq_clock(rq);
/* 初始化任务的调度实体的平均利用率 */
// post_init_entity_util_avg(p);

/* 将任务放入运行队列,并标记为可调度。 */
activate_task(rq, p, ENQUEUE_NOCLOCK | ENQUEUE_INITIAL);
trace_sched_wakeup_new(p);
/* 检查是否需要抢占当前运行的任务 */
wakeup_preempt(rq, p, wake_flags);
#ifdef CONFIG_SMP
if (p->sched_class->task_woken) {
/*
* Nothing relies on rq->lock after this, so it's fine to
* drop it.
*/
rq_unpin_lock(rq, &rf);
p->sched_class->task_woken(rq, p);
rq_repin_lock(rq, &rf);
}
#endif
task_rq_unlock(rq, p, &rf);
}

check_class_changing 通知调度类将要切换到另一个调度类

1
2
3
4
5
6
7
8
9
/*
* ->switching_to() 在持有 pi_lock 和 rq_lock 的情况下被调用,且不得干扰锁定。
*/
void check_class_changing(struct rq *rq, struct task_struct *p,
const struct sched_class *prev_class)
{
if (prev_class != p->sched_class && p->sched_class->switching_to)
p->sched_class->switching_to(rq, p);
}

check_class_changed 通知调度类从一个调度类切换到另一个调度类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* switched_from、switched_to 和 prio_changed 必须 _不能_ 释放 rq->lock,
* 如果需要进行负载均衡,请使用 balance_callback 列表。
*
* 这意味着任何对 check_class_changed() 的调用都必须紧接着调用
* balance_callback()。
*/
void check_class_changed(struct rq *rq, struct task_struct *p,
const struct sched_class *prev_class,
int oldprio)
{
if (prev_class != p->sched_class) {
if (prev_class->switched_from)
prev_class->switched_from(rq, p);

p->sched_class->switched_to(rq, p);
} else if (oldprio != p->prio || dl_task(p))
p->sched_class->prio_changed(rq, p, oldprio);
}

wait_task_inactive 等待一个线程变为非调度状态

  • wait_task_inactive是一个在内核中用于同步等待一个指定的任务p进入一个非运行(inactive)状态的核心函数。非运行状态由调用者通过match_state掩码来定义(例如TASK_UNINTERRUPTIBLE, TASK_PARKED等)。
    它的核心作用是:向调用者保证,当此函数成功返回一个正数时,目标任务p不仅当前不在任何CPU上运行,而且也已经从任何运行队列中被彻底移除,处于一个稳定的睡眠或停止状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
static __always_inline
int __task_state_match(struct task_struct *p, unsigned int state)
{
if (READ_ONCE(p->__state) & state)
return 1;

if (READ_ONCE(p->saved_state) & state)
return -1;

return 0;
}

/*
* wait_task_inactive - 等待一个线程变为非调度状态。
*
* 阻塞等待线程直到在@match_state中设置的任何一个状态。
* 如果它的状态改变了,即@p可能已经被唤醒,则返回零。当我们
* 成功地等到@p离开它的CPU时,我们返回一个正数(它的总切换次数)。
* 如果短时间后的第二次调用返回相同的数字,调用者就可以确定
* @p在整个时间内都保持在非调度状态。
*
* 调用者必须确保该任务*将会*在不久的将来变为非调度状态,
* 否则这个函数可能会长时间地自旋。此函数不能在中断关闭的情况下调用,
* 否则如果我们等待的进程恰好发送了一个IPI,可能会与smp_call_function()
* 引入死锁。
*/
unsigned long wait_task_inactive(struct task_struct *p, unsigned int match_state)
{
/* running: 布尔值,标记p在持锁检查时是否在CPU上运行。*/
int running;
/* queued: 布尔值,标记p在持锁检查时是否仍在运行队列中。*/
int queued;
/* match: 存储状态匹配的结果。*/
int match;
/* rf: 用于保存task_rq_lock获取锁前的中断状态。*/
struct rq_flags rf;
/* ncsw: "non-voluntary context switches",用于存储p的上下文切换次数,并作为成功/失败的标志。*/
unsigned long ncsw;
/* rq: 指向p所在的运行队列。*/
struct rq *rq;

/* 这是一个无限循环,直到满足成功或失败的退出条件。*/
for (;;) {
/*
* 我们在完全不持有任何任务队列锁的情况下进行初始的早期启发式检查。
* 我们只在情况看起来很有可能成功时,才尝试去获取运行队列锁!
*/
/* 获取p当前所在的运行队列rq的指针(无锁操作)。*/
rq = task_rq(p);

/*
* 如果任务仍然在另一个CPU上活跃地运行,就在不持有任何锁的情况下,
* 放松并忙等待。
*
* 注意!因为我们不持有任何锁,所以甚至不能保证"rq"一直是正确的
* 运行队列!但我们不在乎,因为如果运行队列改变了并且p实际上
* 现在在别处运行,"task_on_cpu()"会返回false!
*/
/* 这是一个无锁的、乐观的自旋等待循环。*/
while (task_on_cpu(rq, p)) {
/* 在自旋期间,持续检查任务状态是否还匹配。如果不匹配,说明等待失败。*/
if (!task_state_match(p, match_state))
return 0; /* 返回0表示失败。*/
/* 调用cpu_relax(),提示CPU可以降低功耗,并避免活锁。*/
cpu_relax();
}

/*
* 好是时候更仔细地看看了!我们现在需要rq锁,来获得*确切的*信息。
* 如果我们错了,我们只会回去并重复。
*/
/* 调用task_rq_lock,获取p所在运行队列的锁,并禁用中断。*/
rq = task_rq_lock(p, &rf);
/*
* 如果任务处于延迟调度状态,则强制将其出队,以避免在下面的
* queued情况下总是碰到tick超时。
*/
if (p->se.sched_delayed)
dequeue_task(rq, p, DEQUEUE_SLEEP | DEQUEUE_DELAYED);
/* 发出一个追踪点,用于调度器延迟分析。*/
trace_sched_wait_task(p);
/* 在持锁的情况下,再次、权威地检查p是否在运行。*/
running = task_on_cpu(rq, p);
/* 在持锁的情况下,权威地检查p是否仍在运行队列中。*/
queued = task_on_rq_queued(p);
/* 将ncsw清零,作为初始的失败标记。*/
ncsw = 0;
/* 在持锁的情况下,权威地检查p的状态是否匹配match_state。*/
if ((match = __task_state_match(p, match_state))) {
/*
* 当匹配p->saved_state时,将此任务视为仍在排队,以便它会等待。
*/
if (match < 0)
queued = 1;
/* 如果状态匹配,则获取p的非自愿上下文切换次数,并将其最高位置为1,
* 作为一个成功的“快照”标记。*/
ncsw = p->nvcsw | LONG_MIN; /* sets MSB */
}
/* 释放运行队列锁,并恢复中断状态。*/
task_rq_unlock(rq, p, &rf);

/*
* 如果它从期望的状态改变了,现在就退出。
* 如果!ncsw为真,说明持锁检查时状态不匹配。
*/
if (unlikely(!ncsw))
break;

/*
* 在我们实际持有正确的锁并检查之后,它到底是不是真的在运行?
*
* 糟糕。回去再试一次..
*/
/* 如果持锁检查时发现它仍在运行,则继续外层循环,回到第一阶段的忙等。*/
if (unlikely(running)) {
cpu_relax();
continue;
}

/*
* 它没有在活跃地运行还不够,它必须*完全地*离开运行队列,
* 并且没有被抢占!
*
* 所以如果它仍然是可运行的(但只是当前没有在活跃地运行),
* 它就是被抢占了,我们应该yield - 这可能会需要一段时间。
*/
/* 如果它仍在队列中(即就绪态,但未运行)。*/
if (unlikely(queued)) {
/* 设置一个短暂的超时时间(通常是一个调度tick)。*/
ktime_t to = NSEC_PER_SEC / HZ;

/* 将我们自己(调用者)置于不可中断睡眠。*/
set_current_state(TASK_UNINTERRUPTIBLE);
/* 调用schedule_hrtimeout进行一次定时的、短暂的睡眠。*/
schedule_hrtimeout(&to, HRTIMER_MODE_REL_HARD);
/* 睡眠结束后,继续外层循环,重新开始所有检查。*/
continue;
}

/*
* 啊哈,一切都好。它没有在运行,也不是可运行的,这意味着
* 它未来也不会自己变为运行状态。我们全都搞定了!
*/
/* 如果代码执行到这里,说明running和queued都为false,等待成功。*/
break;
}

/* 返回ncsw。如果成功,它是一个设置了最高位的正数;如果失败,它是0。*/
return ncsw;
}