linux进程调度 - 完全公平调度类

时间:2021-10-15 03:51:33

完全公平调度类是调度类的一个实例

static const struct sched_class fair_sched_class = {
    .next           = &idle_sched_class,
    .enqueue_task       = enqueue_task_fair,
    .dequeue_task       = dequeue_task_fair,
    .yield_task     = yield_task_fair,

    .check_preempt_curr = check_preempt_wakeup,

    .pick_next_task     = pick_next_task_fair,
    .put_prev_task      = put_prev_task_fair,


    .set_curr_task          = set_curr_task_fair,
    .task_tick      = task_tick_fair,
    .task_new       = task_new_fair,
};

在主调度器和周期调度器中会根据进程类型调用完全公平调度类或者实时调度类的函数。

@next,指向空闲调度类,而实时调度类的next则指向完全公平调度类。这个顺序在编译之前已经建立好了,不会在系统运行时动态创建。

@enqueue_task_fair,向就绪队列添加一个新进程,在进程从睡眠状态变为可运行状态时,即发生该操作。

@dequeue_task,将一个进程从就绪队列去除,在进程从可运行状态切换到不可运行状态时,即发生该操作。虽然这里称为一个队列,但是内部的具体实现,完全是调度器类决定的。

@yield_task,进程可以通过系统调用sched_yield自动放弃执行,此时内核会调用调度器类的yield_task函数。

@check_preempt_curr,在必要的情况下,会调用这个函数来抢占当前的进程。

@pick_next_task,用于选择下一个即将运行的进程

@put_prev_task,在用另外一个进程代替当前运行的进程之前调用

@set_curr_task,在当前进程的调度策略发生变化时调用,那么需要调用这个函数改变CPU的当前task

@task_tick,在每次激活周期性调度器时,由周期性调度器调用

@new_task,当系统创建新的task时,需要通过这个函数通知调度器类


数据结构

主调度器的每个就绪队列中都嵌入了一个该结构的实例:

 230 /* CFS-related fields in a runqueue */
 231 struct cfs_rq {
 232     struct load_weight load;
 233     unsigned long nr_running;
 234 
 235     u64 exec_clock;
 236     u64 min_vruntime;
 237 
 238     struct rb_root tasks_timeline;
 239     struct rb_node *rb_leftmost;
 240     struct rb_node *rb_load_balance_curr;
 241     /* 'curr' points to currently running entity on this cfs_rq.
 242      * It is set to NULL otherwise (i.e when none are currently running).
 243      */
 244     struct sched_entity *curr;
 245 
 246     unsigned long nr_spread_over;
 262 };

nr_running计算了队列上可运行进程的数目,load则维护了所有这些进程的累积值。我们会通过load来计算虚拟时钟

exec_clock 仅具有统计做的实际运行时间。

min_vruntime计算了队列上所有进程的最小虚拟运行时间。

tasks_timeline 是红黑树的树节点,这棵红黑树管理所有进程,按照这些进程的虚拟运行时间进行排序,最左边的进程是虚拟运行时间最小的进程,也是最需被调用的进程。

rb_leftmost 指向这棵树最左边的节点,实际上我们可以通过遍历tasks_timeline找到这个节点。

curr 指向cfs_rq中当前可执行进程的调度实体


CFS工作原理

完全公平调度算法依赖于虚拟时钟,用以度量等待进程在完全公平系统中所能得到的CPU时间。但数据结构中并没有虚拟时钟的表示,这是因为虚拟时钟可以通过实际时钟,以及与每个进程相关的负荷权重计算出来。

所有和虚拟时钟相关的计算都在update_curr中进行的,该函数在系统中各个不同的地方调用。

 336 static void update_curr(struct cfs_rq *cfs_rq)
 337 {
 338     struct sched_entity *curr = cfs_rq->curr;
 339     u64 now = rq_of(cfs_rq)->clock;
 340     unsigned long delta_exec;
 341 
 342     if (unlikely(!curr))
 343         return;
 344 
 345     /*
 346      * Get the amount of time the current task was running
 347      * since the last time we changed load (this cannot
 348      * overflow on 32 bits):
 349      */
 350     delta_exec = (unsigned long)(now - curr->exec_start);
 351 
 352     __update_curr(cfs_rq, curr, delta_exec);
 353     curr->exec_start = now;
 354 
 355     if (entity_is_task(curr)) {
 356         struct task_struct *curtask = task_of(curr);
 357 
 358         cpuacct_charge(curtask, delta_exec);
 359     }
 360 }

339 rq_of(cfs_rq)->clock 用于实现就绪队列自身的时钟,每次调用周期性调度器时,都会更新clock的值

350 curr->exec_start保存了上次更改load时的时间,注意并不是进程的上一次运行时间。当前进程在一次运行过程中,可能会发生多次update_curr


__update_curr

 304 static inline void
 305 __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
 306           unsigned long delta_exec)
 307 {
 308     unsigned long delta_exec_weighted;
 309     u64 vruntime;
 310 
 311     schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));
 312 
 313     curr->sum_exec_runtime += delta_exec;
 314     schedstat_add(cfs_rq, exec_clock, delta_exec);
 315     delta_exec_weighted = delta_exec;
 316     if (unlikely(curr->load.weight != NICE_0_LOAD)) {
 317         delta_exec_weighted = calc_delta_fair(delta_exec_weighted,
 318                             &curr->load);
 319     }
 320     curr->vruntime += delta_exec_weighted;
 321 
 322     /*
 323      * maintain cfs_rq->min_vruntime to be a monotonic increasing
 324      * value tracking the leftmost vruntime in the tree.
 325      */
 326     if (first_fair(cfs_rq)) {
 327         vruntime = min_vruntime(curr->vruntime,
 328                 __pick_next_entity(cfs_rq)->vruntime);
 329     } else
 330         vruntime = curr->vruntime;
 331 
 332     cfs_rq->min_vruntime =
 333         max_vruntime(cfs_rq->min_vruntime, vruntime);
 334 }

313 sum_exec_runtime 该进程消耗的CPU时间累积值,@delta_exec是上一次更新负荷统计量时两次的差值,二者都是真实时间。

316 如果进程的优先级为120(nice = 0),那么虚拟时间和物理时间相同,否则通过calc_delta_mine计算虚拟执行时间

326 first_fait检测树上是否有最左边的节点,即是否有进程在树上等待调度

332 cfs_rq->min_vruntime是单调增加的

在运行过程中,进程调度实体的vruntime是单调增加的,优先级越高的进程,增加的速度越慢,因此他们向右移动的速度也就越慢。这样被调度的机会就越大。


延迟跟踪

内核有一个固有的概念,称之为调度延迟,保证每个进程至少被运行一次的时间间隔。

sysctl_sched_latency

参数用来控制这个行为,缺省定义为20ms,可以通过/proc/sys/kernel/sched_latency_ns来控制。

sched_nr_latency

控制在一个延迟周期内处理的最大活动数目。如果活动进程的数据超过了该上限,则延迟周期也成比例的线性扩展。

__sched_period

延迟周期的长度,通常就是sysctl_sched_latency,但是如果有更多的进程正在运行,那么其值要通过如下公式计算:

__sched_period = sysctl_sched_latency * nr_running / sched_nr_latency


在一个延迟周期内,通过考虑各个进程的权重,将延迟周期的时间在活动进程之间进行分配。对于有某个调度实体表示的给定进程,分配到的时间如下计算。

static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    u64 slice = __sched_period(cfs_rq->nr_running);

    slice *= se->load.weight;
    do_div(slice, cfs_rq->load.weight);

    return slice;
}