当系统软件调用schedule放弃cpu的时候会发生进程切换,如果某一cpu消耗性进程长时间不调用schedule,我们如何推进调度器的运行来保障系统性能呢?这就是本文要分析的cfs对linux周期性调度的支持。
CFS调度器并不会显示地为进程分配对应的时间片,相应地CFS用周期性调度实现进程的及时切换:在SysTick的中断函数中通过一层层函数调用最终到scheduler_tick函数,周期性地检查当前进程是否耗尽了自己应该分得的一个调度周期里面对应的时间片,并以此为依据决策是否要设置抢占标志,同时设计rq->hrtick_timer配合SysTick提升系统的实时性。

1. SysTick中断调度

1.1 定时器中断scheduler_tick
当系统调度不够频繁的时候,System Tick中断可以继续推进调度器的及时运转,我们首先来看定时器中断入口Timer.c (kernel\time):
CFS对linux周期性调度的实现
在定时器中断中我们会调用update_process_times来更新进程的时间统计信息。内部会调用scheduler_tick执行具体的处理,该函数的执行频率是HZ:
CFS对linux周期性调度的实现
(1)调用调度类实现的task_tick方法,针对CFS调度类该函数是task_tick_fair。
(2)触发负载均衡,暂时忽略。
1.2 CFS的周期性调度处理task_tick_fair
CFS对linux周期性调度的实现
for_each_sched_entity是对当前进程的group层级进行处理,对每一个层级的sched_entity调用entity_tick传入queued=0进行处理:
CFS对linux周期性调度的实现
(1)调用update_curr()更新当前运行的调度实体的虚拟时间等信息
(2)除了System Tick定时器,per-cpu rq还有一个hrtimer在推进调度器的调度工作,如果hrtimer的callback正在执行,那么这个时候我们系统定时器的周期调度就此返回,不用继续执行下面进一步的检查。
(3)如果就绪队列的调度实体个数大于1需要检查是否满足抢占条件,如果可以抢占就设置TIF_NEED_RESCHED flag。

1.3 判断当前进程是否可以被抢占
CFS对linux周期性调度的实现
(1)sched_slice计算curr进程在本次调度周期中应该分配的时间片。时间片用完就应该被抢占,计算方式为:
CFS对linux周期性调度的实现
CFS对linux周期性调度的实现
根据当前就绪进程个数计算调度周期,当cfs_rq上的进程数不超过8情况下,调度周期默认6ms。进程数超过8的时候调度周期是进程数乘以0.75ms,也就是每个进程分配0.75ms的时间片。for循环根据se->parent层级往上计算比例。获得se依附的cfs_rq的负载信息。计算slice = slice * se->load.weight / cfs_rq->load.weight的值。
(2)delta_exec是当前进程已经运行的实际时间。
(3)如果实际运行时间已经超过分配给进程的时间片,自然就需要抢占当前进程。设置TIF_NEED_RESCHED flag。
(4)为了防止频繁过度抢占,我们应该保证每个进程运行时间不应该小于最小粒度时间sysctl_sched_min_granularity=0.75ms因此如果运行时间小于最小粒度时间,不应该抢占。
(5)从红黑树中找到虚拟时间最小的调度实体。如果当前进程的虚拟时间仍然比红黑树中最左边调度实体虚拟时间小,也不应该发生调度。
(6)如果当前进程的虚拟运行时间比ideal_runtime加红黑树最左端进程的虚拟运行时间的和还大,那么也需要调用resched_curr设置TIF_NEED_RESCHED flag。此时可以理解为curr进程已经优先于left进程多运行了一个周期, 而left又是红黑树总最饥渴的那个进程, 因此curr进程已经远远领先于队列中的其他进程, 此时应该补偿其他进程。

**小结:**系统定时器中断中我们会更新当前运行进程的时间统计信息,同时判断当前进程是不是应该被抢占,判断的依据是其真实运行时间是否超过按权重计算出的一个调度周期内分配到的时间,如果超过了那么就调用resched_curr设置TIF_NEED_RESCHED flag。

1.4 从resched_curr到进程切换
上面分析到,当系统时钟中断scheduler_tick中判断需要立马抢占当前进程的时候,是通过调用resched_curr设置当前进程的TIF_NEED_RESCHED flag,那么设置该flag之后当前进程何时会被切出去呢?
我们在系统时钟中断中设置当前进程的TIF_NEED_RESCHED flag,然后在中断返回时调用schedule,执行进程切换。
内核__schedule函数头上面给出的解释是:
CFS对linux周期性调度的实现

2. schedule中的延迟调度(rq->hrtick_timer)

