调度数据结构&主动调度

调度策略

在 Linux 里面,进程大概可以分成两种。

一种称为实时进程,也就是需要尽快执行返回结果的那种。另一种是普通进程,大部分的进程其实都是这种。

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

从这里可以看出,所有的实时进程都比普通进程优先级要高。

通常把一个task叫作最小调度单元。但是 Linux 调度器不仅仅只能够调度单个任务,而且还可以将一组任务,甚至属于某个用户的所有任务作为整体进行调度。

这就允许我们实现组调度,从而将 CPU 时间先分配到进程组,再在组内分配到单个线程。

当引入这项功能后,可以大幅度提升桌面系统的交互性。比如,可以将编译任务聚集成一个组,然后进行调度,从而不会对交互性产生明显的影响。

Linux 调度器不仅仅能直接调度task,也能对调度单元(schedulable entities)进行调度。这样的调度单元正是用调度实体来表示的。

由于调度器是面向调度单元设计的,所以它会将单个 task 也视为调度单元,因此会使用调度实体结构体操作它们。

policy 表明任务的调度策略,通常意味着针对某些特定的进程组(比如需要更长时间片,更高优先级等)应用特殊的调度决策。

/****** task_struct 进程调度相关 ******/
// 是否在运行队列上
int        on_rq;
// 优先级
int        prio;
int        static_prio;
int        normal_prio;
unsigned int      rt_priority;
// 表示进程位于哪个调度器类,封装了调度策略的执行逻辑
const struct sched_class  *sched_class;
// 调度实体
struct sched_entity    se; // 完全公平算法调度实体
struct sched_rt_entity    rt; // 实时调度实体
struct sched_dl_entity    dl; // Deadline 调度实体
// 调度策略
unsigned int      policy;
// 可以使用哪些CPU
int        nr_cpus_allowed;
cpumask_t      cpus_allowed;
struct sched_info    sched_info;


// 调度策略定义
#define SCHED_NORMAL    0
#define SCHED_FIFO    1  // 先来先服务
#define SCHED_RR    2
#define SCHED_BATCH    3
#define SCHED_IDLE    5
#define SCHED_DEADLINE    6

// 完全公平算法调度实体
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;
  ......
};

实时任务的调度策略:SCHED_FIFO, SCHED_RR, SCHED_DEADLINE

SCHED_FIFO 先来先服务,高优先级的进程可以抢占低优先级的进程,而相同优先级的进程,我们遵循先来先得。

SCHED_RR 轮流调度算法,采用时间片,相同优先级的任务当用完时间片会被放到队列尾部,以保证公平性,而高优先级的任务也是可以抢占低优先级的任务。

SCHED_DEADLINE,是按照任务的 deadline 进行调度的。当产生一个调度点的时候,DL 调度器总是选择其 deadline 距离当前时间点最近的那个任务,并调度它执行。

普通任务的调度策略:SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE

SCHED_NORMAL 是普通的进程。

SCHED_BATCH 是后台进程,几乎不需要和前端进行交互。

SCHED_IDLE 是特别空闲的时候才跑的进程。

每个 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-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;
  ......
};

调度类的定义如下:

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;   // 对应上面的 deadline 调度策略
extern const struct sched_class rt_sched_class;   // 对应 RR 算法或者 FIFO 算法的调度策略,具体调度策略由进程的 task_struct->policy 指定
extern const struct sched_class fair_sched_class; // 普通进程的调度策略
extern const struct sched_class idle_sched_class; // 空闲进程的调度策略

它们其实是放在一个链表上的。这里我们以调度最常见的操作,取下一个任务为例,来解析一下。

可以看到,这里面有一个 for_each_class 循环,沿着上面的顺序,依次调用每个调度类的方法。

这就说明,调度的时候是从优先级最高的调度类到优先级低的调度类,依次执行。而对于每种调度类,有自己的实现,例如,CFS 就有 fair_sched_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;
    }
  }
}


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,
  .yield_to_task    = yield_to_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_fork    = task_fork_fair,
  .prio_changed    = prio_changed_fair,
  .switched_from    = switched_from_fair,
  .switched_to    = switched_to_fair,
  .get_rr_interval  = get_rr_interval_fair,
  .update_curr    = update_curr_fair,
};

