linux 进程调度1

调度策略与调度类

进程分为实时进程和普通进程,分别对应实时调度策略和普通调度策略

在 task_struct 中,有一个成员变量,我们叫调度策略

unsigned int policy;

它有以下几个定义:

#define SCHED_NORMAL    0
#define SCHED_FIFO    1
#define SCHED_RR    2
#define SCHED_BATCH    3
#define SCHED_IDLE    5
#define SCHED_DEADLINE    6

配合调度策略的,还有我们刚才说的优先级,也在 task_struct 中

int prio, static_prio, normal_prio;
unsigned int rt_priority;

实时进程,优先级的范围是 0~99;对于普通进程,优先级的范围是 100~139。数值越小,优先级越高

实时进程的调度策略: SCHED_FIFO、SCHED_RR、SCHED_DEADLINE

普通进程的调度策略:SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE

代码实现:

在 task_struct 里面,还有这样的成员变量:

const struct sched_class *sched_class;

调度策略的执行逻辑,就封装在这里面

  • stop_sched_class 优先级最高的任务会使用这种策略,会中断所有其他线程,且不会被其他任务打断;
  • dl_sched_class 就对应上面的 deadline 调度策略;采用EDF算法调度的实时调度实体
  • rt_sched_class 就对应 RR 算法或者 FIFO 算法的调度策略,具体调度策略由进程的 task_struct->policy 指定;
  • fair_sched_class 就是普通进程的调度策略;采用CFS算法调度的普通非实时进程的调度实体
  • idle_sched_class 就是空闲进程的调度策略
其所属进程的优先级顺序为: stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class

普通进程使用的调度策略是 fair_sched_class,顾名思义,对于普通进程来讲,公平是最重要的

调度算法:

完全公平调度算法

CFS 全称 Completely Fair Scheduling,叫完全公平调度

原理:

首先,你需要记录下进程的运行时间。CPU 会提供一个时钟,过一段时间就触发一个时钟中断。就像咱们的表滴答一下,这个我们叫 Tick。CFS 会为每一个进程安排一个虚拟运行时间 vruntime。如果一个进程在运行,随着时间的增长,也就是一个个 tick 的到来,进程的 vruntime 将不断增大。没有得到执行的进程 vruntime 不变

显然,那些 vruntime 少的,原来受到了不公平的对待,需要给它补上,所以会优先运行这样的进程。

 1 /*
 2  * Update the current task's runtime statistics.
 3  */
 4 static void update_curr(struct cfs_rq *cfs_rq)
 5 {
 6   struct sched_entity *curr = cfs_rq->curr;
 7   u64 now = rq_clock_task(rq_of(cfs_rq));
 8   u64 delta_exec;
 9 ......
10   delta_exec = now - curr->exec_start;
11 ......
12   curr->exec_start = now;
13 ......
14   curr->sum_exec_runtime += delta_exec;
15 ......
16   curr->vruntime += calc_delta_fair(delta_exec, curr);
17   update_min_vruntime(cfs_rq);
18 ......
19 }
20 
21 
22 /*
23  * delta /= w
24  */
25 static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
26 {
27   if (unlikely(se->load.weight != NICE_0_LOAD))
28         /* delta_exec * weight / lw.weight */
29     delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
30   return delta;
31 }

在这里得到当前的时间,以及这次的时间片开始的时间,两者相减就是这次运行的时间 delta_exec ,但是得到的这个时间其实是实际运行的时间,需要做一定的转化才作为虚拟运行时间 vruntime。

转化方法如下:

  虚拟运行时间 vruntime += 实际运行时间 delta_exec * NICE_0_LOAD/ 权重

调度队列与调度实体

CFS 需要一个数据结构来对 vruntime 进行排序,找出最小的那个,更新的时候也需要能够快速地调整排序,要知道 vruntime 可是经常在变的,变了再插入这个数据结构,就需要重新排序

能够平衡查询和更新速度的是树,在这里使用的是红黑树。红黑树的的节点是应该包括 vruntime 的,称为调度实体

对于普通进程的调度实体定义如下,这里面包含了 vruntime 和权重 load_weight,以及对于运行时间的统计

struct sched_entity {
  struct load_weight    load;
  struct rb_node      run_node;
  struct list_head    group_node;
  unsigned int      on_rq;
  u64        exec_start;
  u64        sum_exec_runtime;
  u64        vruntime;
  u64        prev_sum_exec_runtime;
  u64        nr_migrations;
  struct sched_statistics    statistics;
......
};

实例:

 

 所有可运行的进程通过不断地插入操作最终都存储在以时间为顺序的红黑树中,vruntime 最小的在树的左侧,vruntime 最多的在树的右侧。 CFS 调度策略会选择红黑树最左边的叶子节点作为下一个将获得 CPU 的任务。

