为方便称呼,执行唤醒流程的进程是waker,被唤醒进程是wakee。
waker所在的cpu,waker与wakee可能具有某种程度的亲和性;
CPU的亲和性
,进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性,进程迁移的频率小就意味着产生的负载小。亲和性一词是从affinity翻译来的,实际可以称为CPU绑定。
作用
: 在多核运行的机器上,每个CPU本身自己会有缓存,在缓存中存着进程使用的数据,而没有绑定CPU的话,进程可能会被操作系统调度到其他CPU上,如此CPU cache(高速缓冲存储器)命中率就低了,也就是说调到的CPU缓存区没有这类数据,要先把内存或硬盘的数据载入缓存。而当缓存区绑定CPU后,程序就会一直在指定的CPU执行,不会被操作系统调度到其他CPU,性能上会有一定的提高。
wakee上次执行的cpu,也许cache上的数据还是hot的;
同理,与waker所在cpu或者wakee上次执行的cpu共享cache的cpu也有很高的选择优先级;
处于负载均衡考虑,放在某个idle的cpu上会更高效利用系统资源;
处于功耗考虑,选择一个cpu使得耗能最少。
显而易见,选择cpu是一个非常复杂的问题。
我们从try_to_wake_up -->select_task_rq 此条路径出发,简单了解一下睡眠进程被唤醒过程中的选核思路。
select_task_rq 函数会选择wakee 的调度类中定义的select_task_rq 去执行。
这里以CFS中定义的select_task_rq_fair 函数为例进行分析。
先看函数开始部分:
staticint select_task_rq_fair( structtask_struct*p, intprev_cpu, intsd_flag, intwake_flags)
{
structs ched_domain* tmp, *sd=NULL;
int cpu=smp_processor_id; //cpu 为waker 现在执行的cpu;
int new_cpu=prev_cpu; //new_cpu 以及prev_cpu 为wakee 上次执行的cpu;
int want_affine=0; //want_affine 用于表示是否满足亲和条件,在后续判断(如果满足亲和条件,则将进程唤醒到waker 现在执 行的cpu上也是一个比较优的选择);
int sync=( wake_flags&WF_SYNC) &&!( current->flags&PF_EXITING);
/*
sync 是用来表示waker 与wakee 之间的关系的。
我们认为waker与wakee之间有两种关系:sync,以及non-sync。
sync——waker在唤醒wakee的时候知道自己很快进入睡眠状态,故最好不要进行抢占。
non-sync——没有同步关系,唤醒的时候,可以尝试触发一次调度。
在这里,sync 本质上是放宽了对waker 的cpu的idle的判断条件。*/
if( sd_flag&SD_BALANCE_WAKE) {
record_wakee( p);
if( sched_energy_enabled) {
new_cpu=find_energy_efficient_cpu( p, prev_cpu);
if( new_cpu>=0)
returnnew_cpu;
new_cpu=prev_cpu;
}
want_affine=!wake_wide( p) &&cpumask_test_cpu( cpu, p->cpus_ptr);
}
进程唤醒可以视为一次主动的BALANCE。
record_wakee 主要用于更新waker 的wakee_flips ,用于后续的判断。
staticvoidrecord_wakee( structtask_struct*p)
{
/*
* Only decay a single time; tasks that have less then 1 wakeup per
* jiffy will not have built up many flips.
*/
if( time_after( jiffies, current->wakee_flip_decay_ts+HZ)) {
current->wakee_flips>>=1;
current->wakee_flip_decay_ts=jiffies;
}
if( current->last_wakee!=p) {
current->last_wakee=p;
current->wakee_flips++;
}
}
task_struct 中有三个成员:
wakee_flips_decay_ts 表示上次进行衰减的时刻;
last_wakee 为上次由它唤醒的进程;
wakee_flips 度量由它唤醒进程的数量,wakee_flips 一秒之前的计数全部除以2,有点衰减的那味儿了。
sched_energy_enabled 分支的部分,与功耗EAS调度器有关,暂不分析。
可以明确的是,find_energy_efficient_cpu 用于在开启功耗模型下寻找功耗最低的cpu去执行。
wake_wide 函数检测waker /wakee 是否符合wake_affine 模型。(最终用want_affine 表示)
当wake_wide 支持以及waker 所在的cpu可以执行wakee 时,want_affine 生效。
符合wake_affine 模型则优先去waker 现在执行以及wakee 上次执行的cpus中进行选择(放宽对waker 执行cpu的条件,最后从二者中挑选一个target cpu)。
staticintwake_wide( structtask_struct*p)
{
unsignedintmaster=current->wakee_flips;
unsignedintslave=p->wakee_flips;
intfactor=__this_cpu_read( sd_llc_size);
if( master<slave)
swap( master, slave);
if( slave<factor||master<slave*factor)
return0;
return1;
}
sd_llc_domain 为当前sched_domain 中能够共享cache的CPU数目;
将waker 和wakee 中wakee_flips 中较大的称为master ,较小的称为slave 。
当较小的wakee_flips 或者较大的wakee_flips 与较小的wakee_flips 的比值小于sd_llc_size 是可以作为wake_affine 生效的一部分判断依据。
继续看select_task_rq_fair 函数:
rcu_read_lock;
for_each_domain( cpu, tmp) {
/*
* If both 'cpu' and 'prev_cpu' are part of this domain,
* cpu is a valid SD_WAKE_AFFINE target.
*/
if( want_affine&&( tmp->flags&SD_WAKE_AFFINE) &&
cpumask_test_cpu( prev_cpu, sched_domain_span( tmp))) {
if( cpu!=prev_cpu)
new_cpu=wake_affine( tmp, p, cpu, prev_cpu, sync);
sd=NULL; /* Prefer wake_affine over balance flags */
break;
}
if( tmp->flags&sd_flag)
sd=tmp;
elseif( !want_affine)
break;
}
以waker 执行的cpu调度域逐步向上,进行如下判断:
当want_affine 成立,并且调度域满足亲和性条件,并且waker 的cpu与wakee 上次执行cpu在同一调度域中时,sd 为NULL。
如果条件均不满足,则sd 置为最后满足sd_flag 的调度域。
如果满足want_affine ,退出。
在满足第1个条件,且waker 执行的cpu与wakee 上次执行cpu不相等的情况下,执行wake_affine 进行选择。
wake_affine 本质上是在waker 现在执行的cpu以及wakee 上次执行的cpu进行对比,决定要不要给waker 现在执行的cpu一点机会。
staticintwake_affine( structsched_domain*sd, structtask_struct*p,
intthis_cpu, intprev_cpu, intsync)
{
inttarget=nr_cpumask_bits;
if( sched_feat( WA_IDLE))
target=wake_affine_idle( this_cpu, prev_cpu, sync);
if( sched_feat( WA_WEIGHT) &&target==nr_cpumask_bits)
target=wake_affine_weight( sd, p, this_cpu, prev_cpu, sync);
schedstat_inc( p->se. statistics. nr_wakeups_affine_attempts);
if( target==nr_cpumask_bits)
returnprev_cpu;
schedstat_inc( sd->ttwu_move_affine);
schedstat_inc( p->se. statistics. nr_wakeups_affine);
returntarget;
}
首先,如果waker 执行的cpu(此时waker 为idle,中断唤醒wakee )为空闲或者很快进入空闲态或者wakee 上次执行的cpu为空闲,则通过wake_affine_idle 可以找到一个合适的target cpu。
staticint wake_affine_idle( intthis_cpu, intprev_cpu, intsync)
{
if( available_idle_cpu( this_cpu) &&cpus_share_cache( this_cpu, prev_cpu))
returnavailable_idle_cpu( prev_cpu) ?prev_cpu: this_cpu;
if( sync&&cpu_rq( this_cpu) ->nr_running==1)
returnthis_cpu;
returnnr_cpumask_bits;
}
如果waker 的cpu空闲(中断唤醒)并且waker 执行的cpu与wakee 上次执行的cpu共享cache时:
wakee 上次执行的cpu也空闲,那么回到上次执行的cpu,否则是waker 的cpu。
否则sync 为1(表示waker 很快要退出)并且只有一个进程在执行,那么返回waker 执行的cpu。
如果在wake_affine_idle 中没有找到target cpu,则进入wake_affine_weight 中继续寻找。
staticint
wake_affine_weight( structsched_domain*sd, structtask_struct*p,
intthis_cpu, intprev_cpu, intsync)
{
s64this_eff_load, prev_eff_load;
unsignedlongtask_load;
this_eff_load=cpu_load( cpu_rq( this_cpu));
if( sync) {
unsignedlongcurrent_load=task_h_load( current);
if( current_load>this_eff_load)
returnthis_cpu;
this_eff_load-=current_load;
}
task_load=task_h_load( p);
this_eff_load+=task_load;
if( sched_feat( WA_BIAS))
this_eff_load*=100;
this_eff_load*=capacity_of( prev_cpu);
prev_eff_load=cpu_load( cpu_rq( prev_cpu));
prev_eff_load-=task_load;
if( sched_feat( WA_BIAS))
prev_eff_load*=100+( sd->imbalance_pct-100) /2;
prev_eff_load*=capacity_of( this_cpu);
if( sync)
prev_eff_load+=1;
returnthis_eff_load<prev_eff_load?this_cpu: nr_cpumask_bits;
}
计算 waker 的负载以及cpu负载,在 waker 即将执行完成的情况下,cpu负载减去进程负载;
waker 的cpu负载加上 wakee 的进程负载
wakee 的cpu负载减去 wakee 的进程负载(此处是假设进程从上次执行的cpu迁移到 waker 执行的cpu)
最终,哪个cpu负载小,最后return哪个cpu。
如果sd 不为空,则进入find_idlest_cpu 这条慢速路径。
否则,进入select_idle_sibling 快速路径。
if( unlikely( sd)) {
/* Slow path */
new_cpu=find_idlest_cpu( sd, p, cpu, prev_cpu, sd_flag);
} elseif( sd_flag&SD_BALANCE_WAKE) { /* XXX always ? */
/* Fast path */
new_cpu=select_idle_sibling( p, prev_cpu, new_cpu);
if( want_affine)
current->recent_used_cpu=cpu;
}
rcu_read_unlock;
returnnew_cpu;
}
find_idlest_cpu 为SMP负载均衡的常用函数,即选择负载最小的cpu去执行。
select_idle_sibling 在之前挑选出来的target cpu与上一次执行的cpu(有可能taget与上一次执行是一个cpu)共享cache的调度域中,根据负载选择一个最合适的cpu去执行。
总结
cpu的选择,主要有以下关键点(满足SD_BALANCE_WAKE):
find_energy_efficient_cpu,在EAS调度器开启下启用,选择功耗最小的cpu;
如果未开启EAS调度器,首先判断是否根据 wake_wide (依据唤醒进程的强度)以及进程可执行的cpu掩码决定满足 waker 执行cpu的亲和性;
如果满足亲和性,会通过 wake_affine 来判断要不要给唤醒 wakee 的cpu一点执行进程的机会;
不满足亲和性或者调度域不支持情况下,会走到 find_idlest_cpu (慢速路径),进行负载均衡的选择;
否则,进入快速路径,通过 select_idle_sibling ,在 wake_affine 挑选出来的target cpu以及上一次执行的cpu共享cache的调度域中,根据负载选择一个cpu进行执行。