zoukankan      html  css  js  c++  java
  • linux调度器源码分析

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/

    引言

      之前的文章已经介绍了调度器已经初始化完成,现在只需要加入一个周期定时器tick驱动它进行周期调度即可,而加入定时器tick在下一篇文章进行简单说明(主要这部分涉及调度器比较少,更多的是时钟、定时器相关知识)。这篇文章主要说明系统如何把一个进程加入到队列中。

    加入时机

      之前的文章也有提到过,只有处于TASK_RUNNING状态下的进程才能够加入到调度器,其他状态都不行,也就说明了,当一个进程处于睡眠、挂起状态的时候是不存在于调度器中的,而进程加入调度器的时机如下:

    • 当进程创建完成时,进程刚创建完成时,即使它运行起来立即调用sleep()进程睡眠,它也必定先会加入到调度器,因为实际上它加入调度器后自己还需要进行一定的初始化和操作,才会调用到我们的“立即”sleep()。
    • 当进程被唤醒时,也使用sleep的例子说明,我们平常写程序使用的sleep()函数实现原理就是通过系统调用将进程状态改为TASK_INTERRUPTIBLE,然后移出运行队列,并且启动一个定时器,在定时器到期后唤醒进程,再重新放入运行队列。

    sched_fork

      在我的博文关于linux系统如何实现fork的研究(二)中专门描述了copy_process()这个创建函数,而里面有一个函数专门用于进程调度的初始化,就是sched_fork(),其代码如下

     1 int sched_fork(unsigned long clone_flags, struct task_struct *p)
     2 {
     3     unsigned long flags;
     4     /* 获取当前CPU,并且禁止抢占 */
     5     int cpu = get_cpu();
     6     
     7     /* 初始化跟调度相关的值,比如调度实体,运行时间等 */
     8     __sched_fork(clone_flags, p);
     9     /*
    10      * 标记为运行状态,表明此进程正在运行或准备好运行,实际上没有真正在CPU上运行,这里只是导致了外部信号和事件不能够唤醒此进程,之后将它插入到运行队列中
    11      */
    12     p->state = TASK_RUNNING;
    13 
    14     /*
    15      * 根据父进程的运行优先级设置设置进程的优先级
    16      */
    17     p->prio = current->normal_prio;
    18 
    19     /*
    20      * 更新该进程优先级
    21      */
    22     /* 如果需要重新设置优先级 */
    23     if (unlikely(p->sched_reset_on_fork)) {
    24         /* 如果是dl调度或者实时调度 */
    25         if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
    26             /* 调度策略为SCHED_NORMAL,这个选项将使用CFS调度 */
    27             p->policy = SCHED_NORMAL;
    28             /* 根据默认nice值设置静态优先级 */
    29             p->static_prio = NICE_TO_PRIO(0);
    30             /* 实时优先级为0 */
    31             p->rt_priority = 0;
    32         } else if (PRIO_TO_NICE(p->static_prio) < 0)
    33             /* 根据默认nice值设置静态优先级 */
    34             p->static_prio = NICE_TO_PRIO(0);
    35 
    36         /* p->prio = p->normal_prio = p->static_prio */
    37         p->prio = p->normal_prio = __normal_prio(p);
    38         /* 设置进程权重 */
    39         set_load_weight(p);
    40 
    41          /* sched_reset_on_fork成员在之后已经不需要使用了,直接设为0 */
    42         p->sched_reset_on_fork = 0;
    43     }
    44 
    45     if (dl_prio(p->prio)) {
    46         /* 使能抢占 */
    47         put_cpu();
    48         /* 返回错误 */
    49         return -EAGAIN;
    50     } else if (rt_prio(p->prio)) {
    51         /* 根据优先级判断,如果是实时进程,设置其调度类为rt_sched_class */
    52         p->sched_class = &rt_sched_class;
    53     } else {
    54         /* 如果是普通进程,设置其调度类为fair_sched_class */
    55         p->sched_class = &fair_sched_class;
    56     }
    57     /* 调用调用类的task_fork函数 */
    58     if (p->sched_class->task_fork)
    59         p->sched_class->task_fork(p);
    60 
    61     /*
    62      * The child is not yet in the pid-hash so no cgroup attach races,
    63      * and the cgroup is pinned to this child due to cgroup_fork()
    64      * is ran before sched_fork().
    65      *
    66      * Silence PROVE_RCU.
    67      */
    68     raw_spin_lock_irqsave(&p->pi_lock, flags);
    69     /* 设置新进程的CPU为当前CPU */
    70     set_task_cpu(p, cpu);
    71     raw_spin_unlock_irqrestore(&p->pi_lock, flags);
    72 
    73 #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
    74     if (likely(sched_info_on()))
    75         memset(&p->sched_info, 0, sizeof(p->sched_info));
    76 #endif
    77 #if defined(CONFIG_SMP)
    78     p->on_cpu = 0;
    79 #endif
    80     /* task_thread_info(p)->preempt_count = PREEMPT_DISABLED; */
    81     /* 初始化该进程为内核禁止抢占 */
    82     init_task_preempt_count(p);
    83 #ifdef CONFIG_SMP
    84     plist_node_init(&p->pushable_tasks, MAX_PRIO);
    85     RB_CLEAR_NODE(&p->pushable_dl_tasks);
    86 #endif
    87     /* 使能抢占 */
    88     put_cpu();
    89     return 0;
    90 }

       在sched_fork()函数中,主要工作如下:

    • 获取当前CPU号
    • 禁止内核抢占(这里基本就是关闭了抢占,因为执行到这里已经是内核态,又禁止了被抢占)
    • 初始化进程p的一些变量(实时进程和普通进程通用的那些变量)
    • 设置进程p的状态为TASK_RUNNING(这一步很关键,因为只有处于TASK_RUNNING状态下的进程才会被调度器放入队列中)
    • 根据父进程和clone_flags参数设置进程p的优先级和权重。
    • 根据进程p的优先级设置其调度类(实时进程优先级:0~99  普通进程优先级:100~139)
    • 根据调度类进行进程p类型相关的初始化(这里就实现了实时进程和普通进程独有的变量进行初始化)
    • 设置进程p的当前CPU为此CPU。
    • 初始化进程p禁止内核抢占(因为当CPU执行到进程p时,进程p还需要进行一些初始化)
    • 使能内核抢占

      可以看出sched_fork()进行的初始化也比较简单,需要注意的是不同类型的进程会使用不同的调度类,并且也会调用调度类中的初始化函数。在实时进程的调度类中是没有特定的task_fork()函数的,而普通进程使用cfs策略时会调用到task_fork_fair()函数,我们具体看看实现:

     1 static void task_fork_fair(struct task_struct *p)
     2 {
     3     struct cfs_rq *cfs_rq;
     4     
     5     /* 进程p的调度实体se */
     6     struct sched_entity *se = &p->se, *curr;
     7     
     8     /* 获取当前CPU */
     9     int this_cpu = smp_processor_id();
    10     
    11     /* 获取此CPU的运行队列 */
    12     struct rq *rq = this_rq();
    13     unsigned long flags;
    14     
    15     /* 上锁并保存中断记录 */
    16     raw_spin_lock_irqsave(&rq->lock, flags);
    17     
    18     /* 更新rq运行时间 */
    19     update_rq_clock(rq);
    20     
    21     /* cfs_rq = current->se.cfs_rq; */
    22     cfs_rq = task_cfs_rq(current);
    23     
    24     /* 设置当前进程所在队列为父进程所在队列 */
    25     curr = cfs_rq->curr;
    26 
    27     /*
    28      * Not only the cpu but also the task_group of the parent might have
    29      * been changed after parent->se.parent,cfs_rq were copied to
    30      * child->se.parent,cfs_rq. So call __set_task_cpu() to make those
    31      * of child point to valid ones.
    32      */
    33     rcu_read_lock();
    34     /* 设置此进程所属CPU */
    35     __set_task_cpu(p, this_cpu);
    36     rcu_read_unlock();
    37 
    38     /* 更新当前进程运行时间 */
    39     update_curr(cfs_rq);
    40 
    41     if (curr)
    42         /* 将父进程的虚拟运行时间赋给了新进程的虚拟运行时间 */
    43         se->vruntime = curr->vruntime;
    44     /* 调整了se的虚拟运行时间 */
    45     place_entity(cfs_rq, se, 1);
    46 
    47     if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
    48         /*
    49          * Upon rescheduling, sched_class::put_prev_task() will place
    50          * 'current' within the tree based on its new key value.
    51          */
    52         swap(curr->vruntime, se->vruntime);
    53         resched_curr(rq);
    54     }
    55 
    56     /* 保证了进程p的vruntime是运行队列中最小的(这里占时不确定是不是这个用法,不过确实是最小的了) */
    57     se->vruntime -= cfs_rq->min_vruntime;
    58     
    59     /* 解锁,还原中断记录 */
    60     raw_spin_unlock_irqrestore(&rq->lock, flags);
    61 }

      在task_fork_fair()函数中主要就是设置进程p的虚拟运行时间和所处的cfs队列,值得我们注意的是 cfs_rq = task_cfs_rq(current); 这一行,在注释中已经表明task_cfs_rq(current)返回的是current的se.cfs_rq,注意se.cfs_rq保存的并不是根cfs队列,而是所处的cfs_rq,也就是如果父进程处于一个进程组的cfs_rq中,新创建的进程也会处于这个进程组的cfs_rq中。

    wake_up_new_task()

      到这里新进程关于调度的初始化已经完成,但是还没有被调度器加入到队列中,其是在do_fork()中的wake_up_new_task(p);中加入到队列中的,我们具体看看wake_up_new_task()的实现:

     1 void wake_up_new_task(struct task_struct *p)
     2 {
     3     unsigned long flags;
     4     struct rq *rq;
     5 
     6     raw_spin_lock_irqsave(&p->pi_lock, flags);
     7 #ifdef CONFIG_SMP
     8     /*
     9      * Fork balancing, do it here and not earlier because:
    10      *  - cpus_allowed can change in the fork path
    11      *  - any previously selected cpu might disappear through hotplug
    12      */
    13      /* 为进程选择一个合适的CPU */
    14     set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
    15 #endif
    16 
    17     /* Initialize new task's runnable average */
    18     /* 这里是跟多核负载均衡有关 */
    19     init_task_runnable_average(p);
    20     /* 上锁 */
    21     rq = __task_rq_lock(p);
    22     /* 将进程加入到CPU的运行队列 */
    23     activate_task(rq, p, 0);
    24     /* 标记进程p处于队列中 */
    25     p->on_rq = TASK_ON_RQ_QUEUED;
    26     /* 跟调试有关 */
    27     trace_sched_wakeup_new(p, true);
    28     /* 检查是否需要切换当前进程 */
    29     check_preempt_curr(rq, p, WF_FORK);
    30 #ifdef CONFIG_SMP
    31     if (p->sched_class->task_woken)
    32         p->sched_class->task_woken(rq, p);
    33 #endif
    34     task_rq_unlock(rq, p, &flags);
    35 }

      在wake_up_new_task()函数中,将进程加入到运行队列的函数为activate_task(),而activate_task()函数最后会调用到新进程调度类中的enqueue_task指针所指函数,这里我们具体看一下cfs调度类的enqueue_task指针所指函数enqueue_task_fair():

     1 static void
     2 enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
     3 {
     4     struct cfs_rq *cfs_rq;
     5     struct sched_entity *se = &p->se;
     6 
     7     /* 这里是一个迭代,我们知道,进程有可能是处于一个进程组中的,所以当这个处于进程组中的进程加入到该进程组的队列中时,要对此队列向上迭代 */
     8     for_each_sched_entity(se) {
     9         if (se->on_rq)
    10             break;
    11         /* 如果不是CONFIG_FAIR_GROUP_SCHED,获取其所在CPU的rq运行队列的cfs_rq运行队列
    12          * 如果是CONFIG_FAIR_GROUP_SCHED,获取其所在的cfs_rq运行队列
    13          */
    14         cfs_rq = cfs_rq_of(se);
    15         /* 加入到队列中 */
    16         enqueue_entity(cfs_rq, se, flags);
    17 
    18         /*
    19          * end evaluation on encountering a throttled cfs_rq
    20          *
    21          * note: in the case of encountering a throttled cfs_rq we will
    22          * post the final h_nr_running increment below.
    23         */
    24         if (cfs_rq_throttled(cfs_rq))
    25             break;
    26         cfs_rq->h_nr_running++;
    27 
    28         flags = ENQUEUE_WAKEUP;
    29     }
    30 
    31     /* 只有se不处于队列中或者cfs_rq_throttled(cfs_rq)返回真才会运行这个循环 */
    32     for_each_sched_entity(se) {
    33         cfs_rq = cfs_rq_of(se);
    34         cfs_rq->h_nr_running++;
    35 
    36         if (cfs_rq_throttled(cfs_rq))
    37             break;
    38 
    39         update_cfs_shares(cfs_rq);
    40         update_entity_load_avg(se, 1);
    41     }
    42 
    43     if (!se) {
    44         update_rq_runnable_avg(rq, rq->nr_running);
    45         /* 当前CPU运行队列活动进程数 + 1 */
    46         add_nr_running(rq, 1);
    47     }
    48     /* 设置下次调度中断发生时间 */
    49     hrtick_update(rq);
    50 }

      在enqueue_task_fair()函数中又使用了enqueue_entity()函数进行操作,如下:

     1 static void
     2 enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
     3 {
     4     /*
     5      * Update the normalized vruntime before updating min_vruntime
     6      * through calling update_curr().
     7      */
     8     if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
     9         se->vruntime += cfs_rq->min_vruntime;
    10 
    11     /*
    12      * Update run-time statistics of the 'current'.
    13      */
    14     /* 更新当前进程运行时间和虚拟运行时间 */
    15     update_curr(cfs_rq);
    16     enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);
    17     /* 更新cfs_rq队列总权重(就是在原有基础上加上se的权重) */
    18     account_entity_enqueue(cfs_rq, se);
    19     update_cfs_shares(cfs_rq);
    20 
    21     /* 新建的进程flags为0,不会执行这里 */
    22     if (flags & ENQUEUE_WAKEUP) {
    23         place_entity(cfs_rq, se, 0);
    24         enqueue_sleeper(cfs_rq, se);
    25     }
    26 
    27     update_stats_enqueue(cfs_rq, se);
    28     check_spread(cfs_rq, se);
    29     
    30     /* 将se插入到运行队列cfs_rq的红黑树中 */
    31     if (se != cfs_rq->curr)
    32         __enqueue_entity(cfs_rq, se);
    33     /* 将se的on_rq标记为1 */
    34     se->on_rq = 1;
    35 
    36     /* 如果cfs_rq的队列中只有一个进程,这里做处理 */
    37     if (cfs_rq->nr_running == 1) {
    38         list_add_leaf_cfs_rq(cfs_rq);
    39         check_enqueue_throttle(cfs_rq);
    40     }
    41 }

    总结

      需要注意的几点:

    • 新创建的进程先会进行调度相关的结构体和变量初始化,其中会根据不同的类型进行不同的调度类操作,此时并没有加入到队列中。
    • 当新进程创建完毕后,它的父进程会将其运行状态置为TASK_RUNNING,并加入到运行队列中。
    • 加入运行队列时系统会根据CPU的负载情况放入不同的CPU队列中。
  • 相关阅读:
    【转】VS2010中 C++创建DLL图解
    [转]error: 'retainCount' is unavailable: not available in automatic reference counting mode
    [转]关于NSAutoreleasePool' is unavailable: not available in automatic reference counting mode的解决方法
    【转】 Tomcat v7.0 Server at localhost was unable to start within 45
    【转】Server Tomcat v7.0 Server at localhost was unable to start within 45 seconds. If
    【转】SVN管理多个项目版本库
    【转】eclipse安装SVN插件的两种方法
    【转】MYSQL启用日志,和查看日志
    【转】Repository has not been enabled to accept revision propchanges
    【转】SVN库的迁移
  • 原文地址:https://www.cnblogs.com/tolimit/p/4318864.html
Copyright © 2011-2022 走看看