调度器的实现、schedule、switch_context、switch_to

  根据《深入Linux内核架构》和Linux-3.10.1内核源码,记一些调度过程的主体工作。

  调度器任务:CPU数目比要运行的进程数目少,需要程序之间共享CPU时间,创造并行执行的错觉。分为:调度策略和上下文切换。

  Linux调度器不考虑传统时间片,而考虑进程的等待时间,即进程在就绪队列中已经等待了多长时间(不公平程度),每次选择具有最高等待时间运行。该策略还需考虑优先级进程间切换不得太频繁(上下文切换有开销。在运行进程被新进程强占时,内核会确保被抢占者已经运行某一个最小时间限额)。Linux-2.6之后默认使用完全公平调度策略CFS

  两种方法激活调度:1、进程打算睡眠或出于其他原因放弃CPU;2、周期性检测是否有必要进行进程切换。

  内核提供两个调度器周期性调度器主调度器,合称核心调度器/通用调度器

1、数据结构

  通用调度器是一个分配器,与两个组件交互:1、调度类(模块化实现调度策略:完全公平调度、实时调度、无事可做时调度空闲进程)判断接下来运行哪个进程(每个进程都属于一个调度类。调度器自身不涉及进程管理,而将工作委托给调度类);2、上下文切换:在选中运行进程后,与CPU交互、执行底层任务切换。

  (1)task_struct:1、static_prio静态优先级是进程启动时分配的优先级(nice(-20~+19越小越高)和sched_setscheduler系统调用修改(子进程直接继承));2、normal_prio普通优先级是进程的static_prio和调度策略计算出(子进程的prio会继承);3、prio调度器考虑的优先级(因某些情况下内核需暂时提高进程优先级,不影响其余两个优先级);4、sched_class进程所属调度类;sched_entity可调度实体(调度器不限于调度进程,可处理更大实体。因此在此嵌入实体se到task_struct,内核可通过container_of取得task_struct);5、policy保存对该进程的调度策略(SCHED_NORMAL普通进程、SCHED_BATCH和SCHED_IDLE次要进程冷处理,内核将用户层进程定义的这些常量映射到fair_sched_class完全公平调度器类;SCHED_RR和SCHEDSCHED_FIFO实现软实时,映射到rt_sched_class实时调度器类);5、cpus_allowed一个位域,限制进程可在哪些CPU上运行。

  (2)调度器类:提供通用调度器和各个调度方法之间的关联。其数据结构sched_class由多个函数指针表示。成员:1、sched_class *next按照实时进程-完全公平进程-空闲进程的顺序连接不同调度类的sched_class实例。操作:1、enqueue_task向就绪队列添加一个新进程;2、dequeue_task将一个进程从就绪队列(叫队列,但完全公平调度器对此使用红黑树组织)去除;3、yield_task是使用sched_yield系统调用放弃CPU控制权;4、check_preempt_curr用wake_up_new_task唤醒新进程强占当前进程;5、pick_next_task选择下一个将运行进程;6、put_prev_task用另一个进程代替当前运行的进程之前调用;7、new_task每次新进程建立,都需调用以通知调度器,将新进程加入相应类的就绪队列。

  (3)就绪队列struct rq:每个CPU都有自身的就绪队列,每个活动进程只出现在一个就绪队列,多个CPU上不可同时运行一个进程。但进程并不由就绪队列成员直接管理,而是由调度器类管理,因此各个就绪队列中嵌入了特定于调度器类的子就绪队列cfs_rq cfs; rt_rq rt。系统所有就绪队列都在runqueues数组中,该数组每个元素分别对应系统的一个CPU。

  (4)调度实体sched_entity:on_rq该实体是否在就绪队列上接受调度;sum_exex_runtime进程运行时,记录消耗的CPU时间;exec_start每次被调用都被更新到当前时间;vruntime记录进程运行期间虚拟时间流逝;prev_exec_runtime保存进程被撤销时的sum_exec_runtime

2、周期性调度器scheduler_tick

  任务:1、管理系统和各进程与调度相关统计量;2、激活负责当前进程的调度类的周期性调度方法task_tick,若当前应被重新调度,在task_struct中设置重调度TIF_NEED_RESCHED标志,内核会在适当时机完成。

