linux内核普通进程CFS调度原理

[摘要]

[正文一] linux调度系统概述

[正文二] 调度过程

[正文三] 普通进程调度与实时进程调度对比

[正文四] CFS调度器

[总结]

 

注意:请使用谷歌浏览器阅读(IE浏览器排版混乱)

 

[摘要]

本文主要介绍linux内核普通进程的调度过程,即CFS调度器的原理。

请先阅读:linux内核实时进程调度原理,本文的正文一、正文二两部分内容和linux系统实时进程调度过程一文部分内容是相同的。

进程的创建、切换等过程请参考另一篇文档:linux系统进程的创建。

要了解本文需要清楚linux系统与调度相关的时间的概念,请参考博文:linux系统调度之时间 。

[正文一] linux调度系统概述

1 linux系统中常用的调度器:

实时进程(包括SCHED_RR/SCHED_FIFO)基于优先级队列进行调度。

实时进程调度类:rt_sched_class

普通进程(包括SCHED_NORMAL)使用CFS调度器进行调度。

普通进程调度类:fair_sched_class

2 系统中调度策略的定义:

#define SCHED_NORMAL                      0
#define SCHED_FIFO                        1
#define SCHED_RR                          2

3 调度的关键:

1)无论实时进程还是普通进程,调度的关键都在于调度的时机、下一个进程的选取、优先级队列(实时进程中使用)或红黑树的维护(普通进程使用)。

2)对于实时进程来说,下一个进程是从优先级队列上选取的,选择的标准和优先级队列的维护最为关键。

[正文二] 调度过程

调度的发生

当前进程需要被调度的标记:主要讨论当前进程在什么情况下被调度。

1>当前进程主动调度:由当前进程主动调用schedule()进行调度。

举例:比如调用msleep时,系统会启用一个定时器然后主动调度走(即schedule_timeout)。等待互斥锁和睡眠类似。

2>当前进程被动调度:当前进程或中断上下文中被动调度(此处仅仅设置调度标志,并未真正调度否则无法返回中断上下文):

关键代码:wak_up_process->resched_task->set_tsk_need_resched

通过wake_up系列函数主动唤醒某个进程。当系统调用、中断处理返回时,操作系统会在do_work_pending中检查这个标记,如果被设置,则调用schedule()进行调度。

1msleep到期时,定时器中断下文处理函数会wake_up_process .

唤醒睡眠进程: process_timeout->wake_up_process->try_to_wake_up->ttwu_queue-> ttwu_do_activate();

ttwu_do_activate(struct rq *rq, struct task_struct*p, int wake_flags)
{
/*
->activate_task激活进程p,
将p的调度实体(sched_entity)添加到运行队列上,接下来再设置需要调度的标记 .
*/
        ttwu_activate(rq,p, ENQUEUE_WAKEUP | ENQUEUE_WAKING);
/*
->check_preempt_curr->check_preempt_curr_rt->resched_task把当前进程标记为TIF_NEED_RESCHED .
*/
        ttwu_do_wakeup(rq, p, wake_flags);
}

注意此处的调度并不是真正完成上下文切换,而是设置调度标记.

2:新进程创建后(do_fork),也会被唤醒(wake_up_new_task):

void wake_up_new_task(struct task_struct *p)
{
/*
wake_up_new_task->activate_task->enqueue_task->enqueue_task_rt->enqueue_rt_entity
-> activate_task激活进程p,将p的调度实体sched_entity添加到运行队列上
*/
        activate_task(rq,p, 0);
        p->on_rq= 1;
/*
->check_preempt_curr->check_preempt_curr_rt->resched_task
把当前进程标记为TIF_NEED_RESCHED
*/
        check_preempt_curr(rq,p, WF_FORK);
}

其实从以上两个例子可以看到wake_up系列函数,完成两个主要功能:

一 标记当前进程需要被调度;

二 将被唤醒的进程添加到优先级队列,以便schedule()在选取下一个进程运行时,有机会选择到。注意此处:对于实时进程来说,不是添加到优先级队列就一定会被调度选择到,这还与进程的优先级相关,这一点和cfs调度器有明显区别,cfs策略在一个调度周期内所有进程都有机会被调度到,只是运行时间不同,与nice值有关prio_to_weight()。

3>实时进程的时间片耗尽时,当前进程会调度走。

关键代码:task_tick_rt->set_tsk_need_resched

进程时间片管理:定时器中完成进程时间片的管理:

(参考代码update_process_times->scheduler_tick->task_tick_rt)此处可以参考下面优先级队列的维护。

4>设置调度标记:

关键代码实现:set_tsk_need_resched;

系统通过set_tsk_need_resched接口标记tsk进程需要被调度,这个接口一般不直接调用,而是通过如下两个路径实现。

1) resched_task->set_tsk_need_resched

2) task_tick_rt->set_tsk_need_resched

注意:实时进程中直接调用了set_tsk_need_resched,普通进程时通过调用:task_tick_fair->resched_task实现的。

2 scheudle调度过程

上面介绍了当前进程需要被调度的标志设置的时机。事实上设置标记时,并未马上发生调度,而是当系统调用、中断处理返回时,操作系统会在do_work_pending中检查这个标记,如果被设置,则调用schedule()进行调度。下面就开始介绍关键的schedule函数,尽管这个函数有点长,但是因为其重要性,还是要拿出来介绍下:

static void __sched __schedule(void)
{
        structtask_struct *prev, *next;
        unsignedlong *switch_count;
        structrq *rq;
        intcpu;
        
need_resched:
        preempt_disable();
        cpu =smp_processor_id();
        rq =cpu_rq(cpu);
        rcu_note_context_switch(cpu);
/*运行队列上的当前进程,这个进程将要让出cpu给下一个进程*/
        prev =rq->curr;
/* spin_lock原子状态下发生调度,会有告警错误 */
        schedule_debug(prev);
 
        if(sched_feat(HRTICK))
                  hrtick_clear(rq);
 
        /*
         * Make sure thatsignal_pending_state()->signal_pending() below
         * can't be reordered with__set_current_state(TASK_INTERRUPTIBLE)
         * done by the caller to avoid the race withsignal_wake_up().
         */
        smp_mb__before_spinlock();
        raw_spin_lock_irq(&rq->lock);
/*当前进程的被切换次数(分为主动切换和被动切换),初始化为被动切换*/
        switch_count= &prev->nivcsw;
/*
非正在运行且没有被抢占的进程认为是主动切换的即prev->state非0;
(prev->state=TASK_RUNNING=0即被动切换)
比如msleep->schedule_timeout_uninterruptible中:
__set_current_state(TASK_UNINTERRUPTIBLE);
 
*/    
        if(prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
                  if(unlikely(signal_pending_state(prev->state, prev))) {
                           prev->state= TASK_RUNNING;
                  }else {
/*
schedule过程中,如果是自愿调度走,
进程的调度实体也需要从active队列上删除    
如果是抢占的则不从运行队列优先级队列active上删除
*/
                           deactivate_task(rq,prev, DEQUEUE_SLEEP);
                           prev->on_rq= 0;
 
                           /*
                            * If a worker went to sleep, notify and askworkqueue
                            * whether it wants to wake up a task tomaintain
                            * concurrency.
                            */
                           if(prev->flags & PF_WQ_WORKER) {
                                    structtask_struct *to_wakeup;
 
                                    to_wakeup= wq_worker_sleeping(prev, cpu);
                                    if (to_wakeup)
                                              try_to_wake_up_local(to_wakeup);
                           }
                  }
                  switch_count= &prev->nvcsw;
        }
 
        pre_schedule(rq,prev);
 
        if(unlikely(!rq->nr_running))
                  idle_balance(cpu,rq);
 
#ifndef gSysDebugInfoSched           
/*
1 rt_sched_class->put_prev_task_rt
  put_prev_task_rt->update_curr_rt更新进程的运行时间
  有可能resched_task
2 fair_sched_class->put_prev_task_fair
*/
#endif
        put_prev_task(rq,prev);
/*
1 rt_sched_class->pick_next_task_rt
在rt_prio_array即rt_rq->active;上选择优先级最高的进程开始运行
并更新进程在一个时钟中断中开始运行的时间exec_start
2 fair_sched_class->pick_next_task_fair
*/
        next =pick_next_task(rq);

        clear_tsk_need_resched(prev);
        rq->skip_clock_update= 0;
 
        if(likely(prev != next)) {
                  rq->nr_switches++;
#ifndef gSysDebugInfoSched           
/*rq->curr表示当前运行的进程,在此初始化*/
#endif
                  rq->curr= next;
                  ++*switch_count;
/*真正实现进程上下文切换*/
                  context_switch(rq,prev, next); /* unlocks the rq */
                  /*
                   * The context switch have flipped the stackfrom under us
                   * and restored the local variables which weresaved when
                   * this task called schedule() in the past.prev == current
                   * is still correct, but it can be moved toanother cpu/rq.
                   */
                  cpu= smp_processor_id();
                  rq= cpu_rq(cpu);
        } else
                  raw_spin_unlock_irq(&rq->lock);
 
        post_schedule(rq);
 
        sched_preempt_enable_no_resched();
        if(need_resched())
                  goto need_resched;
}

本节给出schedule的全貌,具体在以下章节中分步骤分析。

3 scheudle调度过程中下一个进程的选择.

关键代码实现:__schedule–>pick_next_task(rq)->pick_next_rt_entity;

static struct sched_rt_entity *pick_next_rt_entity(struct rq *rq,struct rt_rq *rt_rq)
{
/*
rt_rq表示Cpu上实时进程的运行队列
rt_rq->active表示就绪的实时进程优先级队列,即队列上的进程可被选择运行。其中:
struct rt_prio_array {
        DECLARE_BITMAP(bitmap,MAX_RT_PRIO+1);
        struct list_head queue[MAX_RT_PRIO];
rt_rq->active有MAX_RT_PRIO(100)个链表,每个实时进程的优先级都对应一个
};
*/
        struct rt_prio_array *array = &rt_rq->active;
        struct sched_rt_entity *next = NULL;
        struct list_head *queue;
        int idx;
/*最高优先级*/
        idx =sched_find_first_bit(array->bitmap);
        BUG_ON(idx>= MAX_RT_PRIO);
/*在最高优先级队列上的链表中选择调度一个实体*/
        queue =array->queue + idx;
        next =list_entry(queue->next, struct sched_rt_entity, run_list);
 
        return next;
}

此处提出几个问题:schedule从优先级队列上选择下一个将要运行的进程,那么:

问题一:优先级队列上这些进程是如何添加上去的?

问题二:进程何时添加到优先级队列上以供调度,又何时从队列上移除,退出调度呢?

问题三:如果一个实时进程没有主动调度走(没有睡眠,没有等待互斥锁,也没有主动调用schedule),那么系统还会让出cpu给低优先级的实时进程或普通进程么?

[正文三] 普通进程调度与实时进程调度对比

以上两部分和linux内核实时进程调度原理一文介绍的相同。

