先大体了解linux的调度器、调度策略、调度类、CFS的基本概念
- linux的调度器: linux调度器使用不同的调度策略和调度类来决定哪个任务应该获得CPU时间。 Linux调度器是Linux操作系统内核的一部分,负责管理和调度进程或线程的执行。调度器的主要职责是决定哪个进程或线程应该获得CPU时间以及何时获得。它是操作系统中一个非常关键的组件,因为它直接影响到系统的性能和响应性。
- 调度策略(Scheduling Policy): 调度策略是指内核为任务分配CPU时间的方式,任务调度策略非常复杂,因为它需要考虑很多因素,比如进程的优先级、系统的整体负载、进程的I/O需求等。而且,为了满足各种不同的应用场景,从普通的桌面应用到高性能的服务器应用,linux需要支持多种不同的调度策略。Linux支持的调度策略有:
- SCHED_NORMAL(或SCHED_OTHER):这是CFS使用的策略,适用于大多数通用任务。
- SCHED_FIFO 和 SCHED_RR:这两种实时调度策略适用于需要严格时间保证的实时任务,实时任务根据它们的优先级被调度,高优先级的实时任务会优先于普通任务。
- SCHED_BATCH:适用于不需要交互的批处理任务。
- SCHED_IDLE:适用于非常低优先级的任务,仅在系统空闲时运行。
- SCHED_DEADLINE:一种针对实时任务设计的调度策略,它为任务分配一个截止时间,任务必须在截止时间之前完成,调度器会优先保证这些任务的执行,以避免错过截止时间。
调度类(Scheduling Class): 调度类是实现不同调度策略的一种抽象机制。每个调度类都实现了一组特定的函数,这些函数定义了该类的行为和特性。调度类允许内核为不同的调度策略提供定制化的调度行为。
linux的调度类:
Linux/kernel/sched/sched.h:2364:extern const struct sched_class stop_sched_class;
Linux/kernel/sched/sched.h:2365:extern const struct sched_class dl_sched_class;
Linux/kernel/sched/sched.h:2366:extern const struct sched_class rt_sched_class;
Linux/kernel/sched/sched.h:2367:extern const struct sched_class fair_sched_class;
Linux/kernel/sched/sched.h:2368:extern const struct sched_class idle_sched_class;
CFS(Completely Fair Scheduler): CFS 称为完全公平调度器,是抽象的 fair_sched_class
调度类的一个具体实现。
总结它们之间的关系:
- 调度器作为整个系统的调度框架,它根据不同的调度策略和调度类来调度任务。linux调度器调用相应调度类的函数来执行具体的调度操作,这种分层和模块化的设计使得Linux的调度系统既灵活又高效。
- 调度策略是指内核为任务分配CPU时间的方式。每种策略都对应着特定的行为和性能保证。
- 调度类是内核中实现调度策略的抽象。每个调度策略都至少对应一个调度类,调度类通过一组函数来管理任务的调度行为。
- CFS是一个具体的调度器实现,它使用SCHED_NORMAL调度策略。CFS通过调度类
fair_sched_class
来提供公平调度的功能。
调度策略 | 调度类 | 调度器 | 代码路径(基于6.8版本) |
SCHED_NORMAL |
| CFS | Linux/kernel/sched/fair.c |
SCHED_FIFO |
| RT | Linux/kernel/sched/rt.c |
SCHED_RR |
| RT | Linux/kernel/sched/rt.c |
SCHED_BATCH |
| CFS | Linux/kernel/sched/fair.c |
SCHED_IDLE |
| IDLE | Linux/kernel/sched/idle.c |
SCHED_DEADLINE |
| DLS | Linux/kernel/sched/deadline.c |
linux的任务调度器的演进及其基本原理
Linux操作系统的任务调度器是内核中一个非常关键的组件,它负责决定哪个任务将在何时运行。随着时间的推移,Linux的任务调度器经历了多次演进,以适应不断变化的硬件和应用需求。以下是Linux任务调度器的演进历程以及每个调度器的基本原理:
- O(n) 调度器:
- 阶段:Linux 0.01至2.4.x版本。
- 原理:这是Linux最早的调度器,它的调度算法复杂度为O(n),意味着每次调度都需要遍历所有任务。它维护一个全局的就绪队列,并为每个任务分配一个时间片。
- O(1) 调度器:
- 阶段:Linux 2.6.0至2.6.22版本。
- 原理:为了解决O(n)调度器的可伸缩性问题,O(1)调度器被引入。它为每个CPU维护了多个运行队列,每个队列对应不同的优先级。通过优先级位图(bitmap)和两个数组(active和expired)来提高调度效率,使得调度器的选择下一个任务的操作时间复杂度为常数。
- 完全公平调度器 (CFS):
- 阶段:Linux 2.6.23版本至今。
- 原理:CFS旨在提供完全公平的调度,它通过红黑树来维护任务的虚拟运行时间(vruntime),并尽可能均匀地分配CPU时间给每个任务。CFS跟踪每个任务的vruntime,并选择vruntime最小的任务来运行,以此来模拟理想的多任务处理器。
- 实时调度器 (RT):
- 阶段:与CFS并行发展,用于实时任务。
- 原理:实时调度器用于需要快速响应的任务,如音频和视频处理。它通常使用SCHED_FIFO(FIFO)或SCHED_RR(Round Robin)策略,根据实时任务的静态优先级进行调度,可以抢占非实时任务。
- BFS (Bottleneck Fairness Scheduler):
- 阶段:作为对CFS的一种补充和反思。
- 原理:BFS调度器由Con Kolivas开发,专为桌面环境设计,目的是提供更好的交互性能。它回到了O(n)调度器的思路,但进行了优化,以减少不必要的复杂性。
- IDLE 调度器:
- 原理:IDLE调度器是一种特殊的调度类,它与SCHED_IDLE调度策略相关联,用于在系统没有更好任务可运行时调度空闲任务。它通常在CPU空闲时被调度,以执行一些后台任务,如磁盘整理或系统维护工作。
每种调度器都有其特定的应用场景和优缺点。随着系统的发展,调度器的设计也在不断进化,以更好地满足实时性、交互性和公平性的需求。
linux任务调度器的特点
通俗点讲,调度器是操作系统内核中的一堆代码(主要代码位于 Linux/kernel/sched/
目录下),它不作为一个独立的进程存在,它提供了一些接口给操作系统使用,比如 schedule()
接口,当任务需要被重新调度时,会调用此接口,然后调度器会执行以下几步操作:
- 评估当前状态:调度器会检查当前正在运行的任务是否应该被剥夺CPU时间,比如它的时间片是否已经用完,或者它是否正在等待某些资源(比如磁盘I/O)。
- 选择下一个任务:调度器会查看所有的就绪队列(ready queue),这些队列中包含了所有准备运行但尚未运行的任务。调度器会根据特定的调度策略(比如完全公平调度器(CFS)、实时调度器(RT))选择下一个要运行的任务。
- 上下文切换:一旦新的任务被选中,调度器会进行上下文切换,即保存当前任务的状态(包括它在CPU中的寄存器信息等),并加载新任务的状态,这样新任务就可以从它上次停止的地方继续执行。
- 更新任务状态:调度器会更新操作系统的内部数据结构,以反映哪些任务正在运行,哪些在等待,哪些被阻塞等。
- 开始新任务的执行:最后,调度器会让CPU开始执行新选中的任务。
以下是Linux调度器的一些关键特点:
- 多任务:Linux调度器支持多任务处理,允许多个任务(进程或线程)同时运行。
- 上下文切换:调度器负责在不同进程或线程之间进行上下文切换,这包括保存当前任务的状态和恢复下一个任务的状态。
- 调度策略:Linux提供了多种调度策略,如SCHED_OTHER、SCHED_FIFO、SCHED_RR、SCHED_IDLE。
- 优先级驱动:调度器根据进程或线程的优先级来调度它们的执行。每个进程或线程都有一个动态调整的优先级。
- 抢占式:Linux是一个可抢占式操作系统,这意味着调度器可以中断正在运行的任务,以便让更高优先级或更紧急的任务运行。
- 负载均衡:调度器尝试在多个CPU核心之间均衡地分配任务,以优化系统性能。
- 可扩展性:Linux调度器设计得非常灵活和可扩展,以适应不同的硬件架构和系统需求。
- 可配置性:系统管理员可以通过各种机制(如/proc文件系统)来配置调度器的行为。
- 调度器扩展:Linux内核允许通过内核模块来扩展或替换默认的调度器。
以下是一些与调度器相关的内核接口:
schedule()
:这是调度器的核心函数,当需要重新调度时,会调用此函数以选择新的任务运行。wake_up()
:唤醒等待队列中的一个或多个一个或多个任务。wake_up_process()
:当一个任务变为就绪状态时,这个函数会被调用,以唤醒该任务并将其加入到调度队列中。try_to_wake_up()
:尝试唤醒一个任务,如果成功,任务将被放入就绪队列。put_task_struct()
:当一个任务结束时,这个函数会被调用以释放任务的资源。pick_next_task()
:选择下一个要执行的任务,在 CFS 中,此函数会根据任务的虚拟运行时间(vruntime)来选择最合适的任务。update_curr()
:更新当前任务的状态,如运行时间统计等。switch_mm()
:在上下文切换过程中,切换任务的内存映射。switch_to()
:执行实际的上下文切换,保存当前任务的状态并加载新任务的状态。cpu_idle()
:当CPU没有任务可运行时,这个函数会被调用。init_idle()
:初始化一个idle任务。kernel_thread()
:创建一个内核线程。kthread_create()
和kthread_stop()
:创建和终止一个内核线程。task_rq()
:获取当前任务的调度实体。sched_class
和struct sched_class
:定义了一组函数指针,用于实现特定的调度策略。struct task_struct
:表示一个任务的数据结构,包含了任务的调度信息。rq
(runqueue):表示一个调度队列,包含了所有可运行的任务。
这些内核接口是调度器正常工作的基础,它们被设计为高效且安全,以确保操作系统的稳定性和性能。内核开发者和系统管理员通常不需要直接与这些接口交互,因为它们的使用由内核代码和内核模块自动管理。了解这些内核接口有助于深入理解操作系统的工作原理,特别是调度器如何管理和调度任务的执行。然而,对于大多数用户和开发者来说,直接与这些内核接口交互并不常见,因为用户空间提供了更高级别的抽象和接口。
以下是一些常用的用户态接口,它们与调度器相关:
nice()
:改变一个任务的nice值,这会影响其在CFS调度器中的优先级。getpriority()
和setpriority()
:分别用于获取和设置进程或进程组的nice值。sched_getscheduler()
:获取指定进程的当前调度策略。sched_setscheduler()
:这个系统调用允许改变一个进程的调度策略和相关参数。例如,可以设置一个进程为实时调度策略。sched_getparam()
:获取进程的调度参数,如实时优先级。sched_setparam()
:允许用户设置一个进程的调度参数,如优先级(对于实时调度策略)。sched_getaffinity()
:获取进程的CPU亲和性,即进程被允许运行的CPU核心集合。sched_setaffinity()
:设置一个进程可以在哪些CPU核心上运行,这称为CPU亲和性(CPU affinity)。sched_yield()
:一个进程可以调用这个函数来放弃当前的CPU时间,即使它还有时间片剩余。调度器将重新调度,可能会选择另一个就绪的进程来运行。clock_nanosleep()
:允许进程以微秒或纳秒为单位休眠,直到指定的时间过了或者被某个信号打断。nanosleep()
:与clock_nanosleep()
类似,但以秒和纳秒为单位。mlockall()
和munlockall()
:分别用于锁定和解锁进程地址空间的所有当前和将来的映射页面,以防止它们被交换出物理内存。prlimit()
:获取和设置进程的资源限制,如CPU时间、内存大小等。setrlimit()
和getrlimit()
:分别用于设置和获取进程的资源限制。times()
:返回进程占用CPU的时间信息。
这些用户态接口为用户程序提供了与内核调度器交云的能力,使得用户程序可以调整其调度行为,如改变优先级、设置CPU亲和性等。正确使用这些接口可以帮助用户程序获得更好的性能,或者满足特定的实时性要求。
需要注意的是,不当使用这些接口可能会对系统性能产生负面影响,甚至导致系统不稳定。因此,在使用这些接口时,应该充分理解它们的工作原理和可能的后果。此外,一些接口可能需要特定的权限才能使用,如改变其他进程的调度策略通常需要管理员权限。
linux上下文是什么?
在多任务操作系统如Linux中,上下文(Context)是指任务(某个进程或线程)的执行环境,包括它的代码、数据、状态信息以及CPU寄存器等。上下文是操作系统用来追踪进程状态和执行位置的一种方式。
进程上下文切换(不同进程之间的切换)的执行环境:
- 虚拟地址空间:每个进程都有自己的虚拟地址空间,包括代码段、数据段、堆和栈等。
- 页表:与虚拟地址空间相关的页表,用于虚拟内存到物理内存的映射。
- 寄存器:包括程序计数器(PC)、堆栈指针(SP)和其他CPU寄存器。
- 堆栈:每个进程有自己的用户级堆栈和内核级堆栈。
- 文件描述符:进程打开的文件句柄和相关的文件状态信息。
- 环境变量:进程的环境变量集合。
- 信号处理:进程的信号处理设置,包括信号处理函数和信号掩码。
- 其他进程特定的状态:如会话和进程组ID、工作目录、根目录、umask等。
线程上下文切换(同进程的线程之间的切换)的执行环境:
- 寄存器:包括程序计数器、堆栈指针和其他CPU寄存器。
- 线程局部存储(Thread Local Storage, TLS):每个线程的局部数据。
- 程序计数器:指示下一条要执行的指令的位置。
- 堆栈:每个线程都有自己的调用栈,用于存储函数调用的参数、局部变量和返回地址。
- 线程特定数据:如线程环境块(TEB)或线程信息块(TIB)。
- 调度信息:线程的调度优先级、状态(如就绪、运行或阻塞)和其他调度器相关的数据。
在多线程环境中,所有线程共享同一进程的虚拟地址空间和文件描述符,但它们有独立的执行上下文,包括自己的寄存器集、堆栈和局部数据。线程上下文切换比进程上下文切换要快,因为它们不需要改变虚拟地址空间或页表,也不需要更新文件描述符和环境变量等进程特定的信息。
上下文切换的时机
上下文切换是操作系统在不同进程或线程之间切换时保存当前任务的状态,并恢复新任务的状态的过程。这通常发生在:
- 任务结束:进程或线程正常结束或异常终止。
- 时间片耗尽:在时间片轮转调度模型中,当前任务的时间片用完。
- 阻塞操作:任务执行I/O操作或其他阻塞操作时,可能需要让出CPU。
- 优先级调度:更高优先级的任务变为就绪状态。
上下文切换的过程
- 保存当前任务的状态:操作系统保存当前任务的CPU寄存器、程序计数器和其他必要的状态信息。
- 选择下一个任务:调度器根据调度策略选择下一个要执行的任务。
- 加载新任务的状态:操作系统加载下一个任务的寄存器状态和其他必要的信息。
- 更新内存映射:如果有必要,更新内存管理单元(MMU)的页表以反映新任务的地址空间。
上下文切换的开销
上下文切换会产生一定的开销,因为:
- 寄存器和状态保存:需要时间来保存和恢复寄存器状态。
- 内存映射更新:更新内存映射可能会涉及TLB(快表)的刷新,这可能会影响性能。
- 调度决策:调度器需要时间来决定下一个要执行的任务。
减少上下文切换的策略
为了提高系统性能,可以采取以下策略:
- 减少不必要的上下文切换:避免频繁的I/O操作和不必要的任务创建。
- 使用合适的调度策略:根据任务特性选择合适的调度策略。
- 优化同步机制:减少锁的使用,使用无锁编程技术。
理解上下文和上下文切换对于分析和优化系统性能至关重要。在设计系统时,开发者需要考虑如何最小化上下文切换的开销,以提高系统的整体效率。