在start_kernel->sched_init流程中,我们会调用init_rq_hrtick为每一颗cpu的rq初始化一个hrtimer,这个feature受控于CONFIG_SCHED_HRTICK:
CFS对linux周期性调度的实现
2.1 per-cpu rq的hrtimer回调函数
CFS对linux周期性调度的实现
也就是说hrtimer到时后,我们仍然会通过调度器的task_tick回调推进调度器向前执行,执行流程跟系统定时器类似。不一样的地方是这里传给entity_tick的queued=1,这样在entity_tick里面不会根据当前进程的运行时间长短来判断是否该抢占当前进程,而是更新完当前进程的时间统计信息后直接调用resched_curr来设置TIF_NEED_RESCHED flag:
CFS对linux周期性调度的实现
由于系统定时器周期调度在系统HZ比较小的时候无法保证实时性,这样的设计可以通过动态改变hrtimer的超时时间来改善调度器的实时性。下面来分析这个hrtimer的调用流程。
2.2 rq->hrtick_timer的延迟调度
(1)cfs的enqueue/dequeue操作最后需要检查是否要发起延迟调度
CFS对linux周期性调度的实现
CFS对linux周期性调度的实现
CFS对linux周期性调度的实现
在enqueue和dequeue操作返回前,如果cfs的就绪队列上面进程数量足够少,那么我们就调用hrtick_start_fair检查是否需要发起延迟调度抢占rq->curr。
之所以在dequeue/enqueue的最后要设计hrtick_timer去推进调度器,我的想法是:在dequeue/enqueue的最后,rq->curr很可能已经运行了一段时间,如果我们要等到System Tick到才去切换进程,那很有可能rq->curr已经运行超过0.75ms了。所以我们需要另外一个hrtimer来推进进程切换,而dequeue/enqueue正好是比较方便去检查rq->curr运行时间进而start hrtick_timer的时间点。这应该是也是提升实时性的一个操作。

(2)本次调度返回新进程前检查是否要发起延迟调度
在本次调度执行pick_next_task_fair中返回新进程p前我们也需要调用hrtick_start_fair检查是否需要发起延迟调度抢占被选中的新进程p:
CFS对linux周期性调度的实现
这样设计的目的是:如果选出来的进程p已经运行了一段时间,那么它应该在比较短的一段运行时间(小于0.75ms)之后被抢占,如果不用hrtick_timer,cfs会默认让p再运行0.75ms,这样就导致一个调度周期内p运行的时间超过0.75ms,所以设计hrtick_timer的目的就是让p运行时间到0.75ms的时候能让出cpu。
(3)延迟调度检查
延迟调度检查调用函数hrtick_start_fair实现:
CFS对linux周期性调度的实现
这里的检查基本跟系统定时器中断中的检查逻辑类似:首先就绪队列中的进程数量要在一个以上,这个时候我们会用sched_slice结合进程优先级算出一个理想的运行时间slice,如果这个时间小于进程真实运行时间ran,这个时候需要判断cfs选出来的进程p是否就是当前正在运行的进程rq->curr,如果是那么调用resched_curr设置TIF_NEED_RESCHED立即抢占,否则什么也不做直接返回。如果理想运行时间slice大于真实运行时间ran,那么调用hrtimer在slice - ran后触发中断执行抢占。
CFS对linux周期性调度的实现
首先确保延迟的时间在10us以上,传入的delay是新进程p在本调度周期内所剩的运行时间。然后调用__hrtick_restart启动hrtimer:
CFS对linux周期性调度的实现

3. cfs周期性调度总体框架

根据以上分析,cfs的周期性调度框架总结如下:
CFS对linux周期性调度的实现
(1)除了明确的schedule触发的进程切换(如mutex、semaphore、qaitqueue等),linux系统时钟中断scheduler_tick也会推进调度器执行更实时的调度,另外我们设计rq->hrtick_timer提供延迟调度的能力,这个更进一步提升了cfs的实时性。
(2)系统时钟周期调度会对当前中断cpu的rq进行检查,进而判断当前运行的进程是否要在中断结束的时候被抢占,这在一定程度确保了所有进程公平地分享cpu。
(3)另外在调度的过程pick_next_task_fair中(schedule选出新进程之后),以及在cfs enqueue/dequeue的末尾,我们也会检查是否需要延迟调度,如果需要我们就会设置超时时间delta启动一个hrtick_timer,这个时间超时后会主动发起进程切换。
(4)Hrtick_timer存在的理由是:System Tick定时器在HZ=1000的系统中调度时间粒度是1ms(在HZ更小的系统中这个时间粒度会更长),我们不希望每次都等到System Tick到时之后才去切换进程,所以我们设计hrtick_timer来推进调度器的运行。在dequeue/enqueue/ pick_next_task_fair的合适位置,我们会计算rq->curr距离0.75ms的单次运行时间还差多少,进而**hrtick_timer保证rq->curr在0.75ms的运行时间到时的时候能被置换出去,个人认为这是cfs公平性和提升实时性的一个设计。

相关文章:

  • 2021-05-05
  • 2022-12-23
  • 2022-02-23
  • 2021-06-15
  • 2021-12-08
  • 2021-05-17
  • 2021-11-26
  • 2021-11-28
猜你喜欢
  • 2021-06-11
  • 2021-07-04
  • 2021-08-28
  • 2022-12-23
  • 2022-12-23
  • 2021-09-20
  • 2021-09-22
相关资源
相似解决方案