如果分析完实时进程的调度原理,再来分析普通进程会方便很多,因为有一部分内容是公用的,只要着重介绍下cfs调度器就可以了。

1 对比实时进程调度与普通进程有几点不同:

1> schedule过程:

实时进程运行队列上选择下一个运行进程:pick_next_task–>pick_next_task_rt;

普通进程运行队列上选择下一个运行进程:pick_next_task–>pick_next_task_fair

2> 对实时进程运行队列的维护:

将调度实体添加到运行队列:wake_up_process->activate_task->enqueue_task_rt

将调度实体从运行队列删除:schedule调度过程中如果是主动调度,需要把当前进程从调度队列上删除:deactivate_task ->dequeue_task_rt

对普通进程运行队列的维护:

将调度实体添加到运行队列:wake_up_process->activate_task->enqueue_task_fair

将调度实体从运行队列删除:schedule调度过程中如果是主动调度,需要把当前进程从调度队列上删除:deactivate_task ->dequeue_task_fair

3> 调度器时间的维护:

对实时进程来说,当前进程时间片耗尽后,将当前进程从调度队列上移除,再添加到队列尾,并设置需要调度的标志。scheduler_tick->task_tick_rt中完成。

对普通进程来说,cfs调度器相关时间在scheduler_tick->task_tick_fair中完成。 

[正文四] CFS调度器

cfs常用概念(complete fair schedule)

要想弄清楚cfs调度策略,必须要先了解几个关键概念:

1 分配给进程的实际运行时间=调度周期 *进程权重 /所有进程权重之和 (公式1); (sysctl_sched_latency是默认的调度周期)

普通进程有一个权重的概念,我们知道普通进程是通过nice来调整期优先级的,系统中每个nice对应一个权重:static const int prio_to_weight[40];

观察prio_to_weight,nice值越小,优先级越大,权重越大,根据公式1:分配给进程的运行时间也越大.

2 虚拟时间vruntime: cfs公平是体现在另外一个量上面,叫做virtual runtime(vruntime),它记录着进程已经运行的时间.

vruntime =实际运行时间 * 1024 /进程权重 (公式2);这里直接写的1024,实际上它等于nice为0的进程的权重,代码中是NICE_0_LOAD.

vruntime = (调度周期 *进程权重 /所有进程总权重) *1024 /进程权重=调度周期 * 1024 /所有进程总权重(公式3);

从公式3可知:在一个调度周期内,不同进程的vruntime是相同的(即进程的运行时间是相同的,不过这个运行时间是虚拟的运行时间)

这个虚拟的运行时间和真实时间的区别。

从上面几个公式可以看到,不同进程的虚拟运行时间是相同的,但真实时间是不同的。

权重越大的进程(即nice值越小,优先级越大)vruntime增加相同的值所对应的真实时间越长,即优先级越大的进程,到达一个周期内的虚拟运行时间所需的真实时间越多,即实际运行时间越长。

所以虽然在一个调度周期内,不同进程的vruntime是相同的,所有普通进程都有机会运行,且运行相同的虚拟时间,不过优先级低的进程虚拟时间耗费的快.同时每个进程花费的真实时间是不同的,普通进程运行花费的真实时间的总和是一个调度周期。

从 vruntime =实际运行时间 * 1024 /进程权重(公式2).可知:

在一个真实的时钟周期内,vruntime的增加量和进程权重成反比,即进程权重越大(即优先级越高)vruntime增长的越少,而调度算法总是选择vruntime最小的进程进行调度,所以权重越大得到cpu的机会越高。

二. CFS调度的实旨就是选择vrntime最小的进程进行调度

   对优先级高的进程(权重大),在一个时钟周期内,vruntime增长慢(比如vruntime增长了1),因为增长的少,所以调度器选择vruntime最小进程时,还有很大概率选择到这个进程.即使没选择到,因为vruntime小,很快也会再被选择。从而优先级高的进程运行机会更多.

   而对应优先级低的进程,一个调度周期内vruntime增长的多(比如增长了10),因为增长的多,所以调度器选择vruntime最小进程时,有很大概率选择不到当前进程,因为当前进程的vruntime在一个时钟周期内增长的多,从而优先级低的进程运行机会少.

Cfs调度器的一个关键即是vruntime的更新时机,下面介绍.

三.cfs调度代码分析.

1 普通进程权重设置

1)设置调度策略时:__setscheduler->set_load_weight;set_user_nice->set_load_weight

2)进程创建时:copy_process->sched_fork->set_load_weight

3)sched_init->set_load_weight时初始化init_task进程的权重。

static void set_load_weight(struct task_struct *p)
{
 int prio = p->static_prio - MAX_RT_PRIO;
 /*进程的权重可以通过task_struct中的sched_entity获取*/
 struct load_weight *load = &p->se.load;
 /*
  * SCHED_IDLE tasks get minimal weight:
  */
 if (p->policy == SCHED_IDLE) {
  load->weight = scale_load(WEIGHT_IDLEPRIO);
  load->inv_weight = WMULT_IDLEPRIO;
  return;
 }
 /*
 权重跟进程nice值之间有一一对应的关系,可以通过全局数组prio_to_weight来转换,nice值越大,权重越低.
 #define scale_load(w) (w)
 */
 load->weight = scale_load(prio_to_weight[prio]);
 load->inv_weight = prio_to_wmult[prio];
}

2 place_entity()中更新进程的vruntime值,以便把他插入红黑树。

更新进程的vruntime值时机:

1>进程创建时计算进程初始的vruntime值copy_process->sched_fork->task_fork=task_fork_fair->place_entity(cfs_rq,se, 1);

2>唤醒一个进程时:activate_task(rq,p, 0)->enqueue_task_fair->place_entity

3>时钟中断发生时: (task_tick= task_tick_fair)-> update_curr

static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
 u64 vruntime = cfs_rq->min_vruntime;
 /*
  * The 'current' period is already promised to the current tasks,
  * however the extra weight of the new task will slow them down a
  * little, place the new task so that it fits in the slot that
  * stays open at the end.
  */
/*
initial=1是新建进程时,对vruntime进行初始化:task_fork_fair->place_entity();
initial=0是唤醒进程时,对vruntime进行初始化:active_task->enqueue_task_fair->enqueue_entity->place_entity();
1) 进程创建fork时,根据权重计算初始vruntime值:
假设新进程都能获得最小的vruntime(min_vruntime),那么新进程会第一个被调度运行,
这样就能通过不断的fork新进程来让自己的程序一直占据CPU,这显然是不合理的,
这跟以前使用时间片的内核中父子进程要平分父进程的时间片是一个道理。
2) 此处计算进程的初始vruntime。
它以cfs队列的min_vruntime为基准,再加上进程在一次调度周期中所增长的vruntime。
再解释下min_vruntime,这是每个cfs队列一个的变量,它一般小于等于所有就绪态进程
的最小vruntime,也有例外,比如对睡眠进程进行时间补偿会导致vruntime小于min_vruntime。
3) sched_vslice算出进程在一个调度周期内应分配的实际cpu时间,再把它转化为vruntime。
把这个sched_vslice(cfs_rq, se)加在vruntime(min_vruntime)之后,就相当于认为新进程在这一轮调度中已经运行过了。
*/
 if (initial && sched_feat(START_DEBIT))
  vruntime += sched_vslice(cfs_rq, se);
 /* sleeps up to a single latency don't count. */
 if (!initial) {
  unsigned long thresh = sysctl_sched_latency;
  /*
   * Halve their sleep time's effect, to allow
   * for a gentler effect of sleepers:
   */
  if (sched_feat(GENTLE_FAIR_SLEEPERS))
   thresh >>= 1;
/*
此处完成睡眠补偿,thresh是对睡眠进程补偿的虚拟时间,此处是调度周期1/2
下面这条路是唤醒睡眠任务时的代码,我们设想一个任务睡眠了很长时间,它的vruntime就一直不会更新,
这样当它醒来时vruntime会远远小于运行队列上的任何一个任务,于是它会长期占用CPU,这显然是不合理的。
所以这要对唤醒任务的vruntime进行一些调整,我们可以看到,这里是用min_vruntime减去一个thresh,这个thresh的计算过程
就是将sysctl_sched_latency换算成进程的vruntime,而这个sysctl_sched_latency就是默认的调度周期,单核CPU上一般为20ms。
之所以要减去一个值是为了对睡眠进程做一个补偿,能让它醒来时可以快速的到CPU。
*/
  vruntime -= thresh;
 }
 /* ensure we never gain time by being placed backwards. */
 se->vruntime = max_vruntime(se->vruntime, vruntime);
}

place_entity()->sched_vslice()->sched_slice()中计算进程在一个调度周期内应分配到cpu的实际时间.

/*
 * We calculate the vruntime slice of a to-be-inserted task.
 *
 * vs = s/w
 */
static u64 sched_vslice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
 return calc_delta_fair(sched_slice(cfs_rq, se), se);
}

place_entity()->sched_vslice()中把进程在一个调度周期内应分配到cpu的实际时间转化为虚拟时间.

/*
 * We calculate the wall-time slice from the period by taking a part
 * proportional to the weight.
 *
 * s = p*P[w/rw]
 */
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
 u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);
 for_each_sched_entity(se) {
  struct load_weight *load;
  struct load_weight lw;
  cfs_rq = cfs_rq_of(se);
  load = &cfs_rq->load;
  if (unlikely(!se->on_rq)) {
   lw = cfs_rq->load;
   update_load_add(&lw, se->load.weight);
   load = &lw;
  }
  slice = calc_delta_mine(slice, se->load.weight, load);
 }
 return slice;
}

sched_slice->__sched_period 该函数返回普通进程的调度周期

 

/* 
 * The idea is to set a period in which each task runs once.
 *
 * When there are too many tasks (sched_nr_latency) we have to stretch
 * this period because otherwise the slices get too small.
 *
 * p = (nr <= nl) ? l : l*nr/nl
*/
static u64 __sched_period(unsigned long nr_running)
{
 /* 一个调度周期/proc/sys/kernel/sched_latency_ns=12ms */
 u64 period = sysctl_sched_latency;
 /* sched_nr_latency=8 */
 unsigned long nr_latency = sched_nr_latency;
 /* 就绪队列上的进程数超过sysctl_sched_latency个,需要重新计算调度周期 */
 if (unlikely(nr_running > nr_latency)) {
  period = sysctl_sched_min_granularity;
  period *= nr_running;
 }
 return period;
}

3 普通进程调度实体中几个关键时间的更新时机

此处可以参考博文:linux系统调度之时间 

1) 与进程调度有关的时间存放在如下结构体,一般task_struct结构中se表示sched_entity(如:current->se.exex_start)

struct sched_entity {
        u64                    exec_start;
        u64                    sum_exec_runtime;
        u64                    vruntime;
        u64                    prev_sum_exec_runtime;
}