对于同样的 pick_next_task 选取下一个要运行的任务这个动作,不同的调度类有自己的实现。

fair_sched_class 的实现是 pick_next_task_fair,rt_sched_class 的实现是 pick_next_task_rt。

我们会发现这两个函数是操作不同的队列,pick_next_task_rt 操作的是 rt_rq,pick_next_task_fair 操作的是 cfs_rq。

static struct task_struct *
pick_next_task_rt(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
  struct task_struct *p;
  struct rt_rq *rt_rq = &rq->rt;
  ......
}


static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
  struct cfs_rq *cfs_rq = &rq->cfs;
  struct sched_entity *se;
  struct task_struct *p;
  ......
}

这样整个运行的场景就串起来了,在每个 CPU 上都有一个队列 rq,这个队列里面包含多个子队列,例如 rt_rq 和 cfs_rq,不同的队列有不同的实现方式,cfs_rq 就是用红黑树实现的。当有一天,某个 CPU 需要找下一个任务执行的时候,会按照优先级依次调用调度类,不同的调度类操作不同的队列。当然 rt_sched_class 先被调用,它会在 rt_rq 上找下一个任务,只有找不到的时候,才轮到 fair_sched_class 被调用,它会在 cfs_rq 上找下一个任务。这样保证了实时任务的优先级永远大于普通任务。

我们重点看下 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);
}

主动调度

举例:从 Tap 网络设备等待一个读取。Tap 网络设备是虚拟机使用的网络设备。当没有数据到来的时候,它也需要等待,所以也会选择把 CPU 让给其他进程。

static ssize_t tap_do_read(struct tap_queue *q,
         struct iov_iter *to,
         int noblock, struct sk_buff *skb)
{
  ......
  while (1) {
    if (!noblock)
      prepare_to_wait(sk_sleep(&q->sk), &wait,
          TASK_INTERRUPTIBLE);
    ......
    /* Nothing to read, let's sleep */
    schedule();
  }
  ......
}

schedule 函数的调用过程

asmlinkage __visible void __sched schedule(void)
{
  struct task_struct *tsk = current;


  sched_submit_work(tsk);
  do {
    preempt_disable();
    __schedule(false);
    sched_preempt_enable_no_resched();
  } while (need_resched());
}


static void __sched notrace __schedule(bool preempt)
{
  struct task_struct *prev, *next;
  unsigned long *switch_count;
  struct rq_flags rf;
  struct rq *rq;
  int cpu;

  // 1. 在当前的 CPU 上取出任务队列 rq
  cpu = smp_processor_id();
  rq = cpu_rq(cpu);
  prev = rq->curr;
  ......
  // 2. 获取下一个任务,即继任
  next = pick_next_task(rq, prev, &rf);
  clear_tsk_need_resched(prev);
  clear_preempt_need_resched();
  ......
  // 3. 当选出的继任者和前任不同,就要进行上下文切换,继任者进程正式进入运行
  if (likely(prev != next)) {
    rq->nr_switches++;
    rq->curr = next;
    ++*switch_count;
    ......
    rq = context_switch(rq, prev, next, &rf);
    ......
}

 pick_next_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;
  /*
   * Optimization: we know that if all tasks are in the fair class we can call that function directly, 
   * but only if the @prev task wasn't of a higher scheduling class, 
   * because otherwise those loose the opportunity to pull in more work from other CPUs.
   */
  if (likely((prev->sched_class == &idle_sched_class ||
        prev->sched_class == &fair_sched_class) &&
       rq->nr_running == rq->cfs.h_nr_running)) {
    p = fair_sched_class.pick_next_task(rq, prev, rf);
    if (unlikely(p == RETRY_TASK))
      goto again;
    /* Assumes fair_sched_class->next == idle_sched_class */
    if (unlikely(!p))
      p = idle_sched_class.pick_next_task(rq, prev, rf);
    return p;
  }
again:
  for_each_class(class) {
    p = class->pick_next_task(rq, prev, rf);
    if (p) {
      if (unlikely(p == RETRY_TASK))
        goto again;
      return p;
    }
  }
}

