本文为原创,转载请注明: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队列中。