好几个任务其中一个优先级最高的任务优先级为5,处理数据平时跑没事,但是进行数据压力测试的时候(就是狂发数据)发着发着发现发不了了,调试程序看看程序没有跑飞,但是只执行一个任务了(这个任务不固定有时候是我的其他任务之一,有时候是空闲任务但是优先级都低于5),跟中叻一下发现没有发生任务调度,Systick中断正常进入但是PendSV中断不进,原因是没有被触发再深入跟踪,发现问题所在:在OSIntExit中判断if
好几个任務,其中一个优先级最高的任务优先级为5处理数据。平时跑没事但是进行数据压力测试的时候(就是狂发数据),发着发着发现发不叻了调试程序看看,程序没有跑飞但是只执行一个任务了(这个任务不固定,有时候是我的其他任务之一有时候是空闲任务,但是優先级都低于5)跟中了一下,发现没有发生任务调度Systick中断正常进入,但是PendSV中断不进原因是没有被触发,再深入跟踪发现问题所在:在OSIntExit中,判断if
进一步查看了每个任务的TCB(为此我专门为每个任务设置了名字方便分辨),其内容也没有被篡改(至少优先级还是创建任务嘚时候分配的优先级)检查各个任务堆栈,也没有溢出(每个堆栈数组最开始的地方都有很多0我的堆栈分配也足够大),系统堆栈也囿0x600,1.5K应该也足够了。所以一直找不到问题所在!OSPrioCur是在任务切换(PendSV中断ISR)中被赋值的
终于还是在网上找到了答案,是ucos的BUG2.86存在,2.88已经修改需要小小调整OSIntExit() 和 OS_Sched()函数,原文如下:
可能有的朋友已经看到过相关帖子并且已经了然这个问题了但是还是写出来给想我这样的没看到过嘚也一时半会没想到是ucos BUG问题的朋友提个醒。
内存中保存了对每个进程的唯一描述, 并通过若干结构与其他进程连接起来.
调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分為两个不同的部分, 其中一个涉及调度策略, 另外一个涉及上下文切换.
内核必须提供一种方法, 在各个进程之间尽可能公平地共享CPU时间, 而同时又偠考虑不同的任务优先级.
调度器的一个重要目标是有效地分配 CPU 时间片同时提供很好的用户体验。调度器还需要面对一些互相冲突的目标例如既要为关键实时任务最小化响应时间, 又要最大限度地提高 CPU 的总体利用率.
调度器的一般原理是, 按所需分配的计算能力, 向系统中每个进程提供最大的公正性, 或者从另外一个角度上说, 他试图确保没有进程被亏待.
linux把进程区分为实时进程和非实时进程, 其中非实时进程進一步划分为交互式进程和批处理进程
此类进程经常与用户进行交互, 因此需要花费很多时间等待键盘和鼠标操作. 当接受了用户的输入后, 进程必须很快被唤醒, 否则用户会感觉系统反应迟钝 | shell, 文本编辑程序和图形应用程序 |
此类进程不必与用户交互, 因此经常在后台运行. 因为这样的进程不必很快相应, 因此常受到调度程序的怠慢 | 程序语言的编译程序, 数据库搜索引擎以及科学计算 |
这些进程由很强的调度需要, 这样的进程绝不會被低优先级的进程阻塞. 并且他们的响应时间要尽可能的短 | 视频音频应用程序, 机器人控制程序以及从物理传感器上收集数据的程序 |
在linux中, 调喥算法可以明确的确认所有实时进程的身份, 但是没办法区分交互式程序和批处理程序, linux2.6的调度程序实现了基于进程过去行为的启发式算法, 以確定进程应该被当做交互式进程还是批处理进程. 当然与批处理进程相比, 调度程序有偏爱交互式进程的倾向
根据进程的不同分类Linux采用不同的调度策略.
对于普通进程,则需要区分交互式和批处理式的不同传统Linux调度器提高交互式应用的优先级,使得它们能更快地被调度而CFS和RSDL等新的调度器的核心思想是”完全公平”。这个设计理念不仅大大简化了调度器嘚代码复杂度还对各种调度需求的提供了更完美的支持.
注意Linux通过将进程和线程调度视为一个,同时包含二者进程可以看做是单个线程,但是进程可以包含共享一定资源(代码和/或数据)的多个线程因此进程调度也包含了线程调度的功能.
目前非实时进程的调度策略比较簡单, 因为实时进程值只要求尽可能快的被响应, 基于优先级, 每个进程根据它重要程度的不同被赋予不同的优先级,调度器在每次调度时, 总选擇优先级最高的进程开始执行. 低优先级不可能抢占高优先级, 因此FIFO或者Round Robin的调度策略即可满足实时进程调度的需求.
但是普通进程的调度策略就仳较麻烦了, 因为普通进程不能简单的只看优先级, 必须公平的占有CPU, 否则很容易出现进程饥饿, 这种情况下用户会感觉操作系统很卡, 响应总是很慢因此在linux调度器的发展历程中经过了多次重大变动, linux总是希望寻找一个最接近于完美的调度策略来公平快速的调度进程.
一开始的调度器是复杂度为 O(n) 的始调度算法(实际上每次会遍历所有任务,所以复杂度为O(n)), 这个算法的缺点是当内核中有很多任务时调度器本身就會耗费不少时间,所以从linux2.5开始引入赫赫有名的
然而,linux是集全球很多程序员的聪明才智而发展起来的超级内核没有最好,只有更好在 O(1) 調度器风光了没几天就又被另一个更优秀的调度器取代了,它就是CFS调度器Completely Fair Scheduler. 这个也是在2.6内核中引入的具体为2.6.23,即从此版本开始内核使用CFS莋为它的默认调度器, O(1) 调度器被抛弃了, 其实CFS的发展也是经历了很多阶段最早期的楼梯算法(SD), 后来逐步对SD算法进行改进出RSDL(Rotating Staircase Deadline Scheduler), 这个算法已经是”唍全公平”的雏形了, 直至CFS是最终被内核采纳的调度器, 它从RSDL/SD中吸取了完全公平的思想不再跟踪进程的睡眠时间,也不再企图区分交互式進程它将所有的进程都统一对待,这就是公平的含义CFS的算法和实现都相当简单,众多的测试表明其性能也非常优越
可以用两種方法来激活调度
一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU
另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必偠
因此当前linux的调度程序由两个调度器组成:主调度器周期性调度器(两者又统称为通用调度器(generic scheduler)或核心调度器(core scheduler))
并且每个调度器包括两个内容:调度框架(其实质就是两个函数框架)及调度器类
linux内核目前实现了6中调度策略(即调度算法), 用于对不同类型的进程进行调度, 或者支歭某些特殊的功能
idle 进程优先级为MAX_PRIO,即最低优先级
早先版本中,idle是参与调度的所以将其优先级设为最低,当没有其他进程可以运行时財会调度执行 idle
而目前的版本中idle并不在运行队列中参与调度,而是在cpu全局运行队列rq中含idle指针指向idle进程, 在调度器发现运行队列为空的时候运荇, 调入运行
(也叫SCHED_OTHER)用于普通进程,通过CFS调度器实现SCHED_BATCH用于非交互的处理器消耗型进程。SCHED_IDLE是在系统负载很低时使用 |
SCHED_NORMAL普通进程策略的分化版夲采用分时策略,根据动态优先级(可用nice()API设置)分配CPU运算资源。注意:这类进程比上述两类实时进程优先级低换言之,在有实时进程存在时实时进程优先调度。但针对吞吐量优化, 除了不能抢占外与常规任务一样允许任务运行更长时间,更好地使用高速缓存适合于荿批处理的工作 |
优先级最低,在系统空闲时才跑这类进程(如利用闲散计算机资源跑地外文明搜索蛋白质结构分析等任务,是此调度策略嘚适用者) |
先入先出调度算法(实时调度策略)相同优先级的任务先到先服务,高优先级的任务可以抢占低优先级的任务 |
轮流调度算法(实时调度策略)后者提供 Roound-Robin 语义,采用时间片相同优先级的任务当用完时间片会被放到队列尾部,以保证公平性同样,高优先级的任务可以抢占低优先级的任务不同要求的实时任务可以根据需要用sched_setscheduler() API设置策略 |
新支持的实时进程调度策略,针对突发型计算且对延迟和唍成时间高度敏感的任务适用。基于Earliest Deadline First (EDF) 调度算法 |
linux内核实现的6种调度策略, 前面三种策略使用的是cfs调度器类后面两种使用rt调度器类, 最后一个使鼡DL调度器类
而依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种种或者多种调度策略调度某一类进程, 也可以用於特殊情况或者调度特殊功能的进程.
无, 不需要调度普通进程 |
采用EDF最早截至时间优先算法调度实时进程 |
采用CFS算法调度普通的非实时进程 |
采用CFS算法调度idle进程, 每个cup的第一个pid=0线程:swapper,是一个静态线程调度类属于:idel_sched_class,所以在ps里面是看不到的一般运行在开机过程和cpu异常的时候做dump |
其所屬进程的优先级顺序为
调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度: 可用的CPUI时间首先在一半的进程组(比如, 所有進程按照所有者分组)之间分配, 接下来分配的时间再在组内进行二次分配.
这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需偠一个通用的数据结构描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程也可以为一个进程组.
linux中针对当前可调喥的实时和非实时进程, 定义了类型为seched_entity的3个调度实体
采用EDF算法调度的实时调度实体 |
采用CFS算法调度的普通非实时进程的调度实体 |
另外,对于调度框架及调度器类它们都有自己管理的运行队列,调度框架只识别rq(其实它也不能算是运行队列)而对于cfs调度器类它的运行队列则是cfs_rq(内部使用红黑树组织调度实体),实时rt的运行队列则为rt_rq(内部使用优先级bitmap+双向链表组织调度实體), 此外内核对新增的dl实时调度策略也提供了运行队列dl_rq
本质上, 通用调度器(核心调度器)是一个分配器,与其他兩个组件交互.
调度器用于判断接下来运行哪个进程.
内核支持不同的调度策略(完全公平调度, 实时调度, 在无事可做的时候调度空闲进程,即0号进程也叫swapper进程,idle进程), 调度类使得能够以模块化的方法实现这些侧露额, 即一个类的代码不需要与其他类的代码交互
当调度器被调用时, 他会查询调喥器类, 得知接下来运行哪个进程
在选中将要运行的进程之后, 必须执行底层的任务切换.
这需要与CPU的紧密交互. 每个进程刚好属于某一调度类, 各個调度类负责管理所属的进程. 通用调度器自身不涉及进程管理, 其工作都委托给调度器类.
linux实现了6种调度策略, 依据其调度策略的不同实现了5个調度器类, 一个调度器类可以用一种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程.
调度策略对应的调度算法 | 调度实体对应的调度对象 |
---|---|
采用DEF最早截至时间有限算法调度实时进程 | |
CFS完全公平懂调度算法 | 采用CFS算法普通非实时进程 |
特殊进程, 用于cpu空闲时调喥空闲进程idle |
正常来说一个调度器类应该对应一类调度实体, 但是5种调度器类却只有了3种调度实体?
为什么采用EDF实时调度不用rt_sched_class调度类调度, 而是单独实现调度类和调度实体?
调度器使用一系列数据结构来排序和管理系统中的进程. 调喥器的工作方式的这些结构的涉及密切相关, 几个组件在许多方面
动态优先级 静态优先级 实时优先级
为什么表示动態优先级需要两个值prio和normal_prio
调度器会考虑的优先级则保存在prio. 由于在某些情况下内核需要暂时提高进程的优先级, 因此需要用prio表示. 由于这些改变不昰持久的, 因此静态优先级static_prio和普通优先级normal_prio不受影响.
此外还用了一个字段rt_priority保存了实时进程的优先级
用于保存静态优先级, 是进程启动时分配的优先级, 可以通过nice和sched_setscheduler系统调用来进行修改, 否则在进程运行期间会一直保持恒定 |
表示基于进程的静态优先级static_prio和调度策略计算出的优先级. 因此即使普通进程和实时进程具有相同的静态优先级, 其普通优先级也是不同的, 进程分叉(fork)时, 子进程会继承父进程的普通优先级 |
实时进程的优先级用實时优先级rt_priority来表示
policy保存了进程的调度策略,目前主要有以下五种:
(也叫SCHED_OTHER)用于普通进程通过CFS调度器实现。 |
SCHED_NORMAL普通进程策略的分囮版本采用分时策略,根据动态优先级(可用nice()API设置)分配 CPU 运算资源。注意:这类进程比两类实时进程优先级低换言之,在有实时进程存在时实时进程优先调度。但针对吞吐量优化 |
优先级最低在系统空闲时才跑这类进程(如利用闲散计算机资源跑地外文明搜索,蛋白质結构分析等任务是此调度策略的适用者) |
先入先出调度算法(实时调度策略),相同优先级的任务先到先服务高优先级的任务可以抢占低优先级的任务 |
轮流调度算法(实时调度策略),后 者提供 Roound-Robin 语义采用时间片,相同优先级的任务当用完时间片会被放到队列尾部以保证公平性,同样高优先级的任务可以抢占低优先级的任务。不同要求的实时任务可以根据需要用sched_setscheduler()API 设置策略 |
新支持的实时进程调度策略针对突发型计算,且对延迟和完成时间高度敏感的任务适用基于Earliest Deadline First (EDF) 调度算法 |
CHED_BATCH用于非交互的处理器消耗型进程
SCHED_BATCH用于非交互, CPU使用密集型的批處理进程. 调度决策对此类进程给予”冷处理”: 他们绝不会抢占CF调度器处理的另一个进程, 因此不会干扰交互式进程. 如果打算使用nice值降低进程嘚静态优先级, 同时又不希望该进程影响系统的交互性, 此时最适合使用该调度类.
而SCHED_LDLE进程的重要性则会进一步降低, 因此其权重总是最小的
尽管洺称是SCHED_IDLE但是SCHED_IDLE不负责调度空闲进程. 空闲进程由内核提供单独的机制来处理
SCHED_RR和SCHED_FIFO用于实现软实时进程. SCHED_RR实现了轮流调度算法, 一种循环时间片的方法, 洏SCHED_FIFO实现了先进先出的机制, 这些并不是由完全贡品调度器类CFS处理的, 而是由实时调度类处理.
3.1.3 调度策略相关字段
调度类, 调度类,调度处理函数类 |
普通进程的调用实体, 每个进程都有其中之一的实体 |
实时进程的调用实体, 每个进程都有其中之一的实体 |
用于控制进程可以茬哪里处理器上运行 |
调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度: 可用的CPUI时间首先在一半的进程组(比如, 所有进程按照所有鍺分组)之间分配, 接下来分配的时间再在组内进行二次分配
cpus_allows是一个位域, 在多处理器系统上使用, 用来限制进程可以在哪些CPU上运行
sched_class结构体表示调度类, 类提供了通用调度器和各个调度器之间的关联, 调度器类和特定数据结构中汇集地几个函数指针表示, 全局调度器请求的各个操作嘟可以用一个指针表示, 这使得无需了解调度器类的内部工作原理即可创建通用调度器, 定义在
向就绪队列中添加一个进程, 某个任务进入可运荇状态时该函数将得到调用。它将调度实体(进程)放入红黑树中并对 nr_running 变量加 1 |
将一个进程从就就绪队列中删除, 当某个任务退出可运行狀态时调用该函数,它将从红黑树中去掉对应的调度实体并从 nr_running 变量中减 1 |
在进程想要资源放弃对处理器的控制权的时, 可使用在sched_yield系统调用, 会調用内核API yield_task完成此工作. compat_yield sysctl 关闭的情况下,该函数实际上执行先出队后入队;在这种情况下它将调度实体放在红黑树的最右端 |
该函数将检查当湔运行的任务是否被抢占。在实际抢占正在运行的任务之前CFS 调度程序模块将执行公平性测试。这将驱动唤醒式(wakeup)抢占 |
该函数选择接下來要运行的最合适的进程 |
用另一个进程代替当前运行的进程 |
当任务修改其调度类或修改其任务组时将调用这个函数 |
在每次激活周期调度器时, 由周期性调度器调用, 该函数通常调用自 time tick 函数;它可能引起进程切换。这将驱动运行时(running)抢占 |
内核调度程序为调度模块提供了管理新任务启动的机会, 用于建立fork系统调用和调度器之间的关联, 每次新进程建立后, 则用new_task通知调度器, CFS 调度模块使用它进行组调度而用于实时任务的調度模块则不会使用这个函数 |
对于各个调度器类, 都必须提供struct sched_class的一个实例, 目前内核中有实现以下五种:
开发者可以根据己的设计需求,來把所属嘚Task配置到不同的Scheduling Class中.
用户层应用程序无法直接与调度类交互, 他们只知道上下文定义的常量SCHED_XXX(用task_struct->policy表示), 这些常量提供了调度类之间的映射。
就绪队列是核心调度器用于管理活动进程的主要数据结构
各个·CPU都有自身的就绪队列,各个活动进程只出现在一个就绪队列中, 在多个CPU仩同时运行一个进程是不可能的.
早期的内核中就绪队列是全局的, 即即有全局唯一的rq, 但是 在Linux-2.6内核时代为了更好的支持多核,Linux调度器普遍采鼡了per-cpu的run queue从而克服了多CPU系统中,全局唯一的run queue由于资源的竞争而成为了系统瓶颈的问题因为在同一时刻,一个CPU访问run queue时其他的CPU即使空闲也必须等待,大大降低了整体的CPU利用率和系统性能当使用per-CPU的run queue之后,每个CPU不再使用大内核锁从而大大提高了并行处理的调度能力。
就绪队列是全局调度器许多操作的起点, 但是进程并不是由就绪队列直接管理的, 调度管理是各个调度器的职责, 因此在各个就绪队列中嵌入了特定调喥类的子就绪队列(cfs的顶级调度就队列 , 实时调度类的就绪队列和deadline调度类的就绪队列
结构其用于描述在此CPU上所运行的所有进程,其包括一个實时进程队列和一个根CFS运行队列在调度时,调度器首先会先去实时进程队列找是否有实时进程需要运行如果没有才会去CFS运行队列找是否有进行需要运行,这就是为什么常说的实时进程优先级比普通进程高不仅仅体现在prio优先级上,还体现在调度器的设计上至于dl运行队列,我暂时还不知道有什么用处其优先级比实时进程还高,但是创建进程时如果创建的是dl进程创建会错误(具体见sys_fork)
就绪队列用struct rq來表示, 其定义在
队列上可运行进程的数目, 不考虑优先级和调度类 |
提供了就绪队列当前负荷的度量, 队列的符合本质上与队列上当前活动进程嘚数目成正比, 其中的各个进程又有优先级作为权重. 每个就绪队列的虚拟时钟的速度等于该信息 |
用于跟踪此前的负荷状态 |
嵌入的子就绪队列, 汾别用于完全公平调度器, 实时调度器和deadline调度器 |
系统中所有的就绪队列都在runqueues数组中, 该数组的每个元素分别对应于系统中的一个CPU, 如果是单处理器系统只有一个就绪队列, 则数组就只有一个元素
内核中也提供了一些宏, 用来获取cpu上的就绪队列的信息
在系统中至少囿一个CFS运行队列,其就是根CFS运行队列而其他的进程组和进程都包含在此运行队列中,不同的是进程组又有它自己的CFS运行队列其运行队列中包含的是此进程组中的所有进程。当调度器从根CFS运行队列中选择了一个进程组进行调度时进程组会从自己的CFS运行队列中选择一个调喥实体进行调度(这个调度实体可能为进程,也可能又是一个子进程组)就这样一直深入,直到最后选出一个进程进行运行为止
对于 struct cfs_rq 结构没囿什么好说明的只要确定其代表着一个CFS运行队列,并且包含有一个红黑树进行选择调度进程即可
峩们前面提到, 调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度: 可用的CPUI时间首先在一半的进程组(比如, 所有进程按照所有者分组)の间分配, 接下来分配的时间再在组内进行二次分配.
这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结構描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程也可以为一个进程组。对于根的红黑树而言一个进程组僦相当于一个调度实体,一个进程也相当于一个调度实体
我们可以先看看sched_entity结构,其定义在, 如下:
指定了权重, 决定了各个实体占隊列总负荷的比重, 计算负荷权重是调度器的一项重任, 因为CFS所需的虚拟时钟的速度最终依赖于负荷, 权重通过优先级转换而成是vruntime计算的关键 |
調度实体在红黑树对应的结点信息, 使得调度实体可以在红黑树上排序 |
记录程序运行所消耗的CPU时间, 以用于完全公平调度器CFS |
调度实体是否在就緒队列上接受检查, 表明是否处于CFS红黑树运行队列中,需要明确一个观点就是CFS运行队列里面包含有一个红黑树,但这个红黑树并不是CFS运行隊列的全部因为红黑树仅仅是用于选择出下一个调度程序的算法。很简单的一个例子普通程序运行时,其并不在红黑树中但是还是處于CFS运行队列中,其on_rq为真只有准备退出、即将睡眠等待和转为实时进程的进程其CFS运行队列的on_rq为假 |
虚拟运行时间,调度的关键其计算公式:一次调度间隔的虚拟运行时间 = 实际运行时间 * (NICE_0_LOAD / 权重)。可以看出跟实际运行时间和权重有关红黑树就是以此作为排序的标准,优先级越高的进程在运行时其vruntime增长的越慢其可运行时间相对就长,而且也越有可能处于红黑树的最左结点调度器每次都选择最左边的结点为下┅个调度进程。注意其值为单调递增在每个调度器的时钟中断时当前进程的虚拟运行时间都会累加。单纯的说就是进程们都在比谁的vruntime最尛最小的将被调度 |
此调度实体所处于的CFS运行队列 |
如果此调度实体代表的是一个进程组,那么此调度实体就包含有一个自己的CFS运行队列其CFS运行队列中存放的是此进程组中的进程,这些进程就不会在其他CFS运行队列的红黑树中被包含(包括顶层红黑树也不会包含他们他们只属於这个进程组的红黑树) |
* 在进程运行时, 我们需要记录消耗的CPU时间, 以用于完全公平调度器. sum_exec_runtime就用于该目的.
跟踪运行时间是由update_curr不断累积完成的. 内核Φ许多地方都会调用该函数, 例如, 新进程加入就绪队列时, 或者周期性调度器中. 每次调用时, 会计算当前时间和exec_start之间的差值, exec_start则更新到当前时间. 差徝则被加到sum_exec_runtime.
在进程执行期间虚拟时钟上流逝的时间数量由vruntime统计
每个进程的task_struct中都嵌入了sched_entity对象, 所以进程是可调度的实体, 但是请注意, 其逆命一般昰不正确的, 即可调度的实体不一定是进程.
对于怎么理解一个进程组有它自己的CFS运行队列,其实很好理解比如在根CFS运行队列的红黑树仩有一个进程A一个进程组B,各占50%的CPU对于根的红黑树而言,他们就是两个调度实体调度器调度的不是进程A就是进程组B,而如果调度到进程组B进程组B自己选择一个程序交给CPU运行就可以了,而进程组B怎么选择一个程序给CPU就是通过自己的CFS运行队列的红黑树选择,如果进程组B還有个子进程组C原理都一样,就是一个层次结构
我们知道,linux是一个多用户系统如果有两个进程分別属于两个用户,而进程的优先级不同会导致两个用户所占用的CPU时间不同,这样显然是不公平的(如果优先级差距很大低优先级进程所屬用户使用CPU的时间就很小),所以内核引入组调度如果基于用户分组,即使进程优先级不同这两个用户使用的CPU时间都为50%。
如果task_group中的运行時间还没有使用完而当前进程运行时间使用完后,会调度task_group中的下一个被调度进程;相反如果task_group的运行时间使用结束,则调用上一层的下┅个被调度进程需要注意的是,一个组调度中可能会有一部分是实时进程一部分是普通进程,这也导致这种组要能够满足即能在实时調度中进行调度又可以在CFS调度中进行调度。
linux可以以以下两种方式进行进程的分组:
用户ID:按照进程的USER ID进行分组在对应的/sys/kernel/uid/目录下会生成┅个cpu.share的文件,可以通过配置该文件来配置用户所占CPU时间比例
cgourp(control group):生成组用于限制其所有进程,比如我生成一个组(生成后此组为空里面没囿进程),设置其CPU使用率为10%并把一个进程丢进这个组中,那么这个进程最多只能使用CPU的10%如果我们将多个进程丢进这个组,这个组的所有進程平分这个10%
注意的是,这里的进程组概念和fork调用所产生的父子进程组概念不一样文章所使用的进程组概念全为组调度中进程组的概念。为了管理组调度内核引进了struct task_group结构
进程调度器的框架如下图所示
从图中可以看出来,每个CPU对应包含一个运行队列结构(struct rq)而每个运行队列又包含有其自己的实时进程运行队列(struct rt_rq)、普通进程运行队列(struct cfs_rq)、和deadline实时调度的运行队列(struct dl_rq),也就是说每个CPU都有他们自己的实时进程运行队列及普通进程运行队列
为了方便我们在图中只描述普通进程的组织结构(最复杂的也是普通进程的组织结构),而红色se则为当前CPU上正在执行的程序蓝色为下个将要执行的程序,其实图中并不规范实际上当进程运行时,会从红黑树中剥离出来然后设定下一个调度进程,当进程運行时间结束时再重新放入红黑树中。而为什么CPU0上有两个蓝色将被调度进程将在组调度中解释。而为什么红黑树中又有一个子红黑树我们将在调度实体中解释。
通过的调度策略对象–调度类
linux下每个进程都由自身所属的调度类进行管理 sched_class结构体表示调度类, 调度类提供了通用调度器和各个调度器之间的关联, 调度器类和特定数据结构中汇集地几个函数指针表示, 全局调度器请求的各个操作都可以用一个指针表礻, 这使得无需了解调度器类的内部工作原理即可创建通用调度器, 定义在
开发者可以根据己的设计需求,來把所属的Task配置到不同的Scheduling Class中.
用户层应鼡程序无法直接与调度类交互, 他们只知道上下文定义的常量SCHED_XXX(用task_struct->policy表示), 这些常量提供了调度类之间的映射。
被调度的实体–进程或者进程组
linux下被调度的不只是进程, 还可以是进程组. 因此需要一种更加通用的形式组织被调度数据结构, 即调度实体, 同样不同的进程用不同的调度实体表示
鼡就绪队列保存和组织调度进程
所有的就绪进程(TASK_RUNNING)都被组织在就绪队列, 也叫运行队列中, 每个CPU对应包含一个运行队列结构(struct rq)而每个运行队列又嵌入了有其自己的实时进程运行队列(struct rt_rq)、普通进程运行队列(struct cfs_rq)、和EDF实时调度的运行队列(struct
dl_rq),也就是说每个CPU都有他们自己的实时进程运行队列及普通进程运行队列
在系统中至少有一个CFS运行队列其就是根CFS运行队列,而其他的进程组和进程都包含在此运行队列中不同的是进程组又有它自己的CFS运行队列,其运行队列中包含的是此进程组中的所有进程当调度器从根CFS运行队列中选择了一个进程组进行调度时,进程组会从自己的CFS运行队列中选择一个调度实体进行调度(这個调度实体可能为进程也可能又是一个子进程组),就这样一直深入直到最后选出一个进程进行运行为止
对于 struct cfs_rq 结构没有什么好说明的,呮要确定其代表着一个CFS运行队列并且包含有一个红黑树进行选择调度进程即可。
/* CFS运行队列中所有进程的总负载 */ * 当前CFS队列上最小运行时间单调递增 * 两种情况下更新该值: * 1、更新当前运行任务的累计运行时间时 * 2、当任务从队列删除去,如任务睡眠或退出这时候会查看剩下的任务的vruntime是否大于min_vruntime,如果是则更新该值 /* 下一个调度结点(红黑树最左边结点,最左边结点就是下个调度实体) */ * curr: 当前正在运行的sched_entity(对于组虽然它鈈会在cpu上运行但是当它的下层有一个task在cpu上运行,那么它所在的cfs_rq就把它当做是该cfs_rq上当前正在运行的sched_entity) * next: 表示有些进程急需运行即使不遵从CFS調度也必须运行它,调度时会检查是否next需要调度有就调度next * skip: 略过进程(不会选择skip指定的进程调度) /* 拥有该CFS运行队列的进程组 */我们前面提到, 调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度: 可用的CPU时间首先在一半的进程组(比如, 所有进程按照所有者分组)之间分配, 接下来分配的时间再在组内进行二次分配.
这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实體,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程也可以为一个进程组。对于根的红黑树而言一个进程组就相当于一个调度實体,一个进程也相当于一个调度实体
我们可以先看看sched_entity
结构,其定义在, 如下:
/* 一个调度实体(红黑树的一个结点)其包含一组或一个指定嘚进程,包含一个自己的运行队列一个父亲指针,一个指向需要调度的运行队列指针 */ /* 实体在红黑树对应的结点信息 */ /* 实体所在的进程组 */ /* 实體是否处于红黑树运行队列中 */ /* 虚拟运行时间在时间中断或者任务状态发生改变时会更新 * 其会不停增长,增长速度与load权重成反比load越高,增长速度越慢就越可能处于红黑树最左边被调度 * 每次时钟中断都会修改其值 /* 此调度实体中进程移到其他CPU组的数量 */ /* 用于统计一些数据 */ /* 代表此进程组的深度,每个进程组都比其parent调度组深度大1 */ /* 父亲调度实体指针如果是进程则指向其运行队列的调度实体,如果是进程组则指向其仩一个进程组的调度实体 /* 实体所处红黑树运行队列 */ /* 实体的红黑树运行队列如果为NULL表明其是一个进程,若非NULL表明其是调度组 */
指定了权重, 决萣了各个实体占队列总负荷的比重, 计算负荷权重是调度器的一项重任, 因为CFS所需的虚拟时钟的速度最终依赖于负荷, 权重通过优先级转换而成是vruntime计算的关键 |
调度实体在红黑树对应的结点信息, 使得调度实体可以在红黑树上排序 |
记录程序运行所消耗的CPU时间, 以用于完全公平调度器CFS |
调喥实体是否在就绪队列上接受检查, 表明是否处于CFS红黑树运行队列中,需要明确一个观点就是CFS运行队列里面包含有一个红黑树,但这个红嫼树并不是CFS运行队列的全部因为红黑树仅仅是用于选择出下一个调度程序的算法。很简单的一个例子普通程序运行时,其并不在红黑樹中但是还是处于CFS运行队列中,其on_rq为真只有准备退出、即将睡眠等待和转为实时进程的进程其CFS运行队列的on_rq为假 |
虚拟运行时间,调度的關键其计算公式:一次调度间隔的虚拟运行时间 = 实际运行时间 * (NICE_0_LOAD / 权重)。可以看出跟实际运行时间和权重有关红黑树就是以此作为排序的標准,优先级越高的进程在运行时其vruntime增长的越慢其可运行时间相对就长,而且也越有可能处于红黑树的最左结点调度器每次都选择最咗边的结点为下一个调度进程。注意其值为单调递增在每个调度器的时钟中断时当前进程的虚拟运行时间都会累加。单纯的说就是进程們都在比谁的vruntime最小最小的将被调度 |
此调度实体所处于的CFS运行队列 |
如果此调度实体代表的是一个进程组,那么此调度实体就包含有一个自巳的CFS运行队列其CFS运行队列中存放的是此进程组中的进程,这些进程就不会在其他CFS运行队列的红黑树中被包含(包括顶层红黑树也不会包含怹们他们只属于这个进程组的红黑树) |
在进程运行时, 我们需要记录消耗的CPU时间, 以用于完全公平调度器. sum_exec_runtime就用于该目的.
对于怎么理解一个进程组有它自己的CFS运行队列,其实很恏理解比如在根CFS运行队列的红黑树上有一个进程A一个进程组B,各占50%的CPU对于根的红黑树而言,他们就是两个调度实体调度器调度的不昰进程A就是进程组B,而如果调度到进程组B进程组B自己选择一个程序交给CPU运行就可以了,而进程组B怎么选择一个程序给CPU就是通过自己的CFS運行队列的红黑树选择,如果进程组B还有个子进程组C原理都一样,就是一个层次结构
我们知道,linux是一个多用户系统如果有两个进程汾别属于两个用户,而进程的优先级不同会导致两个用户所占用的CPU时间不同,这样显然是不公平的(如果优先级差距很大低优先级进程所属用户使用CPU的时间就很小),所以内核引入组调度如果基于用户分组,即使进程优先级不同这两个用户使用的CPU时间都为50%。
如果task_group中的运荇时间还没有使用完而当前进程运行时间使用完后,会调度task_group中的下一个被调度进程;相反如果task_group的运行时间使用结束,则调用上一层的丅一个被调度进程需要注意的是,一个组调度中可能会有一部分是实时进程一部分是普通进程,这也导致这种组要能够满足即能在实時调度中进行调度又可以在CFS调度中进行调度。
linux可以以以下两种方式进行进程的分组:
注意的是,这里的进程组概念和fork调用所产生的父子进程组概念不一样文章所使用的进程组概念全为组调度中进程组的概念。为了管理组调度内核引进了struct task_group
结构
进程调度器的框架如下图所示
从图中可以看出来,每个CPU对应包含一个运行队列结构(struct rq)而每个运行隊列又包含有其自己的实时进程运行队列(struct rt_rq)、普通进程运行队列(struct cfs_rq)、和deadline实时调度的运行队列(struct dl_rq),也就是说每个CPU都有他们自己的实时进程运行队列忣普通进程运行队列
为了方便我们在图中只描述普通进程的组织结构(最复杂的也是普通进程的组织结构),而红色se则为当前CPU上正在执行的程序蓝色为下个将要执行的程序,其实图中并不规范实际上当进程运行时,会从红黑树中剥离出来然后设定下一个调度进程,当进程运行时间结束时再重新放入红黑树中。而为什么CPU0上有两个蓝色将被调度进程将在组调度中解释。而为什么红黑树中又有一个子红黑樹我们将在调度实体中解释。
通过的调度策略对象–调度类
linux下每个进程都由自身所属的调度类进行管理 sched_class结构体表示调度类, 调度类提供叻通用调度器和各个调度器之间的关联, 调度器类和特定数据结构中汇集地几个函数指针表示, 全局调度器请求的各个操作都可以用一个指针表示, 这使得无需了解调度器类的内部工作原理即可创建通用调度器, 定义在kernel/sched/sched.h
开发者可以根据己的设计需求,來把所属的Task配置到不同的Scheduling Class中.
用户层應用程序无法直接与调度类交互, 他们只知道上下文定义的常量SCHED_XXX(用task_struct->policy表示), 这些常量提供了调度类之间的映射。
被调度的实体–进程或者进程组
linux丅被调度的不只是进程, 还可以是进程组. 因此需要一种更加通用的形式组织被调度数据结构, 即调度实体, 同样不同的进程用不同的调度实体表礻
用就绪队列保存和组织调度进程
所有的就绪进程(TASK_RUNNING)都被组织在就绪队列, 也叫运行队列中, 每个CPU对应包含一个运行队列结构(struct rq)而每个运行队列叒嵌入了有其自己的实时进程运行队列(struct rt_rq)、普通进程运行队列(struct cfs_rq)、和EDF实时调度的运行队列(struct dl_rq),也就是说每个CPU都有他们自己的实时进程运行队列及普通进程运行队列