again 就是依次调用调度类。但是这里有了一个优化,因为大部分进程是普通进程,所以大部分情况下会调用上面的逻辑,调用的就是 fair_sched_class.pick_next_task。

根据 fair_sched_class 的定义,它调用的是 pick_next_task_fair。

static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
  struct cfs_rq *cfs_rq = &rq->cfs;
  struct sched_entity *se;
  struct task_struct *p;
  int new_tasks;

对于 CFS 调度类,取出相应的队列 cfs_rq,这就是那棵红黑树。

    struct sched_entity *curr = cfs_rq->curr;
    if (curr) {
      if (curr->on_rq)
        update_curr(cfs_rq);
      else
        curr = NULL;
        ......
    }
    se = pick_next_entity(cfs_rq, curr);

取出当前正在运行的任务 curr,如果依然是可运行的状态,也即处于进程就绪状态,则调用 update_curr 更新 vruntime。

update_curr 会根据实际运行时间算出 vruntime 来。

接着,pick_next_entity 从红黑树里面,取最左边的一个节点。

  // 得到下一个调度实体对应的 task_struct
  p = task_of(se);

  // 如果发现继任和前任不一样,这就说明有一个更需要运行的进程了,就需要更新红黑树了。
  if (prev != p) {
    struct sched_entity *pse = &prev->se;
    ......
    // 前面前任的 vruntime 更新过了,put_prev_entity 放回红黑树,会找到相应的位置
    put_prev_entity(cfs_rq, pse);
    // 将继任者设为当前任务
    set_next_entity(cfs_rq, se);
  }

  return p

进程上下文切换

上下文切换主要干两件事情,一是切换进程空间,也即虚拟内存;二是切换寄存器和 CPU 上下文。

/*
 * context_switch - switch to the new MM and the new thread's register state.
 */
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
         struct task_struct *next, struct rq_flags *rf)
{
  struct mm_struct *mm, *oldmm;
  ......
  mm = next->mm;
  oldmm = prev->active_mm;
  ......
  switch_mm_irqs_off(oldmm, mm, next);
  ......
  /* Here we just switch the register state and the stack. */
  switch_to(prev, next, prev);
  barrier();
  return finish_task_switch(prev);
}

switch_to 是寄存器和栈的切换,它调用到了 __switch_to_asm。这是一段汇编代码,主要用于栈的切换。

// 32 位操作系统
/*
 * %eax: prev task
 * %edx: next task
 */
ENTRY(__switch_to_asm)
  ......
  /* switch stack */
  movl  %esp, TASK_threadsp(%eax)
  movl  TASK_threadsp(%edx), %esp
  ......
  jmp  __switch_to
END(__switch_to_asm)


// 64 位操作系统
/*
 * %rdi: prev task
 * %rsi: next task
 */
ENTRY(__switch_to_asm)
  ......
  /* switch stack */
  movq  %rsp, TASK_threadsp(%rdi)
  movq  TASK_threadsp(%rsi), %rsp
  ......
  jmp  __switch_to
END(__switch_to_asm)

最终,都返回了 __switch_to 这个函数。这个函数对于 32 位和 64 位操作系统虽然有不同的实现,但里面做的事情是差不多的。所以这里仅仅列出 64 位操作系统做的事情。

__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
  struct thread_struct *prev = &prev_p->thread;
  struct thread_struct *next = &next_p->thread;
  ......
  int cpu = smp_processor_id();
  struct tss_struct *tss = &per_cpu(cpu_tss, cpu);
  ......
  load_TLS(next, cpu);
  ......
  this_cpu_write(current_task, next_p);


  /* Reload esp0 and ss1.  This changes current_thread_info(). */
  load_sp0(tss, next);
  ......
  return prev_p;
}

所谓的进程切换,就是将某个进程的 thread_struct 里面的寄存器的值,写入到 CPU 的 TR 指向的 tss_struct,对于 CPU 来讲,这就算是完成了切换。

例如 __switch_to 中的 load_sp0,就是将下一个进程的 thread_struct 的 sp0 的值加载到 tss_struct 里面去。

原文地址:https://www.cnblogs.com/sunnycindy/p/14938814.html