探究Linux系统中的调度开销:如何优化? (linux 调度开销)

在现代操作系统中,调度是操作系统最重要的原则之一。它决定着进程如何被分配资源和执行。然而,随着系统的复杂程度不断升级,调度的成本和开销也在增加。Linux系统也不例外。本文将探究在Linux系统中调度引起的开销,并提供一些优化的建议。

1. 调度的开销

调度的开销包括内核态和用户态两部分。内核态开销是指在内核中进行任务调度时产生的成本,例如上下文切换、进程间数据拷贝、进程调度队列等。用户态开销是指从一个进程切换到另一个进程时发生的成本,例如CPU缓存失效、TLB缓存失效、页表查找等。

由于调度涉及到多种开销因素,因此在Linux系统中,调度开销通常是相当高的。通常情况下,内核态开销远高于用户态开销。在内核中处理调度需要多次上下文切换,从而导致额外的开销。

2. 如何优化调度开销

为了减少调度开销,以下几条建议可以被实践:

2.1. 提高系统资源利用率

在Linux系统中,任务被调度时会占用CPU,而这会导致更高的开销。通过提高系统资源利用率,例如利用轻量级进程、IO调度和负载平衡、多线程等,可以减少系统资源的浪费,从而降低开销。

2.2. 缓存机制

在Linux系统中,缓存机制可以有效地减少调度引起的缓存失效。通过缓存在进程间传递的数据,可以减少数据拷贝的开销。而同时,可以避免产生过多的上下文切换,从而改进CPU缓存和TLB。

2.3. 调整进程调度策略

在Linux系统中,进程调度策略是可以被调整的,以实现更高效的调度。改变进程调度的时间片和优先级,例如采用高优先级的进程将被先执行,这不仅可以减少开销,而且还可以提高系统的性能。

2.4. 多处理器系统

Linux系统支持多处理器架构的机器,因此通过多处理器能够减少调度的开销。虽然多处理器系统需要更多的内存和CPU,但是它可以提供更好的可伸缩性,并且可以分发CPU负载,从而减少调度开销。

3.

在Linux系统中,调度是非常重要的一环。调度开销的高低与系统的优化程度相关,需要通过一系列手段进行优化。虽然在优化调度时,需要在多种开销中寻找平衡点,但是通过上述方法,可以实现更优秀的效果。培养高效的调度策略,将更好地贡献于Linux系统和进程分配的稳定性,提高系统的用途和用户满意度。

相关问题拓展阅读:

Linux 进程调度

Linux的调度策略区分实时进程和普通进程,实时进程的调度策略是SCHED_FIFO和SCHED_RR,普通的,非实时进程的调度策略是SCHED_NORMAL(SCHED_OTHER)。

实时调度策略被实时调度器管理,普通调度策略被完全公平调度器来管理。实时进程的优先级要高于普通进程(坦脊nice越小优先级越高)。

SCHED_FIFO实现了一种简单的先入先出的调度算法,它不使用时间片,但支持抢占,只有优先级更高的SCHED_FIFO或者SCHED_RR进程才能抢占它,否则它会一直执行下去,低优先级的进程不能抢占它,直到它受阻塞或自己主动释放处理器。

SCHED_RR是带有时间片的一种实时轮流调度算法,当SCHED_RR进程耗尽它的时间片时,同一优先级的其它实时进程被轮流调度,时间片只用来重新调用同一优先级的进程,低优先级的进程决不能抢占SCHED_RR任务,即使它的时间片耗尽。SCHED_RR是带时间片的SCHED_FIFO。

Linux的实时调度算法提供了一种软实时工作方式,软实时的含义是尽力调度进程,尽力使进程在它的限定时间到来前运颤橘行,但内核不保证总能满足这些进程的要求,相反,硬实时系统保证在一定的条件下,可以满足任何调度的要求。

SCHED_NORMAL使用完全公平调度算法(CFS),之前的算法直接将nice值对应时间片的长度,而在CFS中,nice值只作为进程获取处理器运行比的权重,每个进程都有一个权重,nice优先级越高,权重越大,表示应该运行更长的时间。Linux的实现中,每个进程都有一个vruntime字段,vruntime是经过量化的进程运行时间,也就是实际运行时间除以权重,所以每个量化后的vruntime应该相等,这就体现了公平性。