3、主调度器schedule

  将CPU分配给与当前活动进程不同的另一个进程(总是假定当前活动进程一定会被另一个进程取代)。

  流程:确定当前就绪队列-》在prev中保存指向现在仍活动进程的task_struct的指针-》更新就绪队列时钟-》清除当前运行进程task_struct中的重调度标志-》用相应调度器类方法使当前运行进程停止活动-》通知调度器类当前运行进程要被另一个进程取代-》pick_next_task以优先级从高到底依次检查每个调度类,从最高优先级的调度类中选择最高优先级的进程作为下一个应执行进程(若其余都睡眠,则只有当前进程可运行,就跳过下面了)-》context_switch分配器,执行底层上下文切换-》另一进程接管CPU,所以该函数后续代码可能运行在不同上下文,但稍后在前一进程被再次选择运行时,刚好在这一点恢复,但prev不指向正确进程,所以需要通过current和Ttest_thread_flag找到当前线程。

  context_switch:1、switch_mm更换通过task_struct->mm描述的内存管理上下文(load_cr3(next->pgd);//下一个进程的页目录基地址写入cr3寄存器,刷出TLB、向MMU提供新信息);2、switch_to切换处理器寄存器内容和内核栈(用户栈在虚拟地址空间的用户部分,已在1中更新)。

  context_swich函数中:1、注意若将运行是内核线程,其task_struct->mm是NULL,它没有自身的用户空间内存上下文,所以用惰性TLB通知底层体系结构无需切换虚拟地址空间部分,而可能在某个随机进程地址空间的上部执行,所以从prev->active_mm设置其next->active_mm即借用prev的地址空间(若prev也是内核线程,它的active_mm也会是借用再上一个);否则就switch_mm;2、若prev是内核线程,则需将prev->active_mm设置为空,以断开内核线程与借用地址空间联系;3、然后switch_to(prev, next, prev)-》barrier-》finish_task_switch针对此前的活动进程进行清理。

static inline void
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)
{//上下文切换 从一个可执行进程切换到另一个
    struct mm_struct *mm, *oldmm;

    prepare_task_switch(rq, prev, next);

    mm = next->mm;
    oldmm = prev->active_mm;
    /*
     * For paravirt, this is coupled with an exit in switch_to to
     * combine the page table reload and the switch backend into
     * one hypercall.
     */
    arch_start_context_switch(prev);

    if (!mm) {//内核线程的mm为NULL
        next->active_mm = oldmm;//利用上一个活动进程的active_mm(若上一个也是内核线程,它的这里也是借用了再上一个,总会有值)
        atomic_inc(&oldmm->mm_count);
        enter_lazy_tlb(oldmm, next);//惰性TLB,因为内核线程没有虚拟地址空间的用户空间部分,告诉底层体系结构无须切换
    } else
        switch_mm(oldmm, mm, next);//把虚拟内存从一个进程映射到新进程中

    if (!prev->mm) {//prev是内核线程
        prev->active_mm = NULL;//断开内核线程与之前借用的地址空间联系
        rq->prev_mm = oldmm;
    }
    /*
     * Since the runqueue lock will be released by the next
     * task (which is an invalid locking op but in the case
     * of the scheduler it's an obvious special-case), so we
     * do an early lockdep release here:
     */
#ifndef __ARCH_WANT_UNLOCKED_CTXSW
    spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
#endif

    context_tracking_task_switch(prev, next);
    /* Here we just switch the register state and the stack. */
    switch_to(prev, next, prev);//从上一个进程处理器状态设置为新进程的处理器状态
    //包括保存、恢复栈信息和寄存器信息和其他与体系结构相关的状态信息,都必须以每个进程为对象管理和保存

    barrier();
    /*
     * this_rq must be evaluated again because prev may have moved
     * CPUs since it called schedule(), thus the 'rq' on its stack
     * frame will be invalid.
     */
    finish_task_switch(this_rq(), prev);
}

  switch_to:之后的代码只有当前进程下一次被选择运行时才执行。三个参数原因:若A->B->C->A,在第一次A被调出时,A的内核栈中prev是A,next是B,之后被调入时,控制权回到swich_to后的点,若恢复栈则prev是A,next是B,但实际上prev是C,所以在switch_to中通过修改最后的那个prev,即返回指向此前运行的指针C。

posted @ 2019-03-06 16:28  前进的code  阅读(1310)  评论(0编辑  收藏  举报