其中 实时进程一般只使用exec_start和sum_exec_runtime;普通进程中这几个成员变量都会被使用. sched_entity中时间的初始化函数:

staticvoid __sched_fork(struct task_struct *p)
{
/* se是普通进程的调度实体struct sched_entity */
/*
进程下一次开始执行的时间点,表示进程在一个时钟中断内开始运行的时间,主要用于在时钟中断中计算时间差,
该时间差主要用于计算进程获取cpu的时间(se.sum_exec_runtime累加该时间差:update_curr中rq->clock_task-exec_start).
此时clock_task为时钟中断开始时间,exec_start可以为之前时钟中断中任一时间,且不必是时钟中断开始的时间点。
调度过程选择下一个进程时,会把它初始为运行队列上的时间(运行队列上时间在
Tick时钟中断中更新)即认为此时为进程开始执行的时间。
pick_next_task_fair->set_next_entity(cfs_rq, se);
tick中断中也会更新该值为运行队列上的时间。运行队列时间在进程入队列时已更新 enqueue_task_fair;
调度实体出入队列:enqueue_entity/dequeue_entity
*/
        p->se.exec_start              = 0;
/*进程执行的总时间大小*/
        p->se.sum_exec_runtime                = 0;
/*
进程上一次运行总时间,即醒来时间。调度过程选择下一个进程是初始为
se.sum_exec_runtime, 
进程最近一次已获取cpu的时间:即进程在上一次调度周期中运行的时间.
sum_exec_runtime-prev_sum_exec_runtime
*/
        p->se.prev_sum_exec_runtime      = 0;
        p->se.nr_migrations                 = 0;
        /*进程在一个调度周期内的虚拟调度时间 */
        p->se.vruntime                           = 0;
}

2) 无论普通进程还是实时进程,都会在选择下一进程时更新一个进程在一个时钟中断中开始执行的时间exec_start,下面以

    普通进程为例进行介绍pick_next_task_fair->set_next_entity():

 ps:实时进程在pick_next_task_rt()->__pick_next_task_rt()中更新se.exec_start;

static void set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
 /* 'current' is not kept within the tree. */
 if (se->on_rq) {
  /*
   * Any task has to be enqueued before it get to execute on
   * a CPU. So account for the time it spent waiting on the
   * runqueue.
   */
  update_stats_wait_end(cfs_rq, se);
  __dequeue_entity(cfs_rq, se);
 }
/*
update_stats_curr_start()函数实现:se->exec_start =rq_of(cfs_rq)->clock_task 
调度器在选择下一个进程执行时,会更新exec_start为队列时间rq->clock_task
*/
 update_stats_curr_start(cfs_rq, se);
 cfs_rq->curr = se;
#ifdef CONFIG_SCHEDSTATS
 /*
  * Track our maximum slice length, if the CPU's load is at
  * least twice that of our own weight (i.e. dont track it
  * when there are only lesser-weight tasks around):
  */
 if (rq_of(cfs_rq)->load.weight >= 2*se->load.weight) {
  se->statistics.slice_max = max(se->statistics.slice_max,
   se->sum_exec_runtime - se->prev_sum_exec_runtime);
#endif
/*
prev_sum_exec_runtime表示进程截止目前获取cpu的时间(即执行时间);
可以使用sum_exec_runtime- prev_sum_exec_runtime计算进程最近一次调度内获取cpu使用权的时间。
不过sum_exec_runtime是使用exec_start计算出的时间差的累加。
*/
 se->prev_sum_exec_runtime = se->sum_exec_runtime;
 }

3) 时钟中断中更新:

调度过程在选择下一进程时:将进程的下一次运行时间exec_start,更新为rq_of(cfs_rq)->clock_task,那么clock_task是何时更新的?

clock_task是运行队列上的时间,和rq->clock一样在update_rq_clock中更新.

主要发生在:1>Tick时钟中断中:scheduler_tick->update_rq_clock.2>进程出队列、入队列等操作:enqueue_task->update_rq_clock.

update_process_times->scheduler_tick()->update_rq_clock()

void update_rq_clock(struct rq *rq)
{
 s64 delta;
 if (rq->skip_clock_update > 0)
  return;
/*
 两次相邻两次周期性调度器运行的时间差 */
 delta = sched_clock_cpu(cpu_of(rq)) - rq->clock;
/*
更新运行队列上的时钟:更新rq->clock_task 与rq->clock
*/
 rq->clock += delta;
/*
此处更新rq->clock_task+=delta,默认配置不开情况rq->clock_task=rq->clock;如果CONFIG_IRQ_TIME_ACCOUNTING配置打开,则rq->clock_task需要减去中断上的时间。
所以可以认为rq->clock队列时间每个tick中断都统计在内,甚至包括中断处理的时间;
而rq->clock_task是进程真正占用的时间,只不过很多情况下rq->clock_task=rq->clock,调度过程中使用rq->clock_task .
*/
 update_rq_clock_task(rq, delta);
}

进程出入调度队列时更新activate_task/deactivate_task->enqueue_task/dequeue_task->update_rq_clock:

static void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
 update_rq_clock(rq);
 sched_info_queued(p);
 /* 
 __setscheduler->
 SCHED_NORMAL: enqueue_task_fair 
 SCHED_RR: enqueue_task_rt
 */
 p->sched_class->enqueue_task(rq, p, flags);
}
static void dequeue_task(struct rq *rq, struct task_struct *p, int flags)
{
 update_rq_clock(rq);
 sched_info_dequeued(p);
 p->sched_class->dequeue_task(rq, p, flags);
}
/*wake_up_new_task->*/
void activate_task(struct rq *rq, struct task_struct *p, int flags)
{
 if (task_contributes_to_load(p))
  rq->nr_uninterruptible--;
 /* 
 __setscheduler-> enqueue_task_fair /enqueue_task_rt
 */
 enqueue_task(rq, p, flags);
}
void deactivate_task(struct rq *rq, struct task_struct *p, int flags)
{
 if (task_contributes_to_load(p))
  rq->nr_uninterruptible++;
 dequeue_task(rq, p, flags);
}

