前言
一、调度器的概念
在 Linux 中,调度器负责管理系统中的进程和线程的执行顺序,以确保 CPU 资源的有效利用。其主要职责包括:
- 任务调度:决定哪个进程或线程在任何时刻获得 CPU 使用权。
- 时间片分配:为每个任务分配执行时间,确保公平性和响应性。
- 优先级处理:根据任务的优先级和其他调度策略,安排任务执行顺序。
- 上下文切换:保存当前任务的状态,加载下一个任务的状态,以实现任务切换。
调度器通过不同的调度策略(如完全公平调度器 CFS、实时调度策略等)来优化系统性能、响应时间和任务的公平性。
二、调度器种类
在 Linux 内核中,调度器的种类通过不同的 sched_class
结构体来实现。这些调度类用于定义不同的调度策略,以满足不同类型的任务需求。主要的调度类包括:
-
stop_sched_class
:- 作用:用于处理被停止的任务(即被暂停的任务)。当任务被停止时,它不会参与调度,也不会占用 CPU 时间。
- 特点:它的调度方法通常是空实现,因为这些任务不会被调度。
-
dl_sched_class
:- 作用:处理 Deadline Scheduling(DL),即带有截止期限的调度策略。适用于实时任务,这些任务必须在特定时间之前完成。
- 特点:基于实时调度策略的算法,任务的调度取决于它们的截止期限和执行时间,确保任务在其截止时间之前完成。
-
rt_sched_class
:- 作用:处理 Real-Time Scheduling(RT),即实时调度策略。适用于需要严格实时性的任务,比如控制系统和音频处理等。
- 特点:实时调度策略包括 FIFO(先进先出)和 Round Robin(轮转调度),这些任务的调度优先级高于普通任务,确保实时任务能够按时执行。
-
fair_sched_class
:- 作用:处理 Completely Fair Scheduler(CFS),这是 Linux 内核的默认调度策略。CFS 旨在公平地分配 CPU 时间给所有任务。
- 特点:CFS 使用红黑树来管理任务,确保任务根据它们的运行时间和等待时间获得公平的 CPU 时间。它尝试在任务之间平衡 CPU 使用,以实现响应性和公平性。
-
idle_sched_class
:- 作用:处理空闲任务调度,即当系统中没有其他任务需要执行时,调度器会选择空闲任务来运行。
- 特点:它处理的任务通常是系统的空闲任务,这些任务主要用于降低功耗或进行系统维护工作。
这些调度类通过相应的 sched_class
结构体中的函数指针实现各自的调度策略,并通过调度器的核心机制来管理任务的执行。每种调度类都有其特定的用途和策略,以满足不同类型任务的需求。
调度类结构体解析:
struct sched_class {
const struct sched_class *next;
// 指向下一个调度类的指针,用于形成一个调度类链表。在任务调度时,内核会遍历这个链表。
#ifdef CONFIG_UCLAMP_TASK
int uclamp_enabled;
// 表示是否启用了任务的uClamp(用户定义的任务资源限制)功能,仅在配置启用了 CONFIG_UCLAMP_TASK 时存在。
#endif
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
// 函数指针,指向将任务加入到运行队列(runqueue)的函数。每个调度类需要定义自己的实现。
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
// 函数指针,指向将任务从运行队列中移除的函数。每个调度类需要定义自己的实现。
void (*yield_task) (struct rq *rq);
// 函数指针,指向让当前任务主动放弃 CPU 的函数。每个调度类需要定义自己的实现。
bool (*yield_to_task)(struct rq *rq, struct task_struct *p, bool preempt);
// 函数指针,指向让当前任务主动让出 CPU 给特定任务的函数。每个调度类需要定义自己的实现。
void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
// 函数指针,指向检查当前任务是否应该被抢占的函数。每个调度类需要定义自己的实现。
struct task_struct *(*pick_next_task)(struct rq *rq);
// 函数指针,指向选择下一个要运行的任务的函数。每个调度类需要定义自己的实现。
void (*put_prev_task)(struct rq *rq, struct task_struct *p);
// 函数指针,指向将当前任务从 CPU 上移除的函数。每个调度类需要定义自己的实现。
void (*set_next_task)(struct rq *rq, struct task_struct *p, bool first);
// 函数指针,指向设置下一个要运行的任务的函数。每个调度类需要定义自己的实现。
#ifdef CONFIG_SMP
int (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
// 函数指针,指向在对称多处理器(SMP)系统中执行负载平衡的函数,仅在配置启用了 CONFIG_SMP 时存在。
int (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
// 函数指针,指向选择任务要运行在哪个 CPU 上的函数。每个调度类需要定义自己的实现。
void (*migrate_task_rq)(struct task_struct *p, int new_cpu);
// 函数指针,指向将任务从一个 CPU 迁移到另一个 CPU 的函数。每个调度类需要定义自己的实现。
void (*task_woken)(struct rq *this_rq, struct task_struct *task);
// 函数指针,指向在任务被唤醒时执行的函数。每个调度类需要定义自己的实现。
void (*set_cpus_allowed)(struct task_struct *p, const struct cpumask *newmask);
// 函数指针,指向设置任务允许运行的 CPU 集合的函数。每个调度类需要定义自己的实现。
void (*rq_online)(struct rq *rq);
// 函数指针,指向在运行队列(rq)变为在线时执行的函数。每个调度类需要定义自己的实现。
void (*rq_offline)(struct rq *rq);
// 函数指针,指向在运行队列(rq)变为离线时执行的函数。每个调度类需要定义自己的实现。
#endif
void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
// 函数指针,指向在系统时钟滴答时更新任务的函数。每个调度类需要定义自己的实现。
void (*task_fork)(struct task_struct *p);
// 函数指针,指向在任务创建时执行的函数。每个调度类需要定义自己的实现。
void (*task_dead)(struct task_struct *p);
// 函数指针,指向在任务结束时执行的函数。每个调度类需要定义自己的实现。
// 这些函数在任务上下文切换时调用,用于处理任务切换前后的状态保存和恢复。
void (*switched_from)(struct rq *this_rq, struct task_struct *task);
void (*switched_to) (struct rq *this_rq, struct task_struct *task);
void (*prio_changed) (struct rq *this_rq, struct task_struct *task, int oldprio);
// 函数指针,指向在任务优先级发生变化时执行的函数。每个调度类需要定义自己的实现。
unsigned int (*get_rr_interval)(struct rq *rq, struct task_struct *task);
// 函数指针,指向获取任务的轮转调度时间片长度的函数。每个调度类需要定义自己的实现。
void (*update_curr)(struct rq *rq);
// 函数指针,指向更新当前任务的运行时间等信息的函数。每个调度类需要定义自己的实现。
#define TASK_SET_GROUP 0
#define TASK_MOVE_GROUP 1
#ifdef CONFIG_FAIR_GROUP_SCHED
void (*task_change_group)(struct task_struct *p, int type);
// 函数指针,指向在任务的调度组发生变化时执行的函数。每个调度类需要定义自己的实现,仅在配置启用了 CONFIG_FAIR_GROUP_SCHED 时存在。
#endif
};
三、linux中的调度策略
在 Linux 内核中,调度策略通过几个宏来定义和管理。以下是六个主要的调度策略宏:
-
SCHED_NORMAL
:- 作用:表示默认的调度策略,用于普通的非实时任务。
- 特点:任务将根据完全公平调度器(CFS)进行调度,这种调度策略旨在公平地分配 CPU 时间。
-
SCHED_FIFO
:- 作用:表示实时任务的 FIFO(先进先出)调度策略。
- 特点:高优先级的实时任务将优先于低优先级任务执行,并且在同优先级任务之间按先后顺序调度。
-
SCHED_RR
:- 作用:表示实时任务的轮转(Round Robin)调度策略。
- 特点:类似于 FIFO,但同优先级任务之间会轮流分配固定时间片,这样可以防止某个任务独占 CPU。
-
SCHED_BATCH
:- 作用:用于批处理任务的调度策略。
- 特点:优化长时间运行的任务,减少对交互性任务的干扰,适合计算密集型的后台任务。
-
SCHED_IDLE
:- 作用:用于空闲任务的调度策略。
- 特点:在系统处于空闲状态时,执行空闲任务,目的是降低功耗或进行系统维护工作。
-
SCHED_DEADLINE
:- 作用:表示带有截止期限的实时调度策略。
- 特点:为每个任务设置截止期限和执行时间,调度算法确保任务在其截止时间之前完成。
这些宏通过在内核中设置不同的调度策略,使得 Linux 内核能够根据任务的不同需求和优先级来灵活地管理任务的调度。
四、CFS调度器
Linux 的 Completely Fair Scheduler(CFS)是 Linux 内核中的默认调度器,用于管理系统中任务的调度。CFS 旨在实现公平的 CPU 时间分配,确保所有任务能够公平地获得 CPU 使用权。它基于红黑树的数据结构来管理任务,并采用了精心设计的算法来平衡任务之间的公平性和响应性。下面是对 CFS 调度器的一些核心概念的详细讲解:
1. 基本原理
- 公平性:CFS 的主要目标是公平地分配 CPU 时间。每个任务都被分配一个时间片,CFS 通过确保所有任务按公平的比例获得 CPU 时间来实现这一目标。
- 红黑树:CFS 使用红黑树(自平衡的二叉搜索树)来维护就绪队列。每个节点表示一个任务,节点的排序基于任务的虚拟运行时间(
vruntime
)。
2. 虚拟运行时间(vruntime
)
- 定义:每个任务有一个
vruntime
,表示该任务的实际运行时间经过了公平的调整。vruntime
是根据任务的实际执行时间和它的优先级来计算的。 - 目的:通过将运行时间和优先级考虑在内,CFS 确保所有任务得到公平的 CPU 时间。
vruntime
越小,任务就越早被调度。
3. 调度过程
- 任务选择:CFS 选择
vruntime
最小的任务作为下一个要运行的任务。因为vruntime
最小的任务表示它在公平调度中还没获得足够的 CPU 时间。 - 时间片:CFS 不使用固定时间片,而是根据任务的
vruntime
动态调整时间片。任务的运行时间会影响其vruntime
,确保长时间运行的任务不会过度占用 CPU。
4. 调度周期
- 上下文切换:当任务的时间片用完或者任务被抢占时,CFS 会进行上下文切换,保存当前任务的状态,并加载下一个任务的状态。
- 负载平衡:CFS 还包括负载平衡机制,它在多处理器系统中确保各个 CPU 之间的负载均衡。
5. CFS 的关键数据结构
cfs_rq
:这是 CFS 调度队列的数据结构,包含所有就绪任务的红黑树。每个 CPU 有一个cfs_rq
实例。task_struct
:这是每个任务的主要数据结构,包含任务的vruntime
和其他调度相关信息。sched_entity
:这是任务调度的核心实体,存储任务的调度信息,包括vruntime
和调度队列相关数据。
6. 调度策略
load_balance
:CFS 包含一个负载平衡机制,确保多核处理器上的负载均衡。这有助于避免某个 CPU 被过度占用,而其他 CPU 空闲。wake_up
:当任务变为就绪状态时,CFS 会更新vruntime
并调整调度队列,确保新的任务能够及时调度。
7. 其他特点
- 延迟:CFS 支持多种延迟机制,例如
sched_latency
和sched_min_granularity
,用来控制任务的调度延迟和最小时间片。 - 可调性:CFS 允许用户通过
sysfs
接口和调度策略参数调整调度行为,以适应不同的应用需求和系统负载。
CFS 的设计目标是通过公平的 CPU 时间分配来提高系统的响应性和任务的公平性,并在现代多核处理器环境中有效地管理任务调度。它的使用大大提升了系统的整体性能和用户体验。
五、进程优先级
在 Linux 内核中,调度系统使用优先级来决定任务的执行顺序。上面定义的宏定义了与任务优先级相关的一些常量。以下是对这些宏的详细解释:
1. #define MAX_USER_RT_PRIO 100
- 作用: 这个宏定义了用户空间实时任务的最大优先级数目。
MAX_USER_RT_PRIO
是一个用于实时调度的优先级限制的常量,表示用户空间可使用的最大实时优先级数目。 - 说明: 在 Linux 内核中,实时任务(如使用
SCHED_FIFO
或SCHED_RR
调度策略的任务)有更高的优先级范围。MAX_USER_RT_PRIO
通常设定为 100,表示用户空间可以使用 100 个实时优先级。
2. #define MAX_RT_PRIO MAX_USER_RT_PRIO
- 作用: 这个宏定义了内核空间中定义的最大实时优先级。
- 说明:
MAX_RT_PRIO
直接等于MAX_USER_RT_PRIO
,表明内核空间的最大实时优先级数目与用户空间的一致。这样设计的目的是保持一致性,确保用户空间和内核空间中的实时优先级范围相同。
3. #define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH)
- 作用: 这个宏定义了所有调度优先级的最大值,包括实时优先级和普通优先级。
- 说明:
MAX_PRIO
是系统中所有任务的最高优先级值。MAX_RT_PRIO
是实时任务的最大优先级,而NICE_WIDTH
是非实时任务的优先级宽度。MAX_PRIO
是实时优先级的最大值加上普通优先级的范围。NICE_WIDTH
表示普通任务优先级的跨度(范围),通常是 40(表示 20 个优先级等级从 -20 到 +19)。
4. #define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2)
- 作用: 这个宏定义了系统中普通任务的默认优先级。
- 说明:
DEFAULT_PRIO
是普通任务的默认优先级,它位于实时优先级和普通优先级范围的中间。计算方式是将MAX_RT_PRIO
与NICE_WIDTH
的一半相加。这意味着默认优先级将处于普通优先级范围的中间,通常这有助于为大多数任务提供适当的优先级。
总结
- 实时任务优先级:
MAX_USER_RT_PRIO
和MAX_RT_PRIO
定义了用户空间和内核空间的最大实时任务优先级。 - 总优先级范围:
MAX_PRIO
定义了所有优先级的最大值,包括实时和普通任务。 - 默认优先级:
DEFAULT_PRIO
是普通任务的默认优先级,位于普通优先级范围的中间位置。
这些宏定义确保了任务调度系统中的优先级范围在实时和非实时任务之间的一致性,并提供了一个默认的优先级设置。