CFS当然也支持抢占,但与实时调度算法不同,实时调度算法是根据优先级进行抢占,CFS是根据vruntime进行抢占,vruntime小就拥有优先被运行的权利。

为了计算时间片,CFS算法需要为完美多任务中的无限小调度周期设定近似值,这个近似值也称作目标延迟,指每个可运行进程在目标延迟内都会调度一次,如果进程数量太多,则时间粒度太小,所以约定时间片的默认最小粒度是1ms。

进程可以分为I/O消耗型和处理器消耗型,这两种进程的调度策略应该不同,I/O消耗型应该更加实时,给对端的感觉是响应很快,同时它一般又不会消耗太多的让洞渗处理器,因而I/O消耗型需要调度频繁。相对来说,处理器消耗型不需要特别实时,应该尽量降低它的调度频度,延长其运行时间。

参考: linux内核分析——CFS(完全公平调度算法) – 一路向北你好 – 博客园

Linux进程调度的概述

Linux的调度程序是一个叫Schedule()的函数,由它来决定是否要进行进程的切换。而所谓的调度时机则是在什么情况下执行调度程序。

Linux进程调度采用的是抢占式多任务处理,所以进程之间的挂悉孝胡起和继续运行无需彼此之间的协作。

主要分为以下几种情况:

1、进程状态转换的时刻:进程终止、进程睡眠

进程要调用sleep()或exit()等函数进行状态转换,这些函数会主动调用调度程序进行进程调度。

2、当前进程的时间片用完时(current->counter=0)

由于进程的时间片是由时钟中断来更新的,因此,这种情况和时机4是一样的。

3、设备驱动程序

当设备驱动程序执行长而重复的任务时,直接调用调度程序。在每次反复循环中,驱动程序都检查need_resched的值,如果必要,则调用调度程序schedule()主动放弃CPU。

4、进程从中断、异睁拦常及系统调用返回到用户态时

不慎升管是从中断、异常还是系统调用返回,最终都调用ret_from_sys_call(),由这个函数进行调度标志的检测,如果必要,则调用调度程序。

在Linux中,进程的运行时间不可能超过分配给他裂首芹们的时间片,他们采用的是抢占式多任务处理,所以进程之间的挂起和继续运行无需彼此之间的协作。

在一个如linux这样的多任务系统中,多个程序可能会竞争使用同一个资源,在这种情况下,我们认为,执行短期的突发性工作并暂停运行以等待输入的程序,要比持续占用处芹清理器以进行计算或不断轮询系统以查看是否有输入到达的程序要更好。我们称表现好的程序为nice程序,而且在某种意义上,这个nice 是可以被计算出来的。操作系统根据进程的nice值来决定它的优先级,一个进程的nice值默认为0并将根据这个程序的表肆毕现不断变化。长期不间断运行的程序的优先级一般会比较低。

一文读懂Linux任务间调度原理和整个执行过程

在前文中,我们分析禅档了内核中进程和线程的统一结构体task_struct,并分析进程、线程的创建和派生的过程。在本文中,我们会对任务间调度进行详细剖析,了解其原理和整个执行过程。由此,进程、线程部分的大体框架就算是介绍完了。本节主要分为三个部分:Linux内核中常见的调度策略,调度的基本结构体以及调度发生的整个流程。下面将详细展开说明。

Linux 作为一个多任务操作系统,将每个 CPU 的时间划分为很短的时间片,再通过调度器轮流分配给各个任务使用,因此造成多任务同时运行的错觉。为了维护 CPU 时间,Linux 通过事先定义的节拍率(内核中表示为 HZ),触发时间中断,并使用全局变量 Jiffies 记录了开机以来的节拍数。每发生一次时间中断,Jiffies 的值就加 1。节拍率 HZ 是内核的可配选项,可以设置为 100、250、1000 等。不同的系统可能设置不同的数值,可以通过查询 /boot/config 内核选项来查看它的配置值。

Linux的调度策略主要分为实时任务和普通任务。实时任务需求尽快返回结果,而普通任务则没有较高的要求。在前文中我们提到了task_struct中调度策略相应的变量为policy,调度优先级有prio, static_prio, normal_prio, rt_priority几个。优先级其实就是一个数值,对于实时进程来说,优先级的范围是 0 99;对于普通进程,优先级的范围是。数值越小,优先级越高。

实时调度策答态略主要包括以下几种

普通调度策略主要包括以下几种:

首先,我们需要一个结构体去执行调度策略,即sched_class。该类有几种实现方式