4)tick中断处理中的更新:update_process_times->scheduler_tick()->task_tick_fair()->entity_tick->update_curr()

static void update_curr(struct cfs_rq *cfs_rq)
{
 struct sched_entity *curr = cfs_rq->curr;
 u64 now = rq_of(cfs_rq)->clock_task;
 unsigned long delta_exec;
 if (unlikely(!curr))
  return;
 /*
  * Get the amount of time the current task was running
  * since the last time we changed load (this cannot
  * overflow on 32 bits):
  */
 /*
此处参考update_curr_rt中对exec_start的解释;
 */
 delta_exec = (unsigned long)(now - curr->exec_start);
 if (!delta_exec)
  return;
 __update_curr(cfs_rq, curr, delta_exec);
/*更新进程下次运行的起始时间
如果被抢占,下次调度时将会更新
*/ 
 curr->exec_start = now;
 if (entity_is_task(curr)) {
  struct task_struct *curtask = task_of(curr);
  trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
  cpuacct_charge(curtask, delta_exec);
  account_group_exec_runtime(curtask, delta_exec);
 }

更新当前进程运行时间统计数据update_curr()->__update_curr()

/*
 * Update the current task's runtime statistics. Skip current tasks that
 * are not in our scheduling class.
 */
static inline void __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
       unsigned long delta_exec)
{
 unsigned long delta_exec_weighted;
 schedstat_set(curr->statistics.exec_max,
        max((u64)delta_exec, curr->statistics.exec_max));
/* 更新该进程获得cpu执行权的总时间,可参考update_curr_rt函数中sum_exec_runtime累计处的解释。
此处更新进程下一次的运行时间:表示进程在一个时钟中断内开始运行的时间 时钟中断到来时clock_task先在update_rq_clock更新。
然后再用clock_task减去exec_start即获取当前进程在一个时钟中断中获取到cpu的时间。如果exec_start恰好等于上一个时钟中断时
队列上时间clock_task,则表示进程在这个时钟中断中未发生调度。
*/
 curr->sum_exec_runtime += delta_exec;
 schedstat_add(cfs_rq, exec_clock, delta_exec);
/*
calc_delta_fair用来将真实时间转化为虚拟时间。进程的优先级不同,它在系统中的地位(权重)也不同,
进程的优先级越高,它的虚拟时间走的越慢。
*/
 delta_exec_weighted = calc_delta_fair(delta_exec, curr);
/*更新该进程获得cpu执行权的虚拟时间*/
 curr->vruntime += delta_exec_weighted;
 update_min_vruntime(cfs_rq);
}

4 时间片耗尽.普通进程在一个调度周期内,运行时间用完也会发生调度.在一个调度周期内每个普通进程都有机会运行.

实时进程在scheduler_tick->task_tick_rt中判断是否将cpu调度走(判断时间片是否耗尽).

普通进程在也会在scheduler_tick->task_tick_fair-> check_preempt_tick中判断是否将cpu调度走.

static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
 unsigned long ideal_runtime, delta_exec;
 struct sched_entity *se;
 s64 delta;
/*
如上面所讲:sched_vslice得到了进程在一个调度周期内分配到的虚拟时间vruntime,
而sched_slice返回值的就是此进程在一个调度周期中应该运行的时间. 
*/
 ideal_runtime = sched_slice(cfs_rq, curr);
/*
计算进程在当前调度周期内已占用的CPU时间,如果超过了当前调度周期内应该占用的时间(ideal_runtime)则设置TIF_NEED_RESCHED标志,
在退出时钟中断的过程中会调用schedule函数进行进程切换 
*/
 delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
 if (delta_exec > ideal_runtime) {
 /*TIF_NEED_RESCHED置位*/
  resched_task(rq_of(cfs_rq)->curr);
  /*
   * The current task ran long enough, ensure it doesn't get
   * re-elected due to buddy favours.
   */
  clear_buddies(cfs_rq, curr);
  return;
 }
 /*
  * Ensure that a task that missed wakeup preemption by a
  * narrow margin doesn't have to wait for a full slice.
  * This also mitigates buddy induced latencies under load.
  */
 if (delta_exec < sysctl_sched_min_granularity)
  return;
 se = __pick_first_entity(cfs_rq);
 delta = curr->vruntime - se->vruntime;
 if (delta < 0)
  return;
 if (delta > ideal_runtime)
  resched_task(rq_of(cfs_rq)->curr);
}

5 在调度器唤醒一个进程时(通过wake_up系列函数)需要检查抢占.

检查抢占发生在schedule过程,当CFS在调度点选择下一个运行进程时pick_next_task_fair->pick_next_entity