每个 CPU 都有自己的 struct rq 结构,其用于描述在此 CPU 上所运行的所有进程,其包括一个实时进程队列 rt_rq 和一个 CFS 运行队列 cfs_rq,在调度时,调度器首先会先去实时进程队列找是否有实时进程需要运行,如果没有才会去 CFS 运行队列找是否有进行需要运行。

struct rq {
  /* runqueue lock: */
  raw_spinlock_t lock;
  unsigned int nr_running;
  unsigned long cpu_load[CPU_LOAD_IDX_MAX];
......
  struct load_weight load;
  unsigned long nr_load_updates;
  u64 nr_switches;


  struct cfs_rq cfs;
  struct rt_rq rt;
  struct dl_rq dl;
......
  struct task_struct *curr, *idle, *stop;
......
};

对于普通进程公平队列 cfs_rq,定义如下:

/* CFS-related fields in a runqueue */
struct cfs_rq {
  struct load_weight load;
  unsigned int nr_running, h_nr_running;


  u64 exec_clock;
  u64 min_vruntime;
#ifndef CONFIG_64BIT
  u64 min_vruntime_copy;
#endif
  struct rb_root tasks_timeline;
  struct rb_node *rb_leftmost;


  struct sched_entity *curr, *next, *last, *skip;
......
};

这里面 rb_root 指向的就是红黑树的根节点,这个红黑树在 CPU 看起来就是一个队列,不断的取下一个应该运行的进程。rb_leftmost 指向的是最左面的节点

 

 

调度类是如何工作

调度类的定义如下

struct sched_class {
  const struct sched_class *next;


  void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
  void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
  void (*yield_task) (struct rq *rq);
  bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);


  void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);


  struct task_struct * (*pick_next_task) (struct rq *rq,
            struct task_struct *prev,
            struct rq_flags *rf);
  void (*put_prev_task) (struct rq *rq, struct task_struct *p);


  void (*set_curr_task) (struct rq *rq);
  void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
  void (*task_fork) (struct task_struct *p);
  void (*task_dead) (struct task_struct *p);


  void (*switched_from) (struct rq *this_rq, struct task_struct *task);
  void (*switched_to) (struct rq *this_rq, struct task_struct *task);
  void (*prio_changed) (struct rq *this_rq, struct task_struct *task, int oldprio);
  unsigned int (*get_rr_interval) (struct rq *rq,
           struct task_struct *task);
  void (*update_curr) (struct rq *rq)

调度类分为下面这几种:

extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;

这里我们以调度最常见的操作,取下一个任务为例,来解析一下。可以看到,这里面有一个 for_each_class 循环,沿着上面的顺序,依次调用每个调度类的方法。

/*
 * Pick up the highest-prio task:
 */
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
  const struct sched_class *class;
  struct task_struct *p;
......
  for_each_class(class) {
    p = class->pick_next_task(rq, prev, rf);
    if (p) {
      if (unlikely(p == RETRY_TASK))
        goto again;
      return p;
    }
  }
}

调度的时候是从优先级最高的调度类到优先级低的调度类,依次执行。而对于每种调度类,有自己的实现

当有一天,某个 CPU 需要找下一个任务执行的时候,会按照优先级依次调用调度类,不同的调度类操作不同的队列。当然 rt_sched_class 先被调用,它会在 rt_rq 上找下一个任务,只有找不到的时候,才轮到 fair_sched_class 被调用,它会在 cfs_rq 上找下一个任务。这样保证了实时任务的优先级永远大于普通任务。

sched_class 定义的与调度有关的函数

  • enqueue_task 向就绪队列中添加一个进程,当某个进程进入可运行状态时,调用这个函数;
  • dequeue_task 将一个进程从就就绪队列中删除;
  • pick_next_task 选择接下来要运行的进程;
  • put_prev_task 用另一个进程代替当前运行的进程;
  • set_curr_task 用于修改调度策略;
  • task_tick 每次周期性时钟到的时候,这个函数被调用,可能触发调度

我们重点看 fair_sched_class 对于 pick_next_task 的实现 pick_next_task_fair,获取下一个进程。

调用路径如下:pick_next_task_fair->pick_next_entity->__pick_first_entity。

struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq)
{
  struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);


  if (!left)
    return NULL;


  return rb_entry(left, struct sched_entity, run_node);

从这个函数的实现可以看出,就是从红黑树里面取最左面的节点

总体调度流程

一个 CPU 上有一个队列,CFS 的队列是一棵红黑树,树的每一个节点都是一个 sched_entity,每个 sched_entity 都属于一个 task_struct,task_struct 里面有指针指向这个进程属于哪个调度类。

 

 

 在调度的时候,依次调用调度类的函数,从 CPU 的队列中取出下一个进程

 

 

参考:

《趣谈Linux操作系统》调度(上)

 

posted @ 2020-02-21 18:11  坚持,每天进步一点点  阅读(519)  评论(0编辑  收藏  举报