• 抢占(PREEMPTION)是如何发生的


    进程切换有自愿(Voluntary)和强制(Involuntary)之分,在前文中详细解释了两者的不同,简单来说,自愿切换意味着进程需要等待某种资源,强制切换则与抢占(Preemption)有关。

    抢占(Preemption)是指内核强行切换正在CPU上运行的进程,在抢占的过程中并不需要得到进程的配合,在随后的某个时刻被抢占的进程还可以恢复运行。发生抢占的原因主要有:进程的时间片用完了,或者优先级更高的进程来争夺CPU了。

    抢占的过程分两步,第一步触发抢占,第二步执行抢占,这两步中间不一定是连续的,有些特殊情况下甚至会间隔相当长的时间:

    1. 触发抢占:给正在CPU上运行的当前进程设置一个请求重新调度的标志(TIF_NEED_RESCHED),仅此而已,此时进程并没有切换。
    2. 执行抢占:在随后的某个时刻,内核会检查TIF_NEED_RESCHED标志并调用schedule()执行抢占。

    抢占只在某些特定的时机发生,这是内核的代码决定的。

    触发抢占的时机

    每个进程都包含一个TIF_NEED_RESCHED标志,内核根据这个标志判断该进程是否应该被抢占,设置TIF_NEED_RESCHED标志就意味着触发抢占。

    直接设置TIF_NEED_RESCHED标志的函数是 set_tsk_need_resched();
    触发抢占的函数是resched_task()。

    TIF_NEED_RESCHED标志什么时候被设置呢?在以下时刻:

    • 周期性的时钟中断

    时钟中断处理函数会调用scheduler_tick(),这是调度器核心层(scheduler core)的函数,它通过调度类(scheduling class)的task_tick方法 检查进程的时间片是否耗尽,如果耗尽则触发抢占:

    void scheduler_tick(void)
    {
            ...
            curr->sched_class->task_tick(rq, curr, 0);
            ...
    }

    Linux的进程调度是模块化的,不同的调度策略比如CFS、Real-Time被封装成不同的调度类,每个调度类都可以实现自己的task_tick方法,调度器核心层根据进程所属的调度类调用对应的方法,比如CFS对应的是task_tick_fair,Real-Time对应的是task_tick_rt,每个调度类对进程的时间片都有不同的定义。

    • 唤醒进程的时候

    当进程被唤醒的时候,如果优先级高于CPU上的当前进程,就会触发抢占。相应的内核代码中,try_to_wake_up()最终通过check_preempt_curr()检查是否触发抢占。

    • 新进程创建的时候

    如果新进程的优先级高于CPU上的当前进程,会触发抢占。相应的调度器核心层代码是sched_fork(),它再通过调度类的 task_fork方法触发抢占:

    int sched_fork(unsigned long clone_flags, struct task_struct *p)
    {
            ...
            if (p->sched_class->task_fork)
                    p->sched_class->task_fork(p);
            ...
    }
    • 进程修改nice值的时候

    如果进程修改nice值导致优先级高于CPU上的当前进程,也会触发抢占。内核代码参见 set_user_nice()。

    • 进行负载均衡的时候

    在多CPU的系统上,进程调度器尽量使各个CPU之间的负载保持均衡,而负载均衡操作可能会需要触发抢占。

    不同的调度类有不同的负载均衡算法,涉及的核心代码也不一样,比如CFS类在load_balance()中触发抢占:

    load_balance()
    {
            ...
            move_tasks();
            ...
            resched_cpu();
            ...
    }

    RT类的负载均衡基于overload,如果当前运行队列中的RT进程超过一个,就调用push_rt_task()把进程推给别的CPU,在这里会触发抢占。

    执行抢占的时机

    触发抢占通过设置进程的TIF_NEED_RESCHED标志告诉调度器需要进行抢占操作了,但是真正执行抢占还要等内核代码发现这个标志才行,而内核代码只在设定的几个点上检查TIF_NEED_RESCHED标志,这也就是执行抢占的时机。

    抢占如果发生在进程处于用户态的时候,称为User Preemption(用户态抢占);如果发生在进程处于内核态的时候,则称为Kernel Preemption(内核态抢占)。

    执行User Preemption(用户态抢占)的时机

    1. 从系统调用(syscall)返回用户态时;
    
    源文件:arch/x86/kernel/entry_64.S
    sysret_careful:
            bt $TIF_NEED_RESCHED,%edx
            jnc sysret_signal
            TRACE_IRQS_ON
            ENABLE_INTERRUPTS(CLBR_NONE)
            pushq_cfi %rdi
            call schedule
            popq_cfi %rdi
            jmp sysret_check

    2. 从中断返回用户态时。

    retint_careful:
            CFI_RESTORE_STATE
            bt    $TIF_NEED_RESCHED,%edx
            jnc   retint_signal
            TRACE_IRQS_ON
            ENABLE_INTERRUPTS(CLBR_NONE)
            pushq_cfi %rdi
            call  schedule
            popq_cfi %rdi
            GET_THREAD_INFO(%rcx)
            DISABLE_INTERRUPTS(CLBR_NONE)
            TRACE_IRQS_OFF
            jmp retint_check

    执行Kernel Preemption(内核态抢占)的时机

    Linux在2.6版本之后就支持内核抢占了,但是请注意,具体取决于内核编译时的选项:

    • CONFIG_PREEMPT_NONE=y
      不允许内核抢占。这是SLES的默认选项。
    • CONFIG_PREEMPT_VOLUNTARY=y
      在一些耗时较长的内核代码中主动调用cond_resched()让出CPU。这是RHEL的默认选项。
    • CONFIG_PREEMPT=y
      允许完全内核抢占。

    在 CONFIG_PREEMPT=y 的前提下,内核态抢占的时机是:

      1. 中断处理程序返回内核空间之前会检查TIF_NEED_RESCHED标志,如果置位则调用preempt_schedule_irq()执行抢占。preempt_schedule_irq()是对schedule()的包装。
    #ifdef CONFIG_PREEMPT
            /* Returning to kernel space. Check if we need preemption */
            /* rcx:  threadinfo. interrupts off. */
    ENTRY(retint_kernel)
            cmpl $0,TI_preempt_count(%rcx)
            jnz  retint_restore_args
            bt  $TIF_NEED_RESCHED,TI_flags(%rcx)
            jnc  retint_restore_args
            bt   $9,EFLAGS-ARGOFFSET(%rsp)  /* interrupts off? */
            jnc  retint_restore_args
            call preempt_schedule_irq
            jmp exit_intr
    #endif
      1. 当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候;
        在preempt_enable()中,会最终调用 preempt_schedule 来执行抢占。preempt_schedule()是对schedule()的包装。
  • 相关阅读:
    github登录不上?!
    js -- even-loop 理解
    前端面试积累(整理中)
    各个ctr算法的比较
    常用ctr算法比较
    BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
    Attention is All You Need
    理解word2vec
    EDA时的画图函数
    alphogo 理解
  • 原文地址:https://www.cnblogs.com/dream397/p/15977863.html
Copyright © 2020-2023  润新知