/*
 * Pick the next process, keeping these things in mind, in this order:
 * 1) keep things fair between processes/task groups
 * 2) pick the "next" process, since someone really wants that to run
 * 3) pick the "last" process, for cache locality
 * 4) do not run the "skip" process, if something else is available
 */
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
/*
红黑树中把vruntime最小的调度实体,挂到红黑色的最左节点(即left = cfs_rq->rb_leftmost),这个调度实体就是
cfs调度器在schedule过程时,选择的下一个运行的进程。不过如果有抢占的情况发生,cfs调度器会先选择抢占的
如下:wakeup_preempt_entity中判断是否发生抢占.
*/
 struct sched_entity *se = __pick_first_entity(cfs_rq);
 struct sched_entity *left = se;
 /*
  * Avoid running the skip buddy, if running something else can
  * be done without getting too unfair.
  */
 if (cfs_rq->skip == se) {
  struct sched_entity *second = __pick_next_entity(se);
  if (second && wakeup_preempt_entity(second, left) < 1)
   se = second;
 }
 /*
  * Prefer last buddy, try to return the CPU to a preempted task.
  */
/*
这个函数wakeup_preempt_entity判断是否发生抢占:其中cfs_rq->last和cfs_rq->next是在check_preempt_wakeup中设置的。
返回-1表示新进程vruntime大于当前进程,当然不能抢占,
返回0表示虽然新进程vruntime比当前进程小,但是没有小到调度粒度,一般也不能抢占
返回1表示新进程vruntime比当前进程小的超过了调度粒度,可以抢占。
wakeup_preempt_entity中使用check_preempt_wakeup的结果。
*/
 if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
  se = cfs_rq->last;
 /*
  * Someone really wants this to run. If it's not unfair, run it.
  */
 if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
  se = cfs_rq->next;
 clear_buddies(cfs_rq, se);
 return se;
}

6 检查抢占.唤醒进程wakeup系列函数时要检查抢占。

调用时机wakeup系列函数:

1> wake_up_new_task->check_preempt_wakeup

2> wake_up_process->ttwu_do_wakeup->check_preempt_wakeup

3> CFS调度器在选择下一个进程时(pick_next_task_fair->pick_next_entity),会根据这个检查结果选择下一个进程.

(即pick_next_entity ->wakeup_preempt_entity中使用check_preempt_wakeup的结果),如上一步中的描述。

wake_up_new_task->check_preempt_curr->check_preempt_wakeup作用:摘自网络的一个解释,挺形象的.

/*
 *Preempt the current task with a newly woken task if needed:
 此处为网上看到的介绍,非常形象.首先对于last和next两个字段给予说明。
 如果这两个字段不为NULL,那么last指向最近被调度出去的进程,
 next指向被调度到cpu的进程。
 例如A正在运行,被B抢占,那么last指向A,next指向B。
 这两个指针有什么用呢?
 当CFS在调度点选择下一个运行进程时,会优先照顾这两个进程,
 我们后面会看到,这里只要记住。<
 这两个指针只使用一次,就是在上面这个函数退出后,
 返回用户空间时会触发schedule,
 在那里选择下一个调度进程时会优先选择next,次优先选择last,
 选择完后,就会清空这两个指针。
 这样设计的原因是,在上面的函数中检测结果是可以抢占并不代表
 已经抢占,而只是设置了调度标志,
 在最后触发schedule时抢占进程B并不一定是最终被调度的进程
 (为什么?因为我们判断能否抢占的根据是抢占进程B比运行
 进程A的vruntime小,但红黑树中可能有比抢占进程B的vruntime更小的进程C,
 这样在调度时就会选中vruntime最小的C,而不是抢占进程B),
 但是我们当然希望优先调度B,
 因为我们就是为了运行B才设置了调度标志,所以这里用一个
 next指针指向B,以便给他个后门走,
 如果B实在不争气,vruntime太大,就还是继续运行被抢占进程A比较合理,
 因此last指向被抢占进程,
 这是一个比next小一点的后门,如果next走后门失败,
 就让被抢占进程A也走一次后门,
 如果被抢占进程A也不争气,vruntime也太大,
 只好从红黑树中挑一个vruntime最小的了。
 不管它们走后门是否成功,一旦选出下一个进程,
 就立刻清空这两个指针,不能老开着这个后门吧。
 需要注意的是,schedule中清空这两个指针只在2.6.29及之后的内核才有,
 之前的内核没有那句话。
 然后调用wakeup_preempt_entity检测是否满足抢占条件,如果满足(返回值为1)
 则对当前进程设置TIF_NEED_RESCHED标志,在退出系统调用时会触发schedule函数进行进程切换,
 这个函数后面再说。
 */
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
 struct task_struct *curr = rq->curr;
 struct sched_entity *se = &curr->se, *pse = &p->se;
 struct cfs_rq *cfs_rq = task_cfs_rq(curr);
 int scale = cfs_rq->nr_running >= sched_nr_latency;
 int next_buddy_marked = 0;
 if (unlikely(se == pse))
  return;
 /*
  * This is possible from callers such as move_task(), in which we
  * unconditionally check_prempt_curr() after an enqueue (which may have
  * lead to a throttle).  This both saves work and prevents false
  * next-buddy nomination below.
  */
 if (unlikely(throttled_hierarchy(cfs_rq_of(pse))))
  return;
 if (sched_feat(NEXT_BUDDY) && scale && !(wake_flags & WF_FORK)) {
  set_next_buddy(pse);
  next_buddy_marked = 1;
 }
 /*
  * We can come here with TIF_NEED_RESCHED already set from new task
  * wake up path.
  *
  * Note: this also catches the edge-case of curr being in a throttled
  * group (e.g. via set_curr_task), since update_curr() (in the
  * enqueue of curr) will have resulted in resched being set.  This
  * prevents us from potentially nominating it as a false LAST_BUDDY
  * below.
  */
 if (test_tsk_need_resched(curr))
  return;
 /* Idle tasks are by definition preempted by non-idle tasks. */
 if (unlikely(curr->policy == SCHED_IDLE) &&
     likely(p->policy != SCHED_IDLE))
  goto preempt;
 /*
  * Batch and idle tasks do not preempt non-idle tasks (their preemption
  * is driven by the tick):
  */
 if (unlikely(p->policy != SCHED_NORMAL) || !sched_feat(WAKEUP_PREEMPTION))
  return;
 find_matching_se(&se, &pse);
 update_curr(cfs_rq_of(se));
 BUG_ON(!pse);
 if (wakeup_preempt_entity(se, pse) == 1) {
  /*
   * Bias pick_next to pick the sched entity that is
   * triggering this preemption.
   */
  if (!next_buddy_marked)
   set_next_buddy(pse);
  goto preempt;
 }
 return;
