zoukankan      html  css  js  c++  java
  • 抢占式调度

    什么情况下会发生抢占呢?最常见的现象就是一个进程执行时间太长了,是时候切换到另一个进程了。

    那怎么衡量一个进程的运行时间呢?在计算机里面有一个时钟,会过一段时间触发一次时钟中断,通知操作系统,时间又过去一个时钟周期,这是个很好的方式,可以查看是否是需要抢占的时间点。

    时钟中断处理函数会调用 scheduler_tick()。

    void scheduler_tick(void)
    {
      int cpu = smp_processor_id();
      // 1. 取出当前 CPU 的运行队列
      struct rq *rq = cpu_rq(cpu);
      // 2. 得到这个队列上当前正在运行中的进程的 task_struct
      struct task_struct *curr = rq->curr;
      ......
      // 3. 调用这个 task_struct 的调度类的 task_tick 函数,来处理时钟事件
      curr->sched_class->task_tick(rq, curr, 0);
      cpu_load_update_active(rq);
      calc_global_load_tick(rq);
      ......
    }
    
    // 如果当前运行的进程是普通进程,调度类为 fair_sched_class,调用的处理时钟的函数为 task_tick_fair
    static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
    {
      // 根据当前进程的 task_struct,找到对应的调度实体 sched_entity 和 cfs_rq 队列,调用 entity_tick
      struct cfs_rq *cfs_rq;
      struct sched_entity *se = &curr->se;
    
      for_each_sched_entity(se) {
        cfs_rq = cfs_rq_of(se);
        entity_tick(cfs_rq, se, queued);
      }
      ......
    }
    
    
    static void
    entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
    {
      // 更新当前进程的 vruntime
      update_curr(cfs_rq);
      update_load_avg(curr, UPDATE_TG);
      update_cfs_shares(curr);
      .....
      if (cfs_rq->nr_running > 1)
        // 检查是否是时候被抢占了
        check_preempt_tick(cfs_rq, curr);
    }
    
    
    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;
    
      // ideal_runtime 是一个调度周期中,该进程运行的理想时间
      ideal_runtime = sched_slice(cfs_rq, curr);
      // sum_exec_runtime 指进程总共执行的实际时间;
      // prev_sum_exec_runtime 指上次该进程被调度时已经占用的实际时间。
      delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
      // delta_exec 这次调度占用实际时间,如果大于 ideal_runtime,则应该被抢占了
      if (delta_exec > ideal_runtime) {
        resched_curr(rq_of(cfs_rq));
        return;
      }
      ......
      // 取出红黑树中最小的进程
      se = __pick_first_entity(cfs_rq);
     // 如果当前进程的 vruntime 大于红黑树中最小的进程的 vruntime,且差值大于 ideal_runtime,也应该被抢占了
      delta = curr->vruntime - se->vruntime;
      if (delta < 0)
        return;
      if (delta > ideal_runtime)
        resched_curr(rq_of(cfs_rq));
    }

    当发现当前进程应该被抢占,不能直接把它踢下来,而是把它标记为应该被抢占。

    为什么呢?一定要等待正在运行的进程调用 __schedule 才行啊,所以这里只能先标记一下。

    标记一个进程应该被抢占,都是调用 resched_curr,它会调用 set_tsk_need_resched,标记进程应该被抢占,但是此时此刻,并不真的抢占,而是打上一个标签 TIF_NEED_RESCHED

    static inline void set_tsk_need_resched(struct task_struct *tsk)
    {
      set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
    }

    另外一个可能抢占的场景是当一个进程被唤醒的时候。

    当一个进程在等待一个 I/O 的时候,会主动放弃 CPU。但是当 I/O 到来的时候,进程往往会被唤醒。

    这个时候是一个时机。当被唤醒的进程优先级高于 CPU 上的当前进程,就会触发抢占。

    try_to_wake_up() 调用 ttwu_queue 将这个唤醒的任务添加到队列当中。

    ttwu_queue 再调用 ttwu_do_activate 激活这个任务。

    ttwu_do_activate 调用 ttwu_do_wakeup。

    这里面调用了 check_preempt_curr 检查是否应该发生抢占。

    如果应该发生抢占,也不是直接踢走当前进程,而是将当前进程标记为应该被抢占。

    static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags,
             struct rq_flags *rf)
    {
      check_preempt_curr(rq, p, wake_flags);
      p->state = TASK_RUNNING;
      trace_sched_wakeup(p);
      ......
    }

    到这里,抢占问题只做完了一半。就是标识当前运行中的进程应该被抢占了,但是真正的抢占动作并没有发生。

    抢占的时机

    真正的抢占需要时机,也就是需要那么一个时刻,让正在运行中的进程有机会调用一下 __schedule。

    这个时机分为用户态和内核态。

    对于用户态的进程来讲,从系统调用中返回的那个时刻,是一个被抢占的时机。

    64 位的系统调用的链路为:

    do_syscall_64->syscall_return_slowpath->prepare_exit_to_usermode->exit_to_usermode_loop

    static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
    {
      while (true) {
        /* We have work to do. */
        local_irq_enable();
    
        if (cached_flags & _TIF_NEED_RESCHED)
          schedule();
        ......
      }
    }

    对于用户态的进程来讲,从中断中返回的那个时刻,也是一个被抢占的时机。

    在 arch/x86/entry/entry_64.S 中有中断的处理过程。

    common_interrupt:
            ASM_CLAC
            addq    $-0x80, (%rsp) 
            interrupt do_IRQ
    ret_from_intr:
            popq    %rsp
            testb   $3, CS(%rsp)
            jz      retint_kernel
    /* Interrupt came from user space */
    GLOBAL(retint_user)
            mov     %rsp,%rdi
            call    prepare_exit_to_usermode
            TRACE_IRQS_IRETQ
            SWAPGS
            jmp     restore_regs_and_iret
    /* Returning to kernel space */
    retint_kernel:
    #ifdef CONFIG_PREEMPT
            bt      $9, EFLAGS(%rsp)  
            jnc     1f
    0:      cmpl    $0, PER_CPU_VAR(__preempt_count)
            jnz     1f
            call    preempt_schedule_irq
            jmp     0b

    中断处理调用的是 do_IRQ 函数,中断完毕后分为两种情况,一个是返回用户态,一个是返回内核态

    先来看返回用户态这一部分,retint_user 会调用 prepare_exit_to_usermode,最终调用 exit_to_usermode_loop,和上面的逻辑一样,发现有标记则调用 schedule()。

    对内核态的执行中,被抢占的时机一般发生在 preempt_enable() 中。

    内核态的执行中,有的操作是不能被中断的,所以在进行这些操作之前,总是先调用 preempt_disable() 关闭抢占,当再次打开的时候,就是一次内核态代码被抢占的机会。

    preempt_enable() 会调用 preempt_count_dec_and_test(),判断 preempt_count 和 TIF_NEED_RESCHED 是否可以被抢占。

    如果可以,就调用 preempt_schedule->preempt_schedule_common->__schedule 进行调度。

    #define preempt_enable() 
    do { 
      if (unlikely(preempt_count_dec_and_test())) 
        __preempt_schedule(); 
    } while (0)
    
    
    #define preempt_count_dec_and_test() 
      ({ preempt_count_sub(1); should_resched(0); })
    
    
    static __always_inline bool should_resched(int preempt_offset)
    {
      return unlikely(preempt_count() == preempt_offset &&
          tif_need_resched());
    }
    
    
    #define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)
    
    
    static void __sched notrace preempt_schedule_common(void)
    {
      do {
        ......
        __schedule(true);
        ......
      } while (need_resched())

    在内核态也会遇到中断的情况,当中断返回的时候,返回的仍然是内核态。

    这个时候也是一个执行抢占的时机,在上面中断返回的代码中返回内核的那部分代码,调用的是 preempt_schedule_irq。

    asmlinkage __visible void __sched preempt_schedule_irq(void)
    {
      ......
      do {
        preempt_disable();
        local_irq_enable();
        __schedule(true);
        local_irq_disable();
        sched_preempt_enable_no_resched();
      } while (need_resched());
      ......
    }
  • 相关阅读:
    2020.10.23 19级training 补题报告
    2020.10.17 天梯赛练习 补题报告
    2020.10.16 19级training 补题报告
    2020.10.9 19级training 补题报告
    2020.10.10 天梯赛练习 补题报告
    2020.10.3 天梯赛练习 补题报告
    2020.10.2 19级training 补题报告
    第十届山东省ACM省赛复现补题报告
    VVDI Key Tool Plus Adds VW Passat 2015 Key via OBD
    Xhorse VVDI Prog Software V5.0.3 Adds Many MCUs
  • 原文地址:https://www.cnblogs.com/sunnycindy/p/14940311.html
Copyright © 2011-2022 走看看