普通任务调度实体源码如下,这里面包含了 vruntime 和权重 load_weight,以及对于运行时间的统计清袭源。

在调度时,多个任务调度实体会首先区分是实时任务还是普通任务,然后通过以时间为顺序的红黑树结构组合起来,vruntime 最小的在树的左侧,vruntime最多的在树的右侧。以CFS策略为例,则会选择红黑树最左边的叶子节点作为下一个将获得 CPU 的任务。而这颗红黑树,我们称之为运行时队列(run queue),即struct rq。

其中包含结构体cfs_rq,其定义如下,主要是CFS调度相关的结构体,主要有权值相关变量、vruntime相关变量以及红黑树指针,其中结构体rb_root_cached即为红黑树的节点

对结构体dl_rq有类似的定义,运行队列由红黑树结构体构成,并按照deadline策略进行管理

对于实施队列相应的rt_rq则有所不同,并没有用红黑树实现。

下面再看看调度类sched_class,该类以函数指针的形式定义了诸多队列操作,如

调度类分为下面几种:

队列操作中函数指针指向不同策略队列的实际执行函数函数,在linux/kernel/sched/目录下,fair.c、idle.c、rt.c等文件对不同类型的策略实现了不同的函数,如fair.c中定义了

以选择下一个任务为例,CFS对应的是pick_next_task_fair,而rt_rq对应的则是pick_next_task_rt,等等。

由此,我们来总结一下:

有了上述的基本策略和基本调度结构体,我们可以形成大致的骨架,下面就是需要核心的调度流程将其拼凑成一个整体,实现调度系统。调度分为两种,主动调度和抢占式调度。

说到调用,逃不过核心函数schedule()。其中sched_submit_work()函数完成当前任务的收尾工作,以避免出现如死锁或者IO中断等情况。之后首先禁止抢占式调度的发生,然后调用__schedule()函数完成调度,之后重新打开抢占式调度,如果需要重新调度则会一直重复该过程,否则结束函数。

而__schedule()函数则是实际的核心调度函数,该函数主要操作包括选取下一进程和进行上下文切换,而上下文切换又包括用户态空间切换和内核态的切换。具体的解释可以参照英文源码注释以及中文对各个步骤的注释。

其中核心函数是获取下一个任务的pick_next_task()以及上下文切换的context_switch(),下面详细展开剖析。首先看看pick_next_task(),该函数会根据调度策略分类,调用该类对应的调度函数选择下一个任务实体。根据前文分析我们知道,最终是在不同的红黑树上选择最左节点作为下一个任务实体并返回。

下面来看看上下文切换。上下文切换主要干两件事情,一是切换任务空间,也即虚拟内存;二是切换寄存器和 CPU 上下文。关于任务空间的切换放在内存部分的文章中详细介绍,这里先按下不表,通过任务空间切换实际完成了用户态的上下文切换工作。下面我们重点看一下内核态切换,即寄存器和CPU上下文的切换。

switch_to()就是寄存器和栈的切换,它调用到了 __switch_to_a。这是一段汇编代码,主要用于栈的切换, 其中32位使用esp作为栈顶指针,64位使用rsp,其他部分代码一致。通过该段汇编代码我们完成了栈顶指针的切换,并调用__switch_to完成最终TSS的切换。注意switch_to中其实是有三个变量,分别是prev, next, last,而实际在使用时,我们会对last也赋值为prev。这里的设计意图需要结合一个例子来说明。假设有ABC三个任务,从A调度到B,B到C,最后C回到A,我们假设仅保存prev和next,则流程如下

最终调用__switch_to()函数。该函数中涉及到一个结构体TSS(Task State Segment),该结构体存放了所有的寄存器。另外还有一个特殊的寄存器TR(Task Register)会指向TSS,我们通过更改TR的值,会触发硬件保存CPU所有寄存器在当前TSS,并从新的TSS读取寄存器的值加载入CPU,从而完成一次硬中断带来的上下文切换工作。系统初始化的时候,会调用 cpu_init()给每一个 CPU 关联一个 TSS,然后将 TR 指向这个 TSS,然后在操作系统的运行过程中,TR 就不切换了,永远指向这个 TSS。当修改TR的值得时候,则为任务调度。

更多Linux内核视频教程文本资料免费领取后台私信【

内核大礼包

】自行获取。

在完成了switch_to()的内核态切换后,还有一个重要的函数finish_task_switch()负责善后清理工作。在前面介绍switch_to三个参数的时候我们已经说明了使用last的重要性。而这里为何让prev和last均赋值为prev,是因为prev在后面没有需要用到,所以节省了一个指针空间来存储last。

至此,我们完成了内核态的切换工作,也完成了整个主动调度的过程。

抢占式调度通常发生在两种情况下。一种是某任务执行时间过长,另一种是当某任务被唤醒的时候。首先看看任务执行时间过长的情况。

该情况需要衡量一个任务的执行时间长短,执行时间过长则发起抢占。在计算机里面有一个时钟,会过一段时间触发一次时钟中断,通知操作系统时间又过去一个时钟周期,通过这种方式可以查看是否是需要抢占的时间点。

时钟中断处理函数会调用scheduler_tick()。该函数首先取出当前CPU,并由此获取对应的运行队列rq和当前任务curr。接着调用该任务的调度类sched_class对应的task_tick()函数进行时间事件处理。

以普通任务队列为例,对应的调度类为fair_sched_class,对应的时钟处理函数为task_tick_fair(),该函数会获取当前的调度实体和运行队列,并调用entity_tick()函数更新时间。

在entity_tick()中,首先会调用update_curr()更新当前任务的vruntime,然后调用check_preempt_tick()检测现在是否可以发起抢占。

check_preempt_tick() 先是调用 sched_slice() 函数计算出一个调度周期中该任务运行的实际时间 ideal_runtime。sum_exec_runtime 指任务总共执行的实际时间,prev_sum_exec_runtime 指上次该进程被调度时已经占用的实际时间,所以 sum_exec_runtime – prev_sum_exec_runtime 就是这次调度占用实际时间。如果这个时间大于 ideal_runtime,则应该被抢占了。除了这个条件之外,还会通过 __pick_first_entity 取出红黑树中最小的进程。如果当前进程的 vruntime 大于红黑树中最小的进程的 vruntime,且差值大于 ideal_runtime,也应该被抢占了。

如果确认需要被抢占,则会调用resched_curr()函数,该函数会调用set_tsk_need_resched()标记该任务为_TIF_NEED_RESCHED,即该任务应该被抢占。

某些任务会因为中断而唤醒,如当 I/O 到来的时候,I/O进程往往会被唤醒。在这种时候,如果被唤醒的任务优先级高于 CPU 上的当前任务,就会触发抢占。try_to_wake_up() 调用 ttwu_queue() 将这个唤醒的任务添加到队列当中。ttwu_queue() 再调用 ttwu_do_activate() 激活这个任务。ttwu_do_activate() 调用 ttwu_do_wakeup()。这里面调用了 check_preempt_curr() 检查是否应该发生抢占。如果应该发生抢占,也不是直接踢走当前进程,而是将当前进程标记为应该被抢占。

由前面的分析,我们知道了不论是是当前任务执行时间过长还是新任务唤醒,我们均会对现在的任务标记位_TIF_NEED_RESCUED,下面分析实际抢占的发生。真正的抢占还需要一个特定的时机让正在运行中的进程有机会调用一下 __schedule()函数,发起真正的调度。

实际上会调用__schedule()函数共有以下几个时机

从系统调用返回用户态:以64位为例,系统调用的链路为do_syscall_64->syscall_return_slowpath->prepare_exit_to_usermode->exit_to_usermode_loop。在exit_to_usermode_loop中,会检测是否为_TIF_NEED_RESCHED,如果是则调用__schedule()

内核态启动:内核态的执行中,被抢占的时机一般发生在 preempt_enable() 中。在内核态的执行中,有的操作是不能被中断的,所以在进行这些操作之前,总是先调用 preempt_disable() 关闭抢占,当再次打开的时候,就是一次内核态代码被抢占的机会。preempt_enable() 会调用 preempt_count_dec_and_test(),判断 preempt_count 和 TIF_NEED_RESCHED 是否可以被抢占。如果可以,就调用 preempt_schedule->preempt_schedule_common->__schedule 进行调度。

   本文分析了任务调度的策略、结构体以及整个调度流程,其中关于内存上下文切换的部分尚未详细叙述,留待内存部分展开剖析。

1、调度相关结构体及函数实现

2、schedule核心函数

关于linux 调度开销的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。


数据运维技术 » 探究Linux系统中的调度开销:如何优化? (linux 调度开销)