preempt:
 resched_task(curr);
 /*
  * Only set the backward buddy when the current task is still
  * on the rq. This can happen when a wakeup gets interleaved
  * with schedule on the ->pre_schedule() or idle_balance()
  * point, either of which can * drop the rq lock.
  *
  * Also, during early boot the idle thread is in the fair class,
  * for obvious reasons its a bad idea to schedule back to it.
  */
 if (unlikely(!se->on_rq || curr == rq->idle))
  return;
 if (sched_feat(LAST_BUDDY) && scale && entity_is_task(se))
  set_last_buddy(se);
}

7 实时进程与普通进程的优先级确定

static inline struct task_struct *pick_next_task(struct rq *rq)
{
 const struct sched_class *class;
 struct task_struct *p;

 /*
进程被添加到运行队列后,即wake_up->enqueue_task时:nr_running++;
if表示运行队列上都是普通进程 ;

*/
 if (likely(rq->nr_running == rq->cfs.h_nr_running)) {
  //pick_next_task_rt pick_next_task_fair
  p = fair_sched_class.pick_next_task(rq);
  if (likely(p))
   return p;
 }

/*如果运行队列上有实时进程,则代码走该分支;

for_each_class保证先选择实时进程,运行队列上没有实时进程后,才会选择普通进程.

*/

 for_each_class(class) {
  p = class->pick_next_task(rq);
  if (p)
   return p;
 }

 BUG(); /* the idle class will always have a runnable task */
}

[总结]

内核态创建的线程都是普通线程,可以参考介绍init_task的文档。

优先级的设置:可以参考介绍优先级的文档.

对于实时进程

1 进程在等待锁、睡眠等时机会主动让出cpu,实现过程就是在schedule->deactivate_task->dequeue_rt_stack->__dequeue_rt_entity。

把进程对应的调度实体从实时进程队列的优先级队列中(rq->rt_prio_array=rt_rq->active)删除掉。然后schedule->pick_next_task中不会再

找到该进程进行调度。若想再次唤醒该进程需要调用wake_up_process系列的唤醒函数,把进程对应的调度实体,重新添加到优先级队列上。

2 进程在时间片耗尽时让出cpu,不是把进程对应的调度实体,从优先级队列上删除,而只是放在优先级队列尾部。

3 实时进程的优先级设置可以通过如下设置:

用户态:sched_setscheduler->do_sched_setscheduler

注意此处设置的优先级大小,是数值越大,优先级越大,这和内核态定义的优先级正好向反,主要原因是:

内核态优先级队列中使用task->prio=MAX_RT_PRIO(100)-1-sched_priority(用户态设置参数),内核态用task->rt_priority保存用户态的

sched_priority.实时进程调度过程中使用的是task->prio

对于普通进程:

1 可以通过nice值调整优先级大小:

setpriority:(设置参数which=0表示调整的是进程的优先级;who=0表示调整当前进程;nice=-19表示优先级)

static_prio= (MAX_RT_PRIO (100)+ (nice) +20);普通进程的normal_prio、Prio都等于静态优先级。调整普通进程的优先级。

实时进程在scheduler_tick->task_tick_rt中判断是否将cpu调度走。(判断时间片是否耗尽)

普通进程在也会在scheduler_tick->task_tick_fair-> check_preempt_tick中判断是否将cpu调度走。

调试调度系统的proc信息

1)强制普通进程,子进程在父进程之前运行(sysctrl.c):

.procname       = "sched_child_runs_first",
.data         =&sysctl_sched_child_runs_first

2)普通进程调度周期:

.procname       = "sched_latency_ns",
.data         = &sysctl_sched_latency,

# cat /proc/sys/kernel/sched_latency_ns

12000000

如下使用sysctl_sched_latency地方:

sched_slice :计算真实时间;

sched_vslice:计算虚拟时间;

3)修改最小调度粒度,但运行队列上的活动进程大于某一数值比如8,时调度周期会发生变化:

check_preempt_tick中会判断进程运行时间是否大于最小粒度sched_min_granularity_ns,如果不大于则不会发生时间到期的调度。

#cat /proc/sys/kernel/sched_min_granularity_ns

750000

普通进程的调度周期通常情况下是:sysctl_sched_latency

但当调度队列上的活动进程数目大于(sysctl_sched_latency/sched_min_granularity_ns)时调度周期则变为sched_min_granularity_ns*活动进程数;参考__sched_period实现。

4) cat /proc/sched_debug  ---系统调度信息

cat /proc/pid/task/tid/sched  ---线程调度信息,配置项CONFIG_SCHED_DEBUG

5) 进程、线程的时间: /proc/pid/sched ; /proc/pid/task/tid/sched :se.exec_start =ms.ns

你可能感兴趣的:(linux内核进程与调度)