在当今的技术领域中,Linux 系统犹如一座巍峨的高山,屹立于服务器、开发环境等众多关键场景的核心位置。据统计,全球超 90% 的超级计算机运行着 Linux 操作系统,在服务器市场中,Linux 更是凭借其高稳定性、安全性以及开源特性,占据了相当可观的份额 ,众多大型网站、企业级应用的服务器都基于 Linux 搭建。对于开发者而言,Linux 也是不可或缺的开发环境,大量的开源软件和丰富的开发工具都能在 Linux 上完美运行。
在 Linux 系统中,进程是其核心概念,是系统进行资源分配和调度的基本单位。可以说,Linux 系统中的一切活动几乎都离不开进程的参与,从启动一个简单的应用程序,到执行复杂的系统命令,再到管理系统资源,进程就像幕后的 “隐形引擎”,驱动着整个 Linux 系统的稳定运行。
一、Linux调度器简介
随着时代的发展,linux也从其初始版本稳步发展到今天,从2.4的非抢占内核发展到今天的可抢占内核,调度器无论从代码结构还是设计思想上也都发生了翻天覆地的变化,其普通进程的调度算法也从O(1)到现在的CFS,一个好的调度算法应当考虑以下几个方面:
公平:保证每个进程得到合理的CPU时间。高效:使CPU保持忙碌状态,即总是有进程在CPU上运行。响应时间:使交互用户的响应时间尽可能短。周转时间:使批处理用户等待输出的时间尽可能短。吞吐量:使单位时间内处理的进程数量尽可能多。负载均衡:在多核多处理器系统中提供更高的性能
而整个调度系统至少包含两种调度算法,是分别针对实时进程和普通进程,所以在整个linux内核中,实时进程和普通进程是并存的,但它们使用的调度算法并不相同,普通进程使用的是CFS调度算法(红黑树调度)。之后会介绍调度器是怎么调度这两种进程。
1.1进程分类
在linux中,进程主要分为两种,一种为实时进程,一种为普通进程。
实时进程:对系统的响应时间要求很高,它们需要短的响应时间,并且这个时间的变化非常小,典型的实时进程有音乐播放器,视频播放器等。普通进程:包括交互进程和非交互进程,交互进程如文本编辑器,它会不断的休眠,又不断地通过鼠标键盘进行唤醒,而非交互进程就如后台维护进程,他们对IO,响应时间没有很高的要求,比如编译器。
它们在linux内核运行时是共存的,实时进程的优先级为0~99,实时进程优先级不会在运行期间改变(静态优先级),而普通进程的优先级为100~139,普通进程的优先级会在内核运行期间进行相应的改变(动态优先级)。
1.2调度策略
在linux系统中,调度策略分为:
SCHED_NORMAL:普通进程使用的调度策略,现在此调度策略使用的是CFS调度器。SCHED_FIFO:实时进程使用的调度策略,此调度策略的进程一旦使用CPU则一直运行,直到有比其更高优先级的实时进程进入队列,或者其自动放弃CPU,适用于时间性要求比较高,但每次运行时间比较短的进程。SCHED_RR:实时进程使用的时间片轮转法策略,实时进程的时间片用完后,调度器将其放到队列末尾,这样每个实时进程都可以执行一段时间。适用于每次运行时间比较长的实时进程。
1.3调度选择
首先,我们需要清楚,什么样的进程会进入调度器进行选择,就是处于TASK_RUNNING状态的进程,而其他状态下的进程都不会进入调度器进行调度。系统发生调度的时机如下:
调用cond_resched()时显式调用schedule()时从系统调用或者异常中断返回用户空间时从中断上下文返回用户空间时
当开启内核抢占(默认开启)时,会多出几个调度时机,如下:
在系统调用或者异常中断上下文中调用preempt_enable()时(多次调用preempt_enable()时,系统只会在最后一次调用时会调度)在中断上下文中,从中断处理函数返回到可抢占的上下文时(这里是中断下半部,中断上半部实际上会关中断,而新的中断只会被登记,由于上半部处理很快,上半部处理完成后才会执行新的中断信号,这样就形成了中断可重入, 但是即使是中断下半部, 也是不能够被调度的)
而在系统启动调度器初始化时会初始化一个调度定时器,调度定时器每隔一定时间执行一个中断,在中断会对当前运行进程运行时间进行更新,如果进程需要被调度,在调度定时器中断中会设置一个调度标志位,之后从定时器中断返回,因为上面已经提到从中断上下文返回时是可能有调度时机的,如果定时器中断返回时正好是返回到用户态空间, 而且调度标志位又置位了, 这时候就会做进程切换. 在内核源码的汇编代码中所有中断返回处理都必须去判断调度标志位是否设置,如设置则执行schedule()进行调度。
而我们知道实时进程和普通进程是共存的,调度器是怎么协调它们之间的调度的呢,其实很简单,每次调度时,会先在实时进程运行队列中查看是否有可运行的实时进程,如果没有,再去普通进程运行队列找下一个可运行的普通进程,如果也没有,则调度器会使用idle进程进行运行。之后的章节会放上代码进行详细说明。
系统并不是每时每刻都允许调度的发生,当处于中断期间的时候(无论是上半部还是下半部),调度是被系统禁止的,之后中断过后才重新允许调度。而对于异常,系统并不会禁止调度,也就是在异常上下文中,系统是有可能发生调度的。
二、调度器理论基础
2.1调度器的关键作用与设计目标
Linux调度器的核心职责是进行CPU资源分配,在多任务环境下,系统中存在大量等待执行的进程,调度器如同一位精准的分配者,决定每个进程何时能够获得 CPU 时间片,确保各个进程都能在有限的CPU资源下有序运行。
为了实现高效稳定的系统运行,调度器有着明确的设计目标。公平性是其中的重要一环,它要求调度器避免某些进程长时间占用 CPU,而其他进程却长时间处于等待状态,确保每个进程都能按照其权重合理地分享 CPU 时间。以多个用户同时使用系统资源为例,公平的调度能让每个用户的任务都能得到及时处理,不会因为某个用户的大型任务而导致其他用户的任务被无限期搁置。
快速响应也是调度器追求的目标之一。在用户进行交互操作时,如点击应用程序图标、输入命令等,调度器需要迅速将 CPU 资源分配给相应的进程,以确保系统能够快速响应用户的请求,提供流畅的交互体验。这对于桌面环境和实时应用来说尤为重要,比如在进行视频会议时,调度器需要快速调度相关音频、视频处理进程,保证音视频的实时性和流畅性。
高吞吐量意味着调度器要尽可能地提高系统在单位时间内完成的任务数量。它通过合理安排进程的执行顺序和时间片,充分利用 CPU 资源,减少 CPU 的空闲时间,从而提高系统的整体处理能力。在服务器环境中,高吞吐量可以让服务器同时处理大量的请求,提高服务的效率和质量。
在如今注重能源效率的时代,低功耗也是调度器需要考虑的因素。对于移动设备和一些对功耗有严格要求的服务器,调度器通过优化调度策略,减少 CPU 不必要的工作时间,在保证系统性能的前提下,降低 CPU 的功耗,延长设备的续航时间或降低能源消耗。
2.2核心调度因素剖析
(1)优先级
优先级是调度器决定进程执行顺序的重要依据。在 Linux 系统中,进程分为实时进程和普通进程,它们有着不同的优先级范围。实时进程的优先级通常较高,范围一般是 1 - 99(数值越大优先级越高) ,普通进程则通过 Nice 值来表示优先级,范围是 - 20 - 19(数值越小优先级越高)。
例如,在一个同时运行视频播放(实时进程)和文件备份(普通进程)的系统中,视频播放进程由于其对实时性要求高,被赋予较高的优先级。当 CPU 资源有限时,调度器会优先调度视频播放进程,确保视频的流畅播放,而文件备份进程则需要等待视频播放进程暂时释放 CPU 资源后才有机会执行。这体现了优先级在调度中的关键作用,它能根据进程的重要性和实时性需求来合理分配 CPU 资源。
(2)时间片
时间片是指进程一次能够占用 CPU 的时间长度。在早期的调度算法中,时间片通常是固定的,而现代的 Linux 调度器如 CFS(完全公平调度器)虽然不再使用传统意义上固定的时间片概念,但也有类似的时间分配机制。在 CFS 中,每个进程都有一个虚拟运行时间(vruntime),它根据进程的权重来计算进程实际占用 CPU 的时间,从而实现公平的调度。
以一个简单的多进程场景为例,假设有两个进程 A 和 B,进程 A 的权重较高,进程 B 的权重较低。在 CFS 调度下,虽然没有固定的时间片分配,但进程 A 会因为其权重高,在相同的物理时间内,其虚拟运行时间增长相对较慢,从而有更多机会获得 CPU 资源,而进程 B 则会根据其权重相对较少地获得 CPU 执行时间,这就保证了在时间分配上的公平性,同时也实现了类似于时间片分配的效果,使得各个进程都能得到执行的机会 。
(3)I/O 需求
进程的 I/O 需求也是调度器需要考虑的重要因素。I/O 密集型进程通常大部分时间用于等待 I/O 操作完成,如文件读写、网络数据传输等,它们对 CPU 的占用时间相对较少。而 CPU 密集型进程则主要进行大量的计算工作,长时间占用 CPU 资源。
调度器会根据进程的 I/O 特性来调整调度策略。对于 I/O 密集型进程,调度器会尽量在 I/O 操作完成后及时调度它们,以减少 I/O 设备的空闲时间,提高系统的整体效率。比如在一个同时运行数据库查询(I/O 密集型)和科学计算(CPU 密集型)的系统中,当数据库查询操作完成 I/O 读取数据后,调度器会迅速调度该进程,让其尽快处理读取到的数据,而不是让其等待较长时间,这样可以充分利用 I/O 设备和 CPU 资源,提高系统的响应速度和吞吐量。
2.3经典调度策略解读
①FIFO(First - In - First - Out,先进先出)
原理:FIFO 调度策略按照进程进入就绪队列的先后顺序进行调度。当一个进程进入就绪队列后,它会排在队列末尾,调度器总是选择队列头部的进程运行,并且该进程会一直运行直到它完成任务、主动放弃 CPU(例如进行 I/O 操作进入阻塞状态)或者被更高优先级的实时进程抢占。
特点:这种调度策略实现简单,不需要复杂的算法来计算调度顺序。但它存在明显的缺点,对于长进程而言,如果它先进入就绪队列,就会一直占用 CPU,导致后面进入的短进程等待时间过长,这在实际应用中可能会造成系统响应性变差,用户体验不佳。
适用场景:FIFO 适用于一些对顺序性要求较高且任务执行时间相对固定的场景,比如某些批处理任务,它们之间没有严格的时间限制和优先级差异,按照先后顺序依次执行即可。
②RR(Round - Robin,轮转调度)
原理:RR 调度策略为每个进程分配一个固定的时间片,当进程的时间片用完后,它会被放回就绪队列末尾,调度器接着调度下一个进程。这样所有处于就绪队列的进程会按照轮转的方式依次获得 CPU 时间片进行执行。
特点:RR 调度策略的公平性较好,每个进程都有机会在一定时间间隔内获得 CPU 资源,不会出现某个进程长时间被饿死的情况。然而,它的时间片大小设置较为关键,如果时间片设置过大,会退化为类似 FIFO 的调度方式,影响短进程的响应;如果时间片设置过小,会导致进程上下文切换过于频繁,增加系统开销。
适用场景:RR 适用于那些对响应时间要求较高且进程执行时间差异不大的场景,如早期的分时操作系统,多个用户通过终端同时连接到系统,RR 调度可以保证每个用户的任务都能得到及时响应,感觉系统是在为自己单独服务。
③CFS(Completely Fair Scheduler,完全公平调度器)
原理:CFS 是 Linux 内核中广泛使用的调度器,它基于虚拟运行时间(vruntime)来实现调度。每个进程都有一个 vruntime,它会随着进程占用 CPU 时间的增加而增长,增长的速度与进程的权重成反比。调度器总是选择 vruntime 最小的进程运行,这样权重高的进程会获得更多的 CPU 时间,从而实现了公平的调度。CFS 使用红黑树来维护就绪队列中的进程,通过红黑树的高效查找功能,可以快速找到 vruntime 最小的进程。
特点:CFS 的优点在于它能够在不同类型的进程之间实现较好的公平性,无论是 CPU 密集型进程还是 I/O 密集型进程,都能根据其权重合理地获得 CPU 资源。它还支持多核 CPU 和 CPU 热插拔,能够动态调整调度策略以适应系统硬件的变化。同时,CFS 通过引入虚拟运行时间的概念,避免了传统时间片调度方式中时间片大小难以确定的问题。
适用场景:CFS 适用于大多数通用场景,包括桌面环境和服务器环境。在桌面环境中,它可以保证用户的交互操作(如鼠标点击、键盘输入等)能够得到快速响应,同时也能合理调度后台的各种任务(如文件同步、系统更新等)。在服务器环境中,CFS 可以高效地管理多个用户的任务请求,确保每个用户的服务质量,提高服务器的整体性能和吞吐量。
三、调度器源码深度解析
3.1源码环境搭建与关键文件定位
在开始深入剖析 Linux 调度器源码之前,搭建一个合适的源码分析环境是至关重要的。首先,你需要获取 Linux 内核源码。可以从官方的 Linux 内核官网(https://www.kernel.org/)下载你想要分析的内核版本源码。例如,若要分析较新的稳定版本,可选择对应的.tar.xz 或.tar.gz 压缩包下载。
下载完成后,解压源码包。假设你将源码解压到了/home/user/linux - kernel目录下。接下来,安装一些必要的工具,如make、gcc等编译工具。在基于 Debian 或 Ubuntu 的系统中,可以通过以下命令安装:
复制
sudo apt - get install build - essential1.
在基于 Red Hat 或 CentOS 的系统中,使用以下命令:
复制
sudo yum install make gcc1.
对于源码阅读,一款好用的工具能大大提高效率。Source Insight 是一个不错的选择,它能够提供语法高亮、代码导航等功能,方便我们快速定位和理解代码。安装并打开 Source Insight 后,通过 “Project” -> “New Project” 创建一个新项目,然后将解压后的 Linux 内核源码目录添加到项目中,Source Insight 会自动解析源码,生成符号数据库,便于后续的代码浏览和分析。
在 Linux 内核源码中,调度器相关的关键文件主要位于kernel/sched目录下。其中,core.c文件包含了调度器的核心逻辑,如调度入口函数schedule等;fair.c实现了完全公平调度器(CFS)的相关代码,包括 CFS 调度类的各种操作函数;rt.c则主要负责实时调度相关的实现,处理实时进程的调度逻辑。这些文件相互协作,共同实现了 Linux 调度器的各种功能 。
3.2核心数据结构剖析
①运行队列(rq)
运行队列是调度器管理进程的关键数据结构之一,每个 CPU 核心都有一个对应的运行队列。在kernel/sched/sched.h文件中可以找到其定义。它包含了当前正在运行的任务指针curr,用于指向当前在该 CPU 上执行的进程;nr_running记录了就绪队列中处于可运行状态的任务数量,调度器可以通过这个值快速了解当前 CPU 的负载情况。
例如,在多核系统中,每个 CPU 核心的运行队列可以独立管理自己的就绪进程,当某个 CPU 核心的nr_running值较高时,说明该核心的负载较重,可能需要进行负载均衡操作,将部分进程迁移到其他负载较轻的 CPU 核心上。运行队列还包含了不同调度类的队列,如完全公平调度队列cfs和实时调度队列rt,这种设计使得不同类型的进程可以按照各自的调度策略进行管理和调度。
②调度类(sched_class)
调度类是一个抽象的概念,它为不同类型的进程提供了不同的调度策略接口。在include/linux/sched.h中定义,每个调度类都有一系列的函数指针,用于实现将任务添加到就绪队列(enqueue_task)、从就绪队列移除任务(dequeue_task)、检查当前任务是否需要被抢占(check_preempt_curr)、选择下一个要运行的任务(pick_next_task)等操作。
以完全公平调度类fair_sched_class为例,它主要用于普通进程的调度,通过实现这些函数指针,实现了基于虚拟运行时间的公平调度算法。当一个新的普通进程进入就绪状态时,会调用fair_sched_class的enqueue_task函数将其添加到 CFS 就绪队列中;当需要选择下一个运行的普通进程时,会调用pick_next_task函数从 CFS 就绪队列中选择虚拟运行时间最小的进程。
③调度实体(sched_entity)
调度实体是连接调度器与进程的桥梁,每个进程都有一个对应的调度实体。在include/linux/sched.h中定义,它包含了进程的调度相关参数,如负载权重load,用于计算进程在调度时的相对优先级;虚拟运行时间vruntime,这是 CFS 调度算法的核心参数,进程的vruntime会随着其占用 CPU 时间的增加而增长,增长速度与进程的权重成反比。
比如,有两个进程 A 和 B,进程 A 的权重较高,进程 B 的权重较低。在运行过程中,进程A的vruntime增长相对较慢,这意味着它在相同的物理时间内,能获得更多的 CPU 执行时间,从而实现了公平调度。调度实体还包含红黑树节点run_node,用于将调度实体组织成红黑树结构,方便调度器快速查找和管理就绪队列中的进程 。
这些核心数据结构相互关联,运行队列通过调度类来管理不同类型的调度实体,调度类通过操作调度实体来实现具体的调度策略,它们共同构成了 Linux 调度器高效运行的基础。
3.3调度入口函数详解
调度入口函数schedule是整个调度流程的起点,定义在kernel/sched/core.c文件中 。当系统需要进行进程调度时,无论是因为时间片耗尽、进程主动放弃 CPU 还是有更高优先级的进程就绪等原因,最终都会直接或间接调用schedule函数。
复制
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);
}
EXPORT_SYMBOL(schedule);1.2.3.4.5.6.7.8.9.10.11.12.
首先,schedule函数获取当前任务结构体的指针tsk,这里的current是一个宏,指向当前正在运行的进程。然后,通过sched_submit_work函数将当前任务提交到调度工作队列中,这一步主要是为了处理一些与任务相关的工作,比如更新任务的一些状态信息等。
接下来,进入一个循环,在循环中,首先调用preempt_disable禁用抢占,这是为了保证在调度过程中,不会被其他高优先级的抢占事件打断,确保调度操作的原子性。然后调用实际的调度函数__schedule,并传入调度策略参数SM_NONE,__schedule函数负责执行具体的调度操作,包括选择下一个要运行的进程以及进行进程上下文切换等关键步骤。
完成调度操作后,调用sched_preempt_enable_no_resched启用抢占,但不进行重新调度,这是因为在调度过程中,可能已经处理了所有需要调度的情况,此时不需要立即再次调度。
循环会一直执行,直到need_resched函数返回假,即表示没有需要重新调度的任务为止。最后,通过sched_update_worker函数更新工作队列中任务的状态,确保任务的状态信息与实际的调度情况一致 。
schedule函数作为调度入口,它有条不紊地协调了各个调度步骤,为系统的进程调度提供了一个统一的入口点,使得整个调度流程能够有序地进行,保证了系统中进程对 CPU 资源的合理分配和高效利用。
3.4选择下一个进程的实现逻辑
在调度过程中,选择下一个要执行的进程是关键步骤之一,这主要由pick_next_task等函数来完成。pick_next_task函数定义在kernel/sched/core.c中,其实现逻辑较为复杂,涉及到多个调度类和调度策略的协同工作。
复制
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;
if (likely(!sched_class_above(prev->sched_class, &fair_sched_class) && rq->nr_running == rq->cfs.h_nr_running)) {
p = pick_next_task_fair(rq, prev, rf);
if (unlikely(p == RETRY_TASK))
goto restart;
if (!p) {
put_prev_task(rq, prev);
p = pick_next_task_idle(rq);
}
return p;
}
restart:
put_prev_task_balance(rq, prev, rf);
for_each_class(class) {
p = class->pick_next_task(rq);
if (p)
return p;
}
BUG();
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.
首先,函数会进行一个优化判断。如果前一个任务是公平调度类中的任务,且运行队列中的任务数与 CFS 队列中的任务数相等,这意味着当前运行队列中的任务全部属于公平调度类,此时可以直接调用pick_next_task_fair函数从 CFS 队列中选择下一个公平调度类任务。pick_next_task_fair函数在kernel/sched/fair.c中定义,它主要根据调度实体的虚拟运行时间vruntime来选择下一个运行的任务,总是选择vruntime最小的任务,以实现公平调度。
如果在调用pick_next_task_fair函数时选择任务失败(返回RETRY_TASK),则跳转到restart标签处,重新进行任务选择。如果pick_next_task_fair函数返回空指针,表示 CFS 队列中没有可运行的任务,此时会调用put_prev_task函数将前一个任务放回队列,并调用pick_next_task_idle函数选择下一个空转调度类任务,空转调度类任务通常在系统空闲时运行,以避免 CPU 空闲浪费。
如果不满足上述优化条件,即运行队列中存在其他调度类的任务,函数会通过put_prev_task_balance函数将前一个任务放回队列进行重新平衡。然后,通过for_each_class宏遍历所有调度类,依次调用每个调度类的pick_next_task函数来选择下一个任务。如果在遍历过程中找到了可运行的任务,则返回该任务;如果遍历完所有调度类都没有找到可运行的任务,会触发BUG,因为系统中应该始终存在可运行的任务(至少有空转任务) 。
pick_next_task函数通过这种复杂而有序的逻辑,能够根据系统中不同调度类任务的状态和优先级,准确地选择出下一个要执行的进程,保证了调度的公平性和高效性,满足了不同类型进程对 CPU 资源的需求。
3.5进程切换的底层实现
进程切换是调度器实现多任务并发执行的关键操作,它使得 CPU 能够在不同进程之间快速切换,实现多个进程看似同时运行的效果。在 Linux 调度器中,进程切换主要由context_switch函数完成,该函数定义在kernel/sched/core.c中。
复制
static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next, unsigned long *switch_count)
{
prepare_task_switch(rq, prev, next);
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);
/*
* kernel -> kernel lazy + transfer active
* user -> kernel lazy + mmgrab() active
*
* kernel -> user switch + mmdrop() active
* user -> user switch
*/
if (!next->mm) { /* 1 */
enter_lazy_tlb(prev->active_mm, next);
next->active_mm = prev->active_mm;
if (prev->mm) /* 2 */
mmdrop(prev->mm);
} else {
membarrier_switch_mm(rq, prev->active_mm, next->mm);
enter_lazy_tlb(next->mm, next);
next->active_mm = next->mm;
if (prev->mm)
mmdrop(prev->mm);
}
rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
rq->skip_clock_update = 0;
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
barrier();
sched_preempt_enable_no_resched();
finish_task_switch(prev, next);
}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.
context_switch函数首先调用prepare_task_switch函数进行一些切换前的准备工作,比如更新一些与任务相关的状态信息等。然后,调用arch_start_context_switch函数,这个函数主要是与体系结构相关的操作,用于启动上下文切换的一些底层准备工作,不同的硬件体系结构(如 x86、ARM 等)会有不同的实现。
接下来,根据切换的目标进程next是否有自己的内存管理结构体mm来进行不同的处理。如果next没有自己的mm(例如内核线程,它共享内核空间,没有独立的用户空间内存映射),则执行enter_lazy_tlb函数进入延迟 TLB(Translation Lookaside Buffer,地址转换后备缓冲器)模式,将next的活动内存管理结构体active_mm设置为prev的active_mm,并在prev有自己的mm时调用mmdrop函数减少prev内存管理结构体的引用计数。
如果next有自己的mm,则先调用membarrier_switch_mm函数处理内存屏障相关的操作,然后进入延迟 TLB 模式,将next的active_mm设置为它自己的mm,同样在prev有mm时调用mmdrop函数。
完成内存相关的切换操作后,调用switch_to函数进行真正的寄存器状态和栈的切换。switch_to函数是一个非常底层的函数,它负责保存当前进程(prev)的寄存器状态和栈指针,然后恢复下一个进程(next)的寄存器状态和栈指针,使得 CPU 能够从next进程的上次执行点继续执行。
在switch_to函数执行完成后,通过barrier函数提供内存屏障,确保内存操作的顺序性。最后,调用sched_preempt_enable_no_resched函数启用抢占但不进行重新调度,并调用finish_task_switch函数完成任务切换的后续工作,比如更新一些统计信息等 。
context_switch函数通过这一系列复杂而精细的操作,实现了进程在 CPU 上的高效切换,保证了多任务系统中各个进程能够快速、有序地交替执行,是 Linux 调度器实现多任务并发的重要基础。
四、调度器初始化
通过代码说明调度器在系统启动初始化阶段是如何初始化和工作的
4.1 init_task和init进程
当linux启动时,最先会通过汇编代码进行硬件和CPU的初始化,最后会跳转到C代码,而最初跳转到的C代码入口为:
复制
/* 代码地址:linux/init/Main.c */
asmlinkage __visible void __init start_kernel(void)1.2.
在start_kerenl函数中,进行了系统启动过程中几乎所有重要的初始化(有一部分在boot中初始化,有一部分在start_kernel之前的汇编代码进行初始化),包括内存、页表、必要数据结构、信号、调度器、硬件设备等。而这些初始化是由谁来负责的?就是由init_task这个进程。
init_task是静态定义的一个进程,也就是说当内核被放入内存时,它就已经存在,它没有自己的用户空间,一直处于内核空间中运行,并且也只处于内核空间运行。当它执行到最后,将start_kernel中所有的初始化执行完成后,会在内核中启动一个kernel_init内核线程和一个kthreadd内核线程,kernel_init内核线程执行到最后会通过execve系统调用执行转变为我们所熟悉的init进程,而kthreadd内核线程是内核用于管理调度其他的内核线程的守护线程。在最后init_task将变成一个idle进程,用于在CPU没有进程运行时运行它,它在此时仅仅用于空转。
4.2 sched_init
在start_kernel中对调度器进行初始化的函数就是sched_init,其主要工作为:
对相关数据结构分配内存初始化root_task_group初始化每个CPU的rq队列(包括其中的cfs队列和实时进程队列)将init_task进程转变为idle进程
需要说明的是init_task在这里会被转变为idle进程,但是它还会继续执行初始化工作,相当于这里只是给init_task挂个idle进程的名号,它其实还是init_task进程,只有到最后init_task进程开启了kernel_init和kthreadd进程之后,才转变为真正意义上的idle进程。
复制
/* 代码路径:内核源代码目录/kernel/sched/Core.c */
/* 执行到此时内核只有一个进程init_task,current就为init_task。之后的init进程在初始化到最后的rest_init中启动 */
void __init sched_init(void)
{
int i, j;
unsigned long alloc_size = 0, ptr;
/* 计算所需要分配的数据结构空间 */
#ifdef CONFIG_FAIR_GROUP_SCHED
alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
#ifdef CONFIG_RT_GROUP_SCHED
alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
#ifdef CONFIG_CPUMASK_OFFSTACK
alloc_size += num_possible_cpus() * cpumask_size();
#endif
if (alloc_size) {
/* 分配内存 */
ptr = (unsigned long)kzalloc(alloc_size, GFP_NOWAIT);
#ifdef CONFIG_FAIR_GROUP_SCHED
/* 设置 root_task_group 每个CPU上的调度实体指针se */
root_task_group.se = (struct sched_entity **)ptr;
ptr += nr_cpu_ids * sizeof(void **);
/* 设置 root_task_group 每个CPU上的CFS运行队列指针cfs_rq */
root_task_group.cfs_rq = (struct cfs_rq **)ptr;
ptr += nr_cpu_ids * sizeof(void **);
#endif /* CONFIG_FAIR_GROUP_SCHED */
#ifdef CONFIG_RT_GROUP_SCHED
/* 设置 root_task_group 每个CPU上的实时调度实体指针se */
root_task_group.rt_se = (struct sched_rt_entity **)ptr;
ptr += nr_cpu_ids * sizeof(void **);
/* 设置 root_task_group 每个CPU上的实时运行队列指针rt_rq */
root_task_group.rt_rq = (struct rt_rq **)ptr;
ptr += nr_cpu_ids * sizeof(void **);
#endif /* CONFIG_RT_GROUP_SCHED */
#ifdef CONFIG_CPUMASK_OFFSTACK
for_each_possible_cpu(i) {
per_cpu(load_balance_mask, i) = (void *)ptr;
ptr += cpumask_size();
}
#endif /* CONFIG_CPUMASK_OFFSTACK */
}
/* 初始化实时进程的带宽限制,用于设置实时进程在CPU中所占用比的 */
init_rt_bandwidth(&def_rt_bandwidth,
global_rt_period(), global_rt_runtime());
init_dl_bandwidth(&def_dl_bandwidth,
global_rt_period(), global_rt_runtime());
#ifdef CONFIG_SMP
/* 初始化默认的调度域,调度域包含一个或多个CPU,负载均衡是在调度域内执行的,相互之间隔离 */
init_defrootdomain();
#endif
#ifdef CONFIG_RT_GROUP_SCHED
/* 初始化实时进程的带宽限制,用于设置实时进程在CPU中所占用比的 */
init_rt_bandwidth(&root_task_group.rt_bandwidth,
global_rt_period(), global_rt_runtime());
#endif /* CONFIG_RT_GROUP_SCHED */
#ifdef CONFIG_CGROUP_SCHED
/* 将分配好空间的 root_task_group 加入 task_groups 链表 */
list_add(&root_task_group.list, &task_groups);
INIT_LIST_HEAD(&root_task_group.children);
INIT_LIST_HEAD(&root_task_group.siblings);
/* 自动分组初始化,每个tty(控制台)动态的创建进程组,这样就可以降低高负载情况下的桌面延迟 */
autogroup_init(&init_task);
#endif /* CONFIG_CGROUP_SCHED */
/* 遍历设置每一个CPU */
for_each_possible_cpu(i) {
struct rq *rq;
/* 获取CPUi的rq队列 */
rq = cpu_rq(i);
/* 初始化rq队列的自旋锁 */
raw_spin_lock_init(&rq->lock);
/* CPU运行队列中调度实体(sched_entity)数量为0 */
rq->nr_running = 0;
/* CPU负载 */
rq->calc_load_active = 0;
/* 负载下次更新时间 */
rq->calc_load_update = jiffies + LOAD_FREQ;
/* 初始化CFS运行队列 */
init_cfs_rq(&rq->cfs);
/* 初始化实时进程运行队列 */
init_rt_rq(&rq->rt, rq);
init_dl_rq(&rq->dl, rq);
#ifdef CONFIG_FAIR_GROUP_SCHED
root_task_group.shares = ROOT_TASK_GROUP_LOAD;
INIT_LIST_HEAD(&rq->leaf_cfs_rq_list);
/*
* How much cpu bandwidth does root_task_group get?
*
* In case of task-groups formed thr the cgroup filesystem, it
* gets 100% of the cpu resources in the system. This overall
* system cpu resource is divided among the tasks of
* root_task_group and its child task-groups in a fair manner,
* based on each entitys (task or task-groups) weight
* (se->load.weight).
*
* In other words, if root_task_group has 10 tasks of weight
* 1024) and two child groups A0 and A1 (of weight 1024 each),
* then A0s share of the cpu resource is:
*
* A0s bandwidth = 1024 / (10*1024 + 1024 + 1024) = 8.33%
*
* We achieve this by letting root_task_groups tasks sit
* directly in rq->cfs (i.e root_task_group->se[] = NULL).
*/
/* 初始化CFS的带宽限制,用于设置普通进程在CPU中所占用比的 */
init_cfs_bandwidth(&root_task_group.cfs_bandwidth);
init_tg_cfs_entry(&root_task_group, &rq->cfs, NULL, i, NULL);
#endif /* CONFIG_FAIR_GROUP_SCHED */
rq->rt.rt_runtime = def_rt_bandwidth.rt_runtime;
#ifdef CONFIG_RT_GROUP_SCHED
init_tg_rt_entry(&root_task_group, &rq->rt, NULL, i, NULL);
#endif
/* 初始化该队列所保存的每个CPU的负载情况 */
for (j = 0; j < CPU_LOAD_IDX_MAX; j++)
rq->cpu_load[j] = 0;
/* 该队列最后一次更新cpu_load的时间值为当前 */
rq->last_load_update_tick = jiffies;
#ifdef CONFIG_SMP
/* 这些参数都是负载均衡使用的 */
rq->sd = NULL;
rq->rd = NULL;
rq->cpu_capacity = SCHED_CAPACITY_SCALE;
rq->post_schedule = 0;
rq->active_balance = 0;
rq->next_balance = jiffies;
rq->push_cpu = 0;
rq->cpu = i;
rq->online = 0;
rq->idle_stamp = 0;
rq->avg_idle = 2*sysctl_sched_migration_cost;
rq->max_idle_balance_cost = sysctl_sched_migration_cost;
INIT_LIST_HEAD(&rq->cfs_tasks);
/* 将CPU运行队列加入到默认调度域中 */
rq_attach_root(rq, &def_root_domain);
#ifdef CONFIG_NO_HZ_COMMON
/* 动态时钟使用的标志位,初始时动态时钟是不使用的 */
rq->nohz_flags = 0;
#endif
#ifdef CONFIG_NO_HZ_FULL
/* 也是动态时钟才使用的标志位,用于保存上次调度tick发生时间 */
rq->last_sched_tick = 0;
#endif
#endif
/* 初始化运行队列定时器,这个是高精度定时器,但是只是初始化,这时并没有使用 */
init_rq_hrtick(rq);
atomic_set(&rq->nr_iowait, 0);
}
/* 设置 init_task 进程的权重 */
set_load_weight(&init_task);
#ifdef CONFIG_PREEMPT_NOTIFIERS
/* 初始化通知链 */
INIT_HLIST_HEAD(&init_task.preempt_notifiers);
#endif
/*
* The boot idle thread does lazy MMU switching as well:
*/
atomic_inc(&init_mm.mm_count);
enter_lazy_tlb(&init_mm, current);
/*
* Make us the idle thread. Technically, schedule() should not be
* called from this thread, however somewhere below it might be,
* but because we are the idle thread, we just pick up running again
* when this runqueue becomes "idle".
*/
/* 将当前进程初始化为idle进程,idle进程用于当CPU没有进程可运行时运行,空转 */
init_idle(current, smp_processor_id());
/* 下次负载更新时间(是一个相对时间) */
calc_load_update = jiffies + LOAD_FREQ;
/*
* During early bootup we pretend to be a normal task:
*/
/* 设置idle进程使用CFS调度策略 */
current->sched_class = &fair_sched_class;
#ifdef CONFIG_SMP
zalloc_cpumask_var(&sched_domains_tmpmask, GFP_NOWAIT);
/* May be allocated at isolcpus cmdline parse time */
if (cpu_isolated_map == NULL)
zalloc_cpumask_var(&cpu_isolated_map, GFP_NOWAIT);
idle_thread_set_boot_cpu();
set_cpu_rq_start_time();
#endif
init_sched_fair_class();
/* 这里只是标记调度器开始运行了,但是此时系统只有一个init_task(idle)进程,并且定时器都还没启动。并不会调度到其他进程,也没有其他进程可供调度 */
scheduler_running = 1;
}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.188.189.190.191.192.193.194.195.196.197.198.199.200.201.202.203.204.205.
五、调度器加入时机
只有处于TASK_RUNNING状态下的进程才能够加入到调度器,其他状态都不行,也就说明了,当一个进程处于睡眠、挂起状态的时候是不存在于调度器中的,而进程加入调度器的时机如下:
当进程创建完成时,进程刚创建完成时,即使它运行起来立即调用sleep()进程睡眠,它也必定先会加入到调度器,因为实际上它加入调度器后自己还需要进行一定的初始化和操作,才会调用到我们的“立即”sleep()。当进程被唤醒时,也使用sleep的例子说明,我们平常写程序使用的sleep()函数实现原理就是通过系统调用将进程状态改为TASK_INTERRUPTIBLE,然后移出运行队列,并且启动一个定时器,在定时器到期后唤醒进程,再重新放入运行队列。
5.1sched_fork
copy_process()这个创建函数,而里面有一个函数专门用于进程调度的初始化,就是sched_fork(),其代码如下:
复制
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
unsigned long flags;
/* 获取当前CPU,并且禁止抢占 */
int cpu = get_cpu();
/* 初始化跟调度相关的值,比如调度实体,运行时间等 */
__sched_fork(clone_flags, p);
/*
* 标记为运行状态,表明此进程正在运行或准备好运行,实际上没有真正在CPU上运行,这里只是导致了外部信号和事件不能够唤醒此进程,之后将它插入到运行队列中
*/
p->state = TASK_RUNNING;
/*
* 根据父进程的运行优先级设置设置进程的优先级
*/
p->prio = current->normal_prio;
/*
* 更新该进程优先级
*/
/* 如果需要重新设置优先级 */
if (unlikely(p->sched_reset_on_fork)) {
/* 如果是dl调度或者实时调度 */
if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
/* 调度策略为SCHED_NORMAL,这个选项将使用CFS调度 */
p->policy = SCHED_NORMAL;
/* 根据默认nice值设置静态优先级 */
p->static_prio = NICE_TO_PRIO(0);
/* 实时优先级为0 */
p->rt_priority = 0;
} else if (PRIO_TO_NICE(p->static_prio) < 0)
/* 根据默认nice值设置静态优先级 */
p->static_prio = NICE_TO_PRIO(0);
/* p->prio = p->normal_prio = p->static_prio */
p->prio = p->normal_prio = __normal_prio(p);
/* 设置进程权重 */
set_load_weight(p);
/* sched_reset_on_fork成员在之后已经不需要使用了,直接设为0 */
p->sched_reset_on_fork = 0;
}
if (dl_prio(p->prio)) {
/* 使能抢占 */
put_cpu();
/* 返回错误 */
return -EAGAIN;
} else if (rt_prio(p->prio)) {
/* 根据优先级判断,如果是实时进程,设置其调度类为rt_sched_class */
p->sched_class = &rt_sched_class;
} else {
/* 如果是普通进程,设置其调度类为fair_sched_class */
p->sched_class = &fair_sched_class;
}
/* 调用调用类的task_fork函数 */
if (p->sched_class->task_fork)
p->sched_class->task_fork(p);
/*
* The child is not yet in the pid-hash so no cgroup attach races,
* and the cgroup is pinned to this child due to cgroup_fork()
* is ran before sched_fork().
*
* Silence PROVE_RCU.
*/
raw_spin_lock_irqsave(&p->pi_lock, flags);
/* 设置新进程的CPU为当前CPU */
set_task_cpu(p, cpu);
raw_spin_unlock_irqrestore(&p->pi_lock, flags);
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
if (likely(sched_info_on()))
memset(&p->sched_info, 0, sizeof(p->sched_info));
#endif
#if defined(CONFIG_SMP)
p->on_cpu = 0;
#endif
/* task_thread_info(p)->preempt_count = PREEMPT_DISABLED; */
/* 初始化该进程为内核禁止抢占 */
init_task_preempt_count(p);
#ifdef CONFIG_SMP
plist_node_init(&p->pushable_tasks, MAX_PRIO);
RB_CLEAR_NODE(&p->pushable_dl_tasks);
#endif
/* 使能抢占 */
put_cpu();
return 0;
}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.
在sched_fork()函数中,主要工作如下:
获取当前CPU号禁止内核抢占(这里基本就是关闭了抢占,因为执行到这里已经是内核态,又禁止了被抢占)初始化进程p的一些变量(实时进程和普通进程通用的那些变量)设置进程p的状态为TASK_RUNNING(这一步很关键,因为只有处于TASK_RUNNING状态下的进程才会被调度器放入队列中)根据父进程和clone_flags参数设置进程p的优先级和权重。根据进程p的优先级设置其调度类(实时进程优先级:0~99 普通进程优先级:100~139)根据调度类进行进程p类型相关的初始化(这里就实现了实时进程和普通进程独有的变量进行初始化)设置进程p的当前CPU为此CPU。初始化进程p禁止内核抢占(因为当CPU执行到进程p时,进程p还需要进行一些初始化)使能内核抢占
可以看出sched_fork()进行的初始化也比较简单,需要注意的是不同类型的进程会使用不同的调度类,并且也会调用调度类中的初始化函数。在实时进程的调度类中是没有特定的task_fork()函数的,而普通进程使用cfs策略时会调用到task_fork_fair()函数,我们具体看看实现:
复制
static void task_fork_fair(struct task_struct *p)
{
struct cfs_rq *cfs_rq;
/* 进程p的调度实体se */
struct sched_entity *se = &p->se, *curr;
/* 获取当前CPU */
int this_cpu = smp_processor_id();
/* 获取此CPU的运行队列 */
struct rq *rq = this_rq();
unsigned long flags;
/* 上锁并保存中断记录 */
raw_spin_lock_irqsave(&rq->lock, flags);
/* 更新rq运行时间 */
update_rq_clock(rq);
/* cfs_rq = current->se.cfs_rq; */
cfs_rq = task_cfs_rq(current);
/* 设置当前进程所在队列为父进程所在队列 */
curr = cfs_rq->curr;
/*
* Not only the cpu but also the task_group of the parent might have
* been changed after parent->se.parent,cfs_rq were copied to
* child->se.parent,cfs_rq. So call __set_task_cpu() to make those
* of child point to valid ones.
*/
rcu_read_lock();
/* 设置此进程所属CPU */
__set_task_cpu(p, this_cpu);
rcu_read_unlock();
/* 更新当前进程运行时间 */
update_curr(cfs_rq);
if (curr)
/* 将父进程的虚拟运行时间赋给了新进程的虚拟运行时间 */
se->vruntime = curr->vruntime;
/* 调整了se的虚拟运行时间 */
place_entity(cfs_rq, se, 1);
if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
/*
* Upon rescheduling, sched_class::put_prev_task() will place
* current within the tree based on its new key value.
*/
swap(curr->vruntime, se->vruntime);
resched_curr(rq);
}
/* 保证了进程p的vruntime是运行队列中最小的(这里占时不确定是不是这个用法,不过确实是最小的了) */
se->vruntime -= cfs_rq->min_vruntime;
/* 解锁,还原中断记录 */
raw_spin_unlock_irqrestore(&rq->lock, 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.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.
在task_fork_fair()函数中主要就是设置进程p的虚拟运行时间和所处的cfs队列,值得我们注意的是 cfs_rq = task_cfs_rq(current); 这一行,在注释中已经表明task_cfs_rq(current)返回的是current的se.cfs_rq,注意se.cfs_rq保存的并不是根cfs队列,而是所处的cfs_rq,也就是如果父进程处于一个进程组的cfs_rq中,新创建的进程也会处于这个进程组的cfs_rq中。
5.2 wake_up_new_task()
到这里新进程关于调度的初始化已经完成,但是还没有被调度器加入到队列中,其是在do_fork()中的wake_up_new_task(p);中加入到队列中的,我们具体看看wake_up_new_task()的实现:
复制
void wake_up_new_task(struct task_struct *p)
{
unsigned long flags;
struct rq *rq;
raw_spin_lock_irqsave(&p->pi_lock, flags);
#ifdef CONFIG_SMP
/*
* Fork balancing, do it here and not earlier because:
* - cpus_allowed can change in the fork path
* - any previously selected cpu might disappear through hotplug
*/
/* 为进程选择一个合适的CPU */
set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
#endif
/* Initialize new tasks runnable average */
/* 这里是跟多核负载均衡有关 */
init_task_runnable_average(p);
/* 上锁 */
rq = __task_rq_lock(p);
/* 将进程加入到CPU的运行队列 */
activate_task(rq, p, 0);
/* 标记进程p处于队列中 */
p->on_rq = TASK_ON_RQ_QUEUED;
/* 跟调试有关 */
trace_sched_wakeup_new(p, true);
/* 检查是否需要切换当前进程 */
check_preempt_curr(rq, p, WF_FORK);
#ifdef CONFIG_SMP
if (p->sched_class->task_woken)
p->sched_class->task_woken(rq, p);
#endif
task_rq_unlock(rq, p, &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.27.28.29.30.31.32.33.34.35.
在wake_up_new_task()函数中,将进程加入到运行队列的函数为activate_task(),而activate_task()函数最后会调用到新进程调度类中的enqueue_task指针所指函数,这里我们具体看一下cfs调度类的enqueue_task指针所指函数enqueue_task_fair():
复制
static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &p->se;
/* 这里是一个迭代,我们知道,进程有可能是处于一个进程组中的,所以当这个处于进程组中的进程加入到该进程组的队列中时,要对此队列向上迭代 */
for_each_sched_entity(se) {
if (se->on_rq)
break;
/* 如果不是CONFIG_FAIR_GROUP_SCHED,获取其所在CPU的rq运行队列的cfs_rq运行队列
* 如果是CONFIG_FAIR_GROUP_SCHED,获取其所在的cfs_rq运行队列
*/
cfs_rq = cfs_rq_of(se);
/* 加入到队列中 */
enqueue_entity(cfs_rq, se, flags);
/*
* end evaluation on encountering a throttled cfs_rq
*
* note: in the case of encountering a throttled cfs_rq we will
* post the final h_nr_running increment below.
*/
if (cfs_rq_throttled(cfs_rq))
break;
cfs_rq->h_nr_running++;
flags = ENQUEUE_WAKEUP;
}
/* 只有se不处于队列中或者cfs_rq_throttled(cfs_rq)返回真才会运行这个循环 */
for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
cfs_rq->h_nr_running++;
if (cfs_rq_throttled(cfs_rq))
break;
update_cfs_shares(cfs_rq);
update_entity_load_avg(se, 1);
}
if (!se) {
update_rq_runnable_avg(rq, rq->nr_running);
/* 当前CPU运行队列活动进程数 + 1 */
add_nr_running(rq, 1);
}
/* 设置下次调度中断发生时间 */
hrtick_update(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.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.
在enqueue_task_fair()函数中又使用了enqueue_entity()函数进行操作,如下:
复制
static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
/*
* Update the normalized vruntime before updating min_vruntime
* through calling update_curr().
*/
if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
se->vruntime += cfs_rq->min_vruntime;
/*
* Update run-time statistics of the current.
*/
/* 更新当前进程运行时间和虚拟运行时间 */
update_curr(cfs_rq);
enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);
/* 更新cfs_rq队列总权重(就是在原有基础上加上se的权重) */
account_entity_enqueue(cfs_rq, se);
update_cfs_shares(cfs_rq);
/* 新建的进程flags为0,不会执行这里 */
if (flags & ENQUEUE_WAKEUP) {
place_entity(cfs_rq, se, 0);
enqueue_sleeper(cfs_rq, se);
}
update_stats_enqueue(cfs_rq, se);
check_spread(cfs_rq, se);
/* 将se插入到运行队列cfs_rq的红黑树中 */
if (se != cfs_rq->curr)
__enqueue_entity(cfs_rq, se);
/* 将se的on_rq标记为1 */
se->on_rq = 1;
/* 如果cfs_rq的队列中只有一个进程,这里做处理 */
if (cfs_rq->nr_running == 1) {
list_add_leaf_cfs_rq(cfs_rq);
check_enqueue_throttle(cfs_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.28.29.30.31.32.33.34.35.36.37.38.39.40.41.
六、调度器如何运行?
6.1系统定时器
因为我们主要讲解的是调度器,而会涉及到一些系统定时器的知识,这里我们简单讲解一下内核中定时器是如何组织,又是如何通过通过定时器实现了调度器的间隔调度。首先我们先看一下内核定时器的框架:
图片
在内核中,会使用strut clock_event_device结构描述硬件上的定时器,每个硬件定时器都有其自己的精度,会根据精度每隔一段时间产生一个时钟中断。
而系统会让每个CPU使用一个tick_device描述系统当前使用的硬件定时器(因为每个CPU都有其自己的运行队列),通过tick_device所使用的硬件时钟中断进行时钟滴答(jiffies)的累加(只会有一个CPU负责这件事),并且在中断中也会调用调度器,而我们在驱动中常用的低精度定时器就是通过判断jiffies实现的。而当使用高精度定时器(hrtimer)时,情况则不一样,hrtimer会生成一个普通的高精度定时器,在这个定时器中回调函数是调度器,其设置的间隔时间同时钟滴答一样,所以在系统中,每一次时钟滴答都会使调度器判断一次是否需要进行调度。
6.2时钟中断
当时钟发生中断时,首先会调用的是tick_handle_periodic()函数,在此函数中又主要执行tick_periodic()函数进行操作。我们先看一下tick_handle_periodic()函数:
复制
void tick_handle_periodic(struct clock_event_device *dev)
{
/* 获取当前CPU */
int cpu = smp_processor_id();
/* 获取下次时钟中断执行时间 */
ktime_t next = dev->next_event;
tick_periodic(cpu);
/* 如果是周期触发模式,直接返回 */
if (dev->mode != CLOCK_EVT_MODE_ONESHOT)
return;
/* 为了防止当该函数被调用时,clock_event_device中的计时实际上已经经过了不止一个tick周期,这时候,tick_periodic可能被多次调用,使得jiffies和时间可以被正确地更新。*/
for (;;) {
/*
* Setup the next period for devices, which do not have
* periodic mode:
*/
/* 计算下一次触发时间 */
next = ktime_add(next, tick_period);
/* 设置下一次触发时间,返回0表示成功 */
if (!clockevents_program_event(dev, next, false))
return;
/*
* Have to be careful here. If were in oneshot mode,
* before we call tick_periodic() in a loop, we need
* to be sure were using a real hardware clocksource.
* Otherwise we could get trapped in an infinite(无限的)
* loop, as the tick_periodic() increments jiffies,
* which then will increment time, possibly causing
* the loop to trigger again and again.
*/
if (timekeeping_valid_for_hres())
tick_periodic(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.
此函数主要工作是执行tick_periodic()函数,然后判断时钟中断是单触发模式还是循环触发模式,如果是循环触发模式,则直接返回,如果是单触发模式,则执行如下操作:
计算下一次触发时间设置下次触发时间如果设置下次触发时间失败,则根据timekeeper等待下次tick_periodic()函数执行时间。返回第一步
而在tick_periodic()函数中,程序主要执行路线为tick_periodic()->update_process_times()->scheduler_tick()。最后的scheduler_tick()函数则是跟调度相关的主要函数。我们在这具体先看看tick_periodic()函数和update_process_times()函数:
复制
/* tick_device 周期性调用此函数
* 更新jffies和当前进程
* 只有一个CPU是负责更新jffies的,其他的CPU只会更新当前自己的进程
*/
static void tick_periodic(int cpu)
{
if (tick_do_timer_cpu == cpu) {
/* 当前CPU负责更新时间 */
write_seqlock(&jiffies_lock);
/* Keep track of the next tick event */
tick_next_period = ktime_add(tick_next_period, tick_period);
/* 更新 jiffies计数,jiffies += 1 */
do_timer(1);
write_sequnlock(&jiffies_lock);
/* 更新墙上时间,就是我们生活中的时间 */
update_wall_time();
}
/* 更新当前进程信息,调度器主要函数 */
update_process_times(user_mode(get_irq_regs()));
profile_tick(CPU_PROFILING);
}
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id();
/* Note: this timer irq context must be accounted for as well. */
/* 更新当前进程的内核态和用户态占用率 */
account_process_tick(p, user_tick);
/* 检查有没有定时器到期,有就运行到期定时器的处理 */
run_local_timers();
rcu_check_callbacks(cpu, user_tick);
#ifdef CONFIG_IRQ_WORK
if (in_irq())
irq_work_tick();
#endif
/* 调度器的tick */
scheduler_tick();
run_posix_cpu_timers(p);
}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.
这两个函数主要工作为将jiffies加1、更新系统的墙上时间、更新当前进程的内核态和用户态的CPU占用率、检查是否有定时器到期,运行到期的定时器。当执行完这些操作后,就到了最重要的scheduler_tick()函数,而scheduler_tick()函数主要做什么呢,就是更新CPU和当前进行的一些数据,然后根据当前进程的调度类,调用task_tick()函数。这里普通进程调度类的task_tick()是task_tick_fair()函数。
复制
void scheduler_tick(void)
{
/* 获取当前CPU的ID */
int cpu = smp_processor_id();
/* 获取当前CPU的rq队列 */
struct rq *rq = cpu_rq(cpu);
/* 获取当前CPU的当前运行程序,实际上就是current */
struct task_struct *curr = rq->curr;
/* 更新CPU调度统计中的本次调度时间 */
sched_clock_tick();
raw_spin_lock(&rq->lock);
/* 更新该CPU的rq运行时间 */
update_rq_clock(rq);
curr->sched_class->task_tick(rq, curr, 0);
/* 更新CPU的负载 */
update_cpu_load_active(rq);
raw_spin_unlock(&rq->lock);
perf_event_task_tick();
#ifdef CONFIG_SMP
rq->idle_balance = idle_cpu(cpu);
trigger_load_balance(rq);
#endif
/* rq->last_sched_tick = jiffies; */
rq_last_tick_reset(rq);
}
/*
* CFS调度类的task_tick()
*/
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);
}
if (numabalancing_enabled)
task_tick_numa(rq, curr);
update_rq_runnable_avg(rq, 1);
}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.
显然,到这里最重要的函数应该是entity_tick(),因为是这个函数决定了当前进程是否需要调度出去。我们必须先明确一点就是,CFS调度策略是使用红黑树以进程的vruntime为键值进行组织的,进程的vruntime越小越在红黑树的左边,而每次调度的下一个目标就是红黑树最左边的结点上的进程。
而当进行运行时,其vruntime是随着实际运行时间而增加的,但是不同权重的进程其vruntime增加的速率不同,正在运行的进程的权重约大(优先级越高),其vruntime增加的速率越慢,所以其所占用的CPU时间越多。而每次时钟中断的时候,在entity_tick()函数中都会更新当前进程的vruntime值。当进程没有处于CPU上运行时,其vruntime是保持不变的。
复制
static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
/*
* Update run-time statistics of the current.
*/
/* 更新当前进程运行时间,包括虚拟运行时间 */
update_curr(cfs_rq);
/*
* Ensure that runnable average is periodically updated.
*/
update_entity_load_avg(curr, 1);
update_cfs_rq_blocked_load(cfs_rq, 1);
update_cfs_shares(cfs_rq);
#ifdef CONFIG_SCHED_HRTICK
/*
* queued ticks are scheduled to match the slice, so dont bother
* validating it and just reschedule.
*/
/* 若queued为1,则当前运行队列的运行进程需要调度 */
if (queued) {
/* 标记当前进程需要被调度出去 */
resched_curr(rq_of(cfs_rq));
return;
}
/*
* dont let the period tick interfere with the hrtick preemption
*/
if (!sched_feat(DOUBLE_TICK) && hrtimer_active(&rq_of(cfs_rq)->hrtick_timer))
return;
#endif
/* 检查是否需要调度 */
if (cfs_rq->nr_running > 1)
check_preempt_tick(cfs_rq, curr);
}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.
在entity_tick()中,首先会更新当前进程的实际运行时间和虚拟运行时间,这里很重要,因为要使用更新后的这些数据去判断是否需要被调度。在entity_tick()函数中最后面的check_preempt_tick()函数就是用来判断进程是否需要被调度的,其判断的标准有两个:
先判断当前进程的实际运行时间是否超过CPU分配给这个进程的CPU时间,如果超过,则需要调度。再判断当前进程的vruntime是否大于下个进程的vruntime,如果大于,则需要调度。
清楚了这两个标准,check_preempt_tick()的代码则很好理解了。
复制
/*
* 检查当前进程是否需要被抢占
* 判断方法有两种,一种就是判断当前进程是否超过了CPU分配给它的实际运行时间
* 另一种就是判断当前进程的虚拟运行时间是否大于下个进程的虚拟运行时间
*/
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
/* ideal_runtime为进程应该运行的时间
* delta_exec为进程增加的实际运行时间
* 如果delta_exec超过了ideal_runtime,表示该进程应该让出CPU给其他进程
*/
unsigned long ideal_runtime, delta_exec;
struct sched_entity *se;
s64 delta;
/* slice为CFS队列中所有进程运行一遍需要的实际时间 */
/* ideal_runtime保存的是CPU分配给当前进程一个周期内实际的运行时间,计算公式为: 一个周期内进程应当运行的时间 = 一个周期内队列中所有进程运行一遍需要的时间 * 当前进程权重 / 队列总权重
* delta_exec保存的是当前进程增加使用的实际运行时间
*/
ideal_runtime = sched_slice(cfs_rq, curr);
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
if (delta_exec > ideal_runtime) {
/* 增加的实际运行实际 > 应该运行实际,说明需要调度出去 */
resched_curr(rq_of(cfs_rq));
/*
* The current task ran long enough, ensure it doesnt get
* re-elected due to buddy favours.
*/
/* 如果cfs_rq队列的last,next,skip指针中的某个等于当前进程,则清空cfs_rq队列中的相应指针 */
clear_buddies(cfs_rq, curr);
return;
}
/*
* Ensure that a task that missed wakeup preemption by a
* narrow margin doesnt have to wait for a full slice.
* This also mitigates buddy induced latencies under load.
*/
if (delta_exec < sysctl_sched_min_granularity)
return;
/* 获取下一个调度进程的se */
se = __pick_first_entity(cfs_rq);
/* 当前进程的虚拟运行时间 - 下个进程的虚拟运行时间 */
delta = curr->vruntime - se->vruntime;
/* 当前进程的虚拟运行时间 大于 下个进程的虚拟运行时间,说明这个进程还可以继续运行 */
if (delta < 0)
return;
if (delta > ideal_runtime)
/* 当前进程的虚拟运行时间 小于 下个进程的虚拟运行时间,说明下个进程比当前进程更应该被CPU使用,resched_curr()函数用于标记当前进程需要被调度出去 */
resched_curr(rq_of(cfs_rq));
}
/*
* resched_curr - mark rqs 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.
*/
/* 标记当前进程需要调度,将当前进程的thread_info->flags设置TIF_NEED_RESCHED标记 */
void resched_curr(struct rq *rq)
{
struct task_struct *curr = rq->curr;
int cpu;
lockdep_assert_held(&rq->lock);
/* 检查当前进程是否已经设置了调度标志,如果是,则不用再设置一遍,直接返回 */
if (test_tsk_need_resched(curr))
return;
/* 根据rq获取CPU */
cpu = cpu_of(rq);
/* 如果CPU = 当前CPU,则设置当前进程需要调度标志 */
if (cpu == smp_processor_id()) {
/* 设置当前进程需要被调度出去的标志,这个标志保存在进程的thread_info结构上 */
set_tsk_need_resched(curr);
/* 设置CPU的内核抢占 */
set_preempt_need_resched();
return;
}
/* 如果不是处于当前CPU上,则设置当前进程需要调度,并通知其他CPU */
if (set_nr_and_not_polling(curr))
smp_send_reschedule(cpu);
else
trace_sched_wake_idle_without_ipi(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.
好了,到这里实际上如果进程需要被调度,则已经被标记,如果进程不需要被调度,则继续执行。这里大家或许有疑问,只标记了进程需要被调度,但是为什么并没有真正处理它?进程调度的发生时机之一就是发生在中断返回时,这里是在汇编代码中实现的,而我们知道这里我们是时钟中断执行上述的这些操作的,当执行完这些后,从时钟中断返回去的时候,会调用到汇编函数ret_from_sys_call,在这个函数中会先检查调度标志被置位,如果被置位,则跳转至schedule(),而schedule()最后调用到__schedule()这个函数进行处理。
复制
static void __sched __schedule(void)
{
/* prev保存换出进程(也就是当前进程),next保存换进进程 */
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
need_resched:
/* 禁止抢占 */
preempt_disable();
/* 获取当前CPU ID */
cpu = smp_processor_id();
/* 获取当前CPU运行队列 */
rq = cpu_rq(cpu);
rcu_note_context_switch(cpu);
prev = rq->curr;
schedule_debug(prev);
if (sched_feat(HRTICK))
hrtick_clear(rq);
/*
* Make sure that signal_pending_state()->signal_pending() below
* cant be reordered with __set_current_state(TASK_INTERRUPTIBLE)
* done by the caller to avoid the race with signal_wake_up().
*/
smp_mb__before_spinlock();
/* 队列上锁 */
raw_spin_lock_irq(&rq->lock);
/* 当前进程非自愿切换次数 */
switch_count = &prev->nivcsw;
/*
* 当内核抢占时会置位thread_info的preempt_count的PREEMPT_ACTIVE位,调用schedule()之后会清除,PREEMPT_ACTIVE置位表明是从内核抢占进入到此的
* preempt_count()是判断thread_info的preempt_count整体是否为0
* prev->state大于0表明不是TASK_RUNNING状态
*
*/
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
/* 当前进程不为TASK_RUNNING状态并且不是通过内核态抢占进入调度 */
if (unlikely(signal_pending_state(prev->state, prev))) {
/* 有信号需要处理,置为TASK_RUNNING */
prev->state = TASK_RUNNING;
} else {
/* 没有信号挂起需要处理,会将此进程移除运行队列 */
/* 如果代码执行到此,说明当前进程要么准备退出,要么是处于即将睡眠状态 */
deactivate_task(rq, prev, DEQUEUE_SLEEP);
prev->on_rq = 0;
/*
* If a worker went to sleep, notify and ask workqueue
* whether it wants to wake up a task to maintain
* concurrency.
*/
if (prev->flags & PF_WQ_WORKER) {
/* 如果当前进程处于一个工作队列中 */
struct task_struct *to_wakeup;
to_wakeup = wq_worker_sleeping(prev, cpu);
if (to_wakeup)
try_to_wake_up_local(to_wakeup);
}
}
switch_count = &prev->nvcsw;
}
/* 更新rq运行队列时间 */
if (task_on_rq_queued(prev) || rq->skip_clock_update < 0)
update_rq_clock(rq);
/* 获取下一个调度实体,这里的next的值会是一个进程,而不是一个调度组,在pick_next_task会递归选出一个进程 */
next = pick_next_task(rq, prev);
/* 清除当前进程的thread_info结构中的flags的TIF_NEED_RESCHED和PREEMPT_NEED_RESCHED标志位,这两个位表明其可以被调度调出(因为这里已经调出了,所以这两个位就没必要了) */
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
rq->skip_clock_update = 0;
if (likely(prev != next)) {
/* 该CPU进程切换次数加1 */
rq->nr_switches++;
/* 该CPU当前执行进程为新进程 */
rq->curr = next;
++*switch_count;
/* 这里进行了进程上下文的切换 */
context_switch(rq, prev, next); /* unlocks the rq */
/*
* The context switch have flipped the stack from under us
* and restored the local variables which were saved when
* this task called schedule() in the past. prev == current
* is still correct, but it can be moved to another cpu/rq.
*/
/* 新的进程有可能在其他CPU上运行,重新获取一次CPU和rq */
cpu = smp_processor_id();
rq = cpu_rq(cpu);
}
else
raw_spin_unlock_irq(&rq->lock); /* 这里意味着下个调度的进程就是当前进程,释放锁不做任何处理 */
/* 上下文切换后的处理 */
post_schedule(rq);
/* 重新打开抢占使能但不立即执行重新调度 */
sched_preempt_enable_no_resched();
if (need_resched())
goto need_resched;
}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.
在__schedule()中,每一步的作用注释已经写得很详细了,选取下一个进程的任务在__schedule()中交给了pick_next_task()函数,而进程切换则交给了context_switch()函数。我们先看看pick_next_task()函数是如何选取下一个进程的:
复制
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev)
{
const struct sched_class *class = &fair_sched_class;
struct task_struct *p;
/*
* Optimization: we know that if all tasks are in
* the fair class we can call that function directly:
*/
if (likely(prev->sched_class == class && rq->nr_running == rq->cfs.h_nr_running)) {
/* 所有进程都处于CFS运行队列中,所以就直接使用cfs的调度类 */
p = fair_sched_class.pick_next_task(rq, prev);
if (unlikely(p == RETRY_TASK))
goto again;
/* assumes fair_sched_class->next == idle_sched_class */
if (unlikely(!p))
p = idle_sched_class.pick_next_task(rq, prev);
return p;
}
again:
/* 在其他调度类中包含有其他进程,从最高优先级的调度类迭代到最低优先级的调度类,并选择最优的进程运行 */
for_each_class(class) {
p = class->pick_next_task(rq, prev);
if (p) {
if (unlikely(p == RETRY_TASK))
goto again;
return p;
}
}
BUG(); /* the idle class will always have a runnable 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.
在pick_next_task()中完全体现了进程优先级的概念,首先会先判断是否所有进程都处于cfs队列中,如果不是,则表明有比普通进程更高优先级的进程(包括实时进程)。内核中是将调度类重优先级高到低进行排列,然后选择时从最高优先级的调度类开始找是否有进程需要调度,如果没有会转到下一优先级调度类,在代码27行所体现,27行展开是:
复制
#define for_each_class(class) \
for (class = sched_class_highest; class; class = class->next)1.2.
而调度类的优先级顺序为:
复制
stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class1.
在pick_next_task()函数中返回了选定的进程的进程描述符,接下来就会调用context_switch()进行进程切换了。
复制
static inline void
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);
if (!mm) {
/* 如果新进程的内存描述符为空,说明新进程为内核线程 */
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
/* 通知底层不需要切换虚拟地址空间
* if (this_cpu_read(cpu_tlbstate.state) == TLBSTATE_OK)
* this_cpu_write(cpu_tlbstate.state, TLBSTATE_LAZY);
*/
enter_lazy_tlb(oldmm, next);
} else
/* 切换虚拟地址空间 */
switch_mm(oldmm, mm, next);
if (!prev->mm) {
/* 如果被切换出去的进程是内核线程 */
prev->active_mm = NULL;
/* 归还借用的oldmm */
rq->prev_mm = oldmm;
}
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler its an obvious special-case), so we
* do an early lockdep release here:
*/
spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
context_tracking_task_switch(prev, next);
/* 切换寄存器和内核栈,还会重新设置current为切换进去的进程 */
switch_to(prev, next, prev);
/* 同步 */
barrier();
/*
* this_rq must be evaluated again because prev may have moved
* CPUs since it called schedule(), thus the rq on its stack
* frame will be invalid.
*/
finish_task_switch(this_rq(), prev);
}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.
到这里整个进程的选择和切换就已经完成了。
七、调度器实践与应用
7.1基于实际场景的调度策略调整
(1)服务器场景:在 Web 服务器环境中,通常面临大量的并发请求,对响应速度要求极高。例如,一个繁忙的电商网站后台服务器,每秒可能会接收数千个用户的商品查询、订单提交等请求。此时,可以将处理用户请求的进程设置为较高优先级,利用 Linux 的实时调度策略(如 SCHED_FIFO 或 SCHED_RR)。通过chrt命令可以实现这一调整,假设 Web 服务器进程的 PID 为 1234,要将其设置为 SCHED_FIFO 调度策略且优先级为 99(实时优先级范围 1 - 99,数值越大优先级越高),可以执行以下命令:
这样,当有新的用户请求到达时,Web 服务器进程能够优先获得 CPU 资源进行处理,减少用户等待时间,提高用户体验。同时,对于一些后台任务,如日志写入、数据备份等 I/O 密集型进程,可以适当降低其优先级,设置为普通调度策略(SCHED_NORMAL),以避免它们占用过多 CPU 资源,影响前端用户请求的处理速度。
(2)桌面场景:在日常的桌面使用中,用户同时运行着多种应用程序,如文字处理软件、浏览器、音乐播放器等。为了保证用户交互的流畅性,需要优先调度与用户交互密切相关的进程。比如,当用户在进行文字编辑时,文字处理软件的进程应具有较高优先级,确保用户输入的字符能够及时显示和处理。而对于后台自动更新的进程,如软件更新程序、系统备份进程等,可以设置为较低优先级,在系统空闲时再进行执行。在 Linux 系统中,可以通过调整进程的 Nice 值来实现优先级的改变。假设文字处理软件进程的 PID 为 5678,将其 Nice 值设置为 - 5(Nice 值范围 - 20 - 19,数值越小优先级越高),命令如下:
这样,文字处理软件进程在竞争 CPU 资源时会更具优势,为用户提供更流畅的交互体验。
7.2利用工具分析调度器行为
perf 工具简介:perf是 Linux 内核自带的一款强大的性能分析工具,它可以用于分析调度器行为、进程的 CPU 使用情况、函数调用关系等多个方面。通过perf,我们可以获取详细的调度信息,帮助我们深入了解系统的运行状态。获取调度信息示例:使用perf sched record命令可以记录系统中的调度事件,然后通过perf sched report命令查看调度报告。例如,要记录一段时间内的调度事件并生成报告,可以执行以下操作:
复制
# 记录调度事件,持续10秒
perf sched record -a -g -- sleep 10
# 查看调度报告
perf sched report1.2.3.4.
在生成的调度报告中,我们可以看到每个进程的调度时间、调度次数、等待时间等信息。例如,报告中可能会显示某个进程在某段时间内被调度了 100 次,总运行时间为 5 秒,等待时间为 3 秒等。这些信息可以帮助我们分析哪些进程的调度存在问题,比如某个进程等待时间过长,可能是因为它的优先级较低,或者系统中存在其他高优先级进程占用了过多 CPU 资源。通过分析这些信息,我们可以针对性地调整调度策略,优化系统性能。此外,perf还支持生成火焰图,通过火焰图可以更直观地展示进程的调用关系和 CPU 使用情况,帮助我们快速定位性能瓶颈。例如,使用perf record记录性能数据,然后通过FlameGraph工具将数据转换为火焰图,命令如下:
复制
# 记录性能数据
perf record -a -g -o perf.data
# 生成火焰图
./FlameGraph/stackcollapse-perf.pl perf.data |./FlameGraph/flamegraph.pl > perf.svg1.2.3.4.
打开生成的perf.svg文件,就可以看到一个直观的火焰图,不同颜色的条带表示不同的函数调用,条带的长度表示函数的执行时间,通过火焰图可以一目了然地看到哪些函数占用了大量 CPU 时间,以及它们之间的调用关系,为优化调度策略提供有力依据。