在我提交并自己打上child-runs-first补丁之前,我做了一个实验,目的是验证一下我先前理论分析的结果,我觉得子进程无论如何抢占父进程的几率都会比不抢占要大些,当然前提是要有的,具体见下面的试验。空有理论是没有用的,理论上cfs调度器执行严格的归一化,然而实际上却不是那样的,实践结果永远比理论更加现实,因为不执行归一化丝毫影响不了cfs调度器选择最小vruntime的进程,这很简单,每一个进程按照自己的权值和当前的系统调度周期执行动态时间片的时间,同时按照不同的速率推进自己的虚拟时间,调度器只要能保证各个进程在运行按照其权值分给它们的不同时间片之后可以向前推进相同的虚拟时间就可以了,管他什么归一化呢?正是这样,cfs调度器运行的很好。
环境:单cpu,HZ=250,linux-2.6.28原始内核(没有打我的child_runs_first补丁)
内核配置与实验前提:sysctl_sched_child_runs_first=0,sysctl_sched_features=(仅打开AKEUP_PREEMPT),sysctl_sched_wakeup_granularity=0,sysctl_sched_latency_ns=20000000.
目的:在没有sysctl_sched_child_runs_first策略的情况下测试cfs调度器在子进程被唤醒时的行为
测试程序:
-------stub--------
/*模拟cpu进程,将cfs_rq的nr_running提高到一定数量*/
int main(int argc,char*argv[] )
{
nice(atoi(argv[1]));
int a = 1,b=0;
while(a++||1)
{
b+=a;
}
}
-------child_run_delay--------
/*父进程延迟一会再fork,避免托shell在fork父进程时给了父进程min_vruntime的福*/
#include
#include
#include
int main(int argc,char *argv[])
{
int v = atoi(argv[1]);
nice(v);
unsigned long i = 1000000;
while(i-->0)
{
v++;
}
if(fork() == 0)
{
printf("sub/n");
exit(0);
}
printf("main,%d/n",v);
}
-------child_run_nodelay--------
/*马上fork子进程,托父进程vruntime很小的福*/
#include
#include
#include
int main(int argc,char *argv[])
{
int v = atoi(argv[1]);
nice(v);
if(fork() == 0)
{
printf("sub/n");
exit(0);
}
printf("main/n");
}
测试过程:连续创建9个stub进程,nice值分散开来,然后以不同的nice值运行child_run_delay和child_run_nodelay。
结果:
测试代码中i=1000时的测试结果
child_run_delay |
child_run_nodelay |
|
nice 15 |
96%preempt |
95%preempt |
nice 10 |
96%preempt |
94%preempt |
nice -10 |
70%preempt |
12%preempt |
nice -17 |
64%preempt |
10%preempt |
nice -20 |
11%preempt |
4%preempt |
测试代码中i=10000000时的测试结果
child_run_delay |
child_run_nodelay |
|
nice 15 |
36%preempt |
96%preempt |
nice 10 |
33%preempt |
96%preempt |
nice -10 |
24%preempt |
10%preempt |
nice -17 |
15%preempt |
9%preempt |
nice -20 |
9%preempt |
6%preempt |
结果分析:
child_run_delay进程和child_run_nodelay进程的创建也是shell执行fork而来的,那时这些进程将被赋予cfs_rq的min_vruntime,不论如何,这个vruntime是所有运行进程的最小的vruntime,包括正在执行的curr(因为min_vruntime在update_curr中被实时更新),cfs_rq的min_vruntime是单调递增的,只有在当前进程是最后一个被落下的进程时,cfs_rq的min_vruntime才会和curr的vruntime相等,也就是说cfs_rq的min_vruntime永远是最小的vruntime,这个比2.6.28以前的有所改进,不会拉大进程间vruntime的距离从而造成对vruntime值大的进程的不公平。既然child_run_delay进程和child_run_nodelay进程的vruntime是最小的,那么如果它们的nice值小的话,其虚拟时钟推进慢,到了fork的时候其vruntime可能还是最小的,于是根据代码和内核配置,抢占的几率就会变小。什么叫变小呢?谁和谁比呢?其实就是main刚执行的时候和fork的时候比较,另外还有一个比较,就是delay程序和nodelay程序的比较,对于nodely,抢占的几率更小,因为父进程趁着自己的vruntime最小的时候,几乎是马上就fork了子进程,此时子进程被赋予最小的min_vruntime,而此时此vruntime很大很大几率就是父进程的还没有推进几步vruntime,如果父进程的nice值很小,推进的更慢,不抢占的几率更大,但是如果父进程的nice值很大的话,虚拟时钟即vruntime推进的很快,稍微一执行就不是最小的了,那么到了fork的时抢占就是很大几率的事情了。
sched_latency_ns比较小,vruntime推进很活跃的情况下,即使执行-20的nice值的child_run_delay测试进程也可能发生抢占并且几率很大,这好像和上面一段的分析结果相反,为什么,因为在执行了很大一会后并且执行的都是cpu指令,没有睡眠之类的,该进程的vruntime即使权值再高它也不一定是在一个系统调度周期(sysctl_sched_latency_ns)最后一个运行的进程,更何况由于进程的不确定的睡眠唤醒新建退出,严格的虚拟时钟归一化已经被打乱,只要不是最后一个运行的进程,也就是说只要说这个进程不是被落下的那一个,那么该进程的vruntime就一定大于cfs_rq的min_vruntime,于是该进程在fork子进程的时候,由于cfs_rq的min_vruntime给了新进程,那么新进程的vruntime会小于current(即高权值的父进程)的vruntime,如果抢占粒度很小的话,那么子进程就会抢占父进程,而我们为了使事情简单化将抢占粒度设置为0,于是抢占发生。可是如果高权值的父进程如果是最后一个运行的,那么由于它的虚拟时钟推进很慢,因此很大的几率下其vruntime将一直是cfs_rq的min_vruntime,如果是,那么抢占不发生,可是即使该种情况下该进程的vruntime和cfs_rq的vruntime相等的几率再大,该进程被落下为最后一个的几率本身就很小,毕竟每一个进程被落下为最后一个都有可能,因此不论如何,发生抢占的几率还是要比不发生抢占的几率大,符合测试,如果将活动进程的数量减小,那么不发生抢占的几率会变大,但是统计意义上还是高不过发生抢占的几率,fork前执行cpu指令的时间越长,效果越明显。
被隐蔽的真相:如果将child_run_delay中的i值减小,结果就是越低nice值的进程越不容易发生抢占,高nice值的几乎都会抢占。这其实是shell在执行我们的测试程序时引入的,原因见上述分析,i的值足够小,i--的循环时间没有到达该父进程的动态时间片,因此就会出现容易误导用户的结果。
事情的真相:不要以为权值大的进程就会被最后运行,不要以为权值大的进程的vruntime就一定是cfs_rq的min_vruntime,不是这样的,以理想情况分析,一个调度周期内谁最后运行是不一定的,要看它们入队的顺序以及入队时红黑树的既有情况。在不借助父进程被shell创建时被赋予很小的vruntime的优势的情况下,也就是fork之前不睡眠不阻塞运行足够长的时间的情况下,fork时的子进程抢占父进程的几率更大些,和父进程的优先级没有必然关系,只能说相同条件下低权值的进程抢占绝对几率更大些但是永远不会超过不抢占发生的几率,因为那么多进程只要父进程不是最后一个运行的那么就不会抢占,而每个进程最后一个运行的机会近乎均等。