• 调度数据结构&主动调度


    调度策略

    在 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 里面去。

  • 相关阅读:
    使用phpstorm和xdebug实现远程调试
    让VS2010/VS2012添加新类时自动添加public关键字
    C#壓縮文件幫助類 使用ICSharpCode.SharpZipLib.dll
    C#使用ICSharpCode.SharpZipLib.dll压缩文件夹和文件
    用C#制作PDF文件全攻略
    多条件动态LINQ 组合查询
    bootstrap fileinput 文件上传工具
    Web Uploader文件上传插件
    Bootstrap文件上传插件File Input的使用
    flashfxp v3.7 注册码
  • 原文地址:https://www.cnblogs.com/sunnycindy/p/14938814.html
Copyright © 2020-2023  润新知