linux中的进程是个很重要的概念,这个就不必多说了,linux中进程创建的fork机制继承了unix的基因,是操作系统中最重要的东西,fork中的写时复制机制是fork的精髓,是进程机制的精髓,它不仅仅代表了这些,它的实现还帮了另一个忙,这就是一般说来,linux在fork之后一般让新进程先运行,这是为了避免不必要的写时复制操作,因为新进程往往不再操作父进程的地址空间而是马上进行新的逻辑或者进行exec调用,但是却复制了父进程的地址空间,如果父进程优先运行,那么父进程的每一步运行只要是写操作都会导致写时复制,这是个根本没有必要的操作,系统的机制虽然要求写时复制,但是策略上却是很少会有子进程操作父进程地址空间的情况,父进程操作其地址空间却是一定的,因为它们共享一个地址空间,所以会导致没有用的写时复制,所以解决的办法就是让子进程先运行,最起码一旦子进程进行了exec,写时复制就再也么有必要了,而这是大多数的情况,在O(1)调度器时期,在fork中有以下逻辑:
if (!(clone_flags & CLONE_STOPPED))
wake_up_new_task(p, clone_flags);
就是说唤醒子进程,唤醒过程:
if (likely(cpu == this_cpu)) {
if (!(clone_flags & CLONE_VM)) {
if (unlikely(!current->array))
__activate_task(p, rq);
else {
p->prio = current->prio;
p->normal_prio = current->normal_prio;
list_add_tail(&p->run_list, ¤t->run_list); //注意,排队位置和正常的入队不一样
p->array = current->array;
p->array->nr_active++;
inc_nr_running(p, rq);
}
set_need_resched(); //为了让子进程先运行,设置当前进程的调度标志
} else //如果不在这个cpu上,就由smp权衡吧!个逻辑很简单,就是让子进程先运行。
事情在O(1)调度器时期是如此简单,但是在cfs中呢?还是这么显然吗?不是了,在cfs中,让子进程先运行这件事不再是机制导致的了,而是用户配置的,用户完全可以不让子进程先运行,这进一步让内核脱离了策略的设置,但是无论如何,内核默认让子进程先运行,用户可以通过配置设置是否让子进程先运行,在cfs中wake_up_new_task为:
void wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
{
unsigned long flags;
struct rq *rq;
rq = task_rq_lock(p, &flags);
BUG_ON(p->state != TASK_RUNNING);
update_rq_clock(rq);
p->prio = effective_prio(p);
if (!p->sched_class->task_new || !current->se.on_rq) {
activate_task(rq, p, 0); //按照传统的机制进行
} else {
p->sched_class->task_new(rq, p); //OO性质进一步体现
inc_nr_running(rq);
}
trace_sched_wakeup_new(rq, p);
check_preempt_curr(rq, p, 0); //这里是一次机会,因为有新人入队了,所以在重大事件前,当前进程必须被权衡,是这么回事吗?一般而言只要update_curr之后就会权衡一下当前进程是否应该会继续运行,是这样吗?看看cfs中task_new中的update_curr调用
...
task_rq_unlock(rq, &flags);
}
在cfs中,其task_new是什么呢?
static void task_new_fair(struct rq *rq, struct task_struct *p)
{
struct cfs_rq *cfs_rq = task_cfs_rq(p);
struct sched_entity *se = &p->se, *curr = cfs_rq->curr;
int this_cpu = smp_processor_id();
sched_info_queued(p);
update_curr(cfs_rq);
place_entity(cfs_rq, se, 1); //这个是本函数中最重要的
//sysctl_sched_child_runs_first为1是子进程先运行的必要条件
if (sysctl_sched_child_runs_first && this_cpu == task_cpu(p) && curr && curr->vruntime < se->vruntime) {
swap(curr->vruntime, se->vruntime); //交换子进程和父进程,就是让子进程先运行
resched_task(rq->curr); //这个再说我就无语了
}
enqueue_task_fair(rq, p, 0);
}
我可以用不能再重要来形容place_entity函数,因为它对新进程进行了零岁教育,婴儿的活力十足是有目共睹的。注意,这个函数中只是尽量让子进程最早运行,fork的写时复制避免机制却是没有在这里体现,因为在cfs中,其实是在调度类出现以后,子进程先运行这件事这个策略就从内核中脱离了,内核不再关注策略。进一步,子进程应该何时运行呢?这里要明白,place_entity的代码最坏的打算就是用户没有设置子进程优先运行的策略,但是它还是想让子进程虽然不是优先但最起码要尽早运行,另一方面,如果用户设置了子进程优先运行并且符合别的条件,以下马上要交换当前进程(父进程)和子进程,cfs调度器保证当前进程虽然被新进程抢占了,但是不能因为这次抢占延误了太多,因此place_entity中存在了太多的策略,我们先看一下最简单的,就是没有START_DEBIT的情况下,新进程的vruntime就是运行队列的min_vrntime,如果当前进程的vrntime小于min_vruntime,那么调度器会在place_entity之前的update_curr中将当前进程的vruntime设定为min_vruntime,所以此时新进程的vruntime将是和父进程的vruntime相等,在后面的if (sysctl_sched_child_runs_first && this_cpu == task_cpu(p) && curr && curr->vruntime < se->vruntime)判定中不会通过,然后在再后面的入队操作中会排到当前进程的右边,这样就不会抢占当前进程,结果就是会有更多的写时复制;如果当前进程的vrantime大于min_vruntime,那么新进程的vruntime肯定小于当前进程的vruntime,抢占是肯定的吗?不,因为如果当前进程如果还没有完成自己的一个虚拟时间的推进,那么还是无法抢占,但是如果恰好完成了一次推进,也就是,那么抢占就是必然的了,因此,写时复制是有限的,毕竟公平是需要代价的,不能说为了不进行一次无用的写时复制而破坏了cfs的公平规则。现在分析另外一部分,在设置了START_DEBIT的情况下,这种情况看似复杂其实不然,debit的意思就是记账,就是将欠款计入欠款的借方,这个很好理解,看下面的代码:
if (initial && sched_feat(START_DEBIT))
vruntime += sched_vslice(cfs_rq, se);
这里的vrntime就是min_vruntime,意思是说,假设新进程已经运行了min_vruntime的虚拟时间了,在此基础上又运行了sched_vslice(cfs_rq, se)虚拟时间,但是新进程并没有运行,这笔帐记到谁身上呢?当然是其父进程身上,这里的思想就是当前的红黑树的最最左下的进程已经承诺给一个进程了,不应该给新的进程,除非最左下的进程就是新进程的父进程,我们不考虑别的进程,仅考虑父子进程,结果就是如果没有设置子进程先运行,那么将不考虑额外的什么,而是完全按照cfs的机制进行调度,新进程假设已经运行了min_vruntime个虚拟时间了,另外有运行了sched_vslice(cfs_rq, se)个虚拟时间,它已经完成了它的最后一次导致不公平的运行,将要入队,这很简单,是吗?是的!结果是当前进程或者别的进程继续运行而不管新进程的死活,但是一旦用户设置了新进程优先运行,那么新进程既然被cfs调度器认为已经在min_vruntime上运行了sched_vslice(cfs_rq, se)个虚拟时间,那么其vruntime肯定比当前进程的vruntime要大(证明很简单),如此一来的结果就是导致当前进程和新进程的vruntime的交换,新进程的优先运行!cfs中都是承诺预先运行sched_vslice(cfs_rq, se)个虚拟时间的,因为只有这样才会导致不公平,就好像小时候玩的赛跑游戏,你先跑我才能追你,所以必须一个进程先引起不公平才可以进程cfs调度。在设置了START_DEBIT的情况下,并且用户设置了子进程优先运行的情况下,即使父进程的当前的动时间片还没有完,也就是说当期的虚拟时间还没有推进完一次,那么还是会经过另一个更高层次的权衡,就是调度器的check_preempt_curr函数,这个函数也是调度类相关的,在cfs中,它的逻辑中含有:
if (wakeup_preempt_entity(se, pse) == 1) {
resched_task(curr);
...
这个wakeup_preempt_entity很有意思,它就是判定抢占的,即使没有设置debit特性当有新进程插入的时候,即使新进程的vruntime即使min_vrntime的情况下,父进程的vruntime如果也是min_vruntime的时候,在调度器的check_preempt_curr回调函数中也会有严格的检查来尽量抢占当前的进程,也就是印证了一句话,cfs调度器尽量保证公平,尽自己最大的努力来保证vruntime最小的进程来运行。以上是新进程的vruntime大于父进程的vruntine的情况下,如果新进程的vruntime小于父进程的vruntime的情况下,那么在check_preempt_curr的时候,抢占是必然的,2.6的内核是抢占的,cfs的内核的更加抢占的。
如果没有用户设置的子进程优先运行一说,我可能没有说清事情的本质,其实子进程优先运行一说和cfs的调服是两码事,cfs的调度器的特性在用户设置子进程优先运行的情况下并且没有设置debit的情况下确实会引起一些写时复制,但是真的不是很严重,因为这种情况下要么子进程就是min_vruntime直接抢占,要么就是子进程的vruntime在当前进程运行一会儿后超过小于当前进程,总之就是将写时复制的劣势降低到最低,这就够了。记住,一切都是权衡!!
我马上就要提交的patch就是做到完全的公平,任何时候只要有进程抢占,就又有响应!!!