文章说明:
-
Linux内核版本:5.0
-
架构:ARM64
-
参考资料及图片来源:《奔跑吧Linux内核》
-
Linux 5.0内核源码注释仓库地址:
1. 唤醒进程的流程
唤醒进程是操作系统中核心的操作之一,Linux内核提供了一个wake_up_process()
接口函数来唤醒进程。唤醒进程涉及应该由哪个CPU来运行唤醒迸程,是本地CPU或当前CPU(称为 wakeup_cpu,因为它调用了 wake_up_process() 函数)还是该进程之前运行的CPU(称为 prev_cpu)呢?
为了更好地解决这个问题,我们先从宏观上大致了解下唤醒进程的流程,如下图所示:
相信大家已经对唤醒进程有了宏观的认识了,下面为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程:
wake_up_process()->try_to_wake_up()->select_task_rq()->...->select_task_rq_fair()
:
// select_task_rq 方法在 CFS 调度类中的实现
// 参数 p 表示将要唤醒的进程
// 参数 prev_cpu 表示该进程上一次调度运行的 CPU
// 参数 sd_flag 表示调度域的标志位
// 参数 wake_flags 表示唤醒标志位
static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
{
struct sched_domain *tmp, *sd = NULL;
// 变量 CPU 指的是 wakeup_cpu
int cpu = smp_processor_id();
// 变量 new_cpu 指的是 prev_cpu
int new_cpu = prev_cpu;
int want_affine = 0;
// sync 表示是否需要同步
int sync = (wake_flags & WF_SYNC) && !(current->flags & PF_EXITING);
// 若 sd_flag 包含 SD_BALANCE_WAKE 标志位,表示这是一个唤醒进程的动作
if (sd_flag & SD_BALANCE_WAKE) {
...
// want_affine 表示有机会采用 wake_up 或者 prev_cpu 来唤醒这个进程,这是一个快速优化路径
want_affine = !wake_wide(p) && !wake_cap(p, cpu, prev_cpu) &&
cpumask_test_cpu(cpu, &p->cpus_allowed);
}
rcu_read_lock();
// for_each_domain() 由 wake_up 开始,从下至上遍历调度域
for_each_domain(cpu, tmp) {
if (!(tmp->flags & SD_LOAD_BALANCE))
break;
if (want_affine && (tmp->flags & SD_WAKE_AFFINE) &&
cpumask_test_cpu(prev_cpu, sched_domain_span(tmp))) {
if (cpu != prev_cpu)
// wake_affine() 函数会重新计算 wakeup_cpu 和 prev_cpu 的负载情况,并且比较使用哪个 CPU
// 来唤醒进程是最合适的。如果 wakeup_cpu 的负载加上唤醒进程的负载比 prev_cpu 的负载小,
// 那么使用 wakeup_cpu 来唤醒进程;否则,使用 prev_cpu。
new_cpu = wake_affine(tmp, p, cpu, prev_cpu, sync);
// 当找到合适的 CPU 来唤醒进程后,设置 sd 为 NULL,并退出 for 循环
sd = NULL; /* Prefer wake_affine over balance flags */
break;
}
...
}
// 若没找到合适的调度域(因为在之前代码里,找到合适调度域的情况会设置 sd 为 NULL),那么进入慢速优化路径
if (unlikely(sd)) {
// 慢速优化路径:调用 find_idlest_cpu() 来查找最悠闲的 CPU
new_cpu = find_idlest_cpu(sd, p, cpu, prev_cpu, sd_flag);
} else if (sd_flag & SD_BALANCE_WAKE) { /* XXX always ? */
// 快速优化路径:调用 select_idle_sibling() 函数来选择一个合适的 CPU
new_cpu = select_idle_sibling(p, prev_cpu, new_cpu);
if (want_affine)
current->recent_used_cpu = cpu;
}
rcu_read_unlock();
// 返回找到的合适 CPU
return new_cpu;
}
符合快速优化路径的3个条件:
- 这是一个唤醒进程的动作,即 sd_flag 包含 SD_BALANCE_WAKE 标志位
- wakeup_cpu 和 prev_cpu 在同一个调度域
- 调度域包含 SD_WAKE_AFFINE 标志位,表示运行唤醒进程的 CPU 可以运行这个被唤醒的进程
2. 快速优化路径
下面来看一下快速优化路径处理函数select_idle_sibling()
的实现:
// 快速优化路径处理函数,该函数优先选择空闲CPU,如果没找到空闲CPU,那么只能选择 prev_cpu 或 wakeup_cpu
// 参数 p 表示要唤醒的进程
// 参数 prev 表示 prev_cpu
// 参数 target 表示前面通过计算推荐的 CPU
static int select_idle_sibling(struct task_struct *p, int prev, int target)
{
...
// available_idle_cpu() 检查 target 指向的 CPU 是否为空闲 CPU
if (available_idle_cpu(target))
return target;
// cpus_share_cache() 函数判断两个 CPU 是否具有高速缓存的亲和性
// 若 prev_cpu 和 target_cpu 不是同一个 CPU,但是它们具有高速缓存的亲和性并且 prev_cpu现在是空闲 CPU,
// 那么也会优先选择 prev_cpu
if (prev != target && cpus_share_cache(prev, target) && available_idle_cpu(prev))
return prev;
// p->recent_used_cpu 记录了进程最近经常使用的 CPU
recent_used_cpu = p->recent_used_cpu;
if (recent_used_cpu != prev &&
recent_used_cpu != target &&
// recent_used_cpu 和 target_cpu 具有共享高速缓存的亲和性
cpus_share_cache(recent_used_cpu, target) &&
// recent_used_cpu 现在是空闲 CPU
available_idle_cpu(recent_used_cpu) &&
// recent_used_cpu 在进程允许运行的 CPU 位图里
cpumask_test_cpu(p->recent_used_cpu, &p->cpus_allowed)) {
p->recent_used_cpu = prev;
// 若满足上述条件,那么选择 recent_used_cpu 作为候选者,并且返回 recent_used_cpu
return recent_used_cpu;
}
// 若不能找到合适的 CPU 来唤醒进程,那么只能从调度域里找了
sd = rcu_dereference(per_cpu(sd_llc, target));
if (!sd)
return target;
i = select_idle_core(p, sd, target);
if ((unsigned)i < nr_cpumask_bits)
return i;
i = select_idle_cpu(p, sd, target);
if ((unsigned)i < nr_cpumask_bits)
return i;
i = select_idle_smt(p, sd, target);
if ((unsigned)i < nr_cpumask_bits)
return i;
// 如果上述遍历过程都没找到合适的CPU,那么只能返回 target_cpu
return target;
}
3. 慢速优化路径
下面来看一下慢速优化路径处理函数find_idlest_cpu()
的实现:
// 慢速优化路径
// 参数 sd 表示从这个调度域里查找的最合适的候选者
// 参数 p 表示将要唤醒的进程
// 参数 cpu 表示 wakeup_cpu
// 参数 prev_cpu 表示唤醒进程之前运行的 CPU
// 参数 sd_flag 表示调用者传递下来的调度域标志
static inline int find_idlest_cpu(struct sched_domain *sd, struct task_struct *p,
int cpu, int prev_cpu, int sd_flag)
{
int new_cpu = cpu;
// 若 sd 里的 cpu 都不在进程允许运行的 CPU 位图里,则直接返回 prev_cpu
if (!cpumask_intersects(sched_domain_span(sd), &p->cpus_allowed))
return prev_cpu;
// 若 sd_flag 标志没有包含 SD_BALANCE_FORK,则说明不是因为 fork() 系统调用而调用到本函数的,
// 调用 sync_entity_load_avg() 函数更新系统的负载信息
if (!(sd_flag & SD_BALANCE_FORK))
sync_entity_load_avg(&p->se);
// 从 sd 开始,自上而下地遍历调度域
while (sd) {
...
// 调用 find_idlest_group() 函数查找一个最空闲的调度组。遍历 sd 中所有的调度组,通过比较每个
// 调度组中的量化负载来找出负载最小的一个调度组。
group = find_idlest_group(sd, p, cpu, sd_flag);
if (!group) {
sd = sd->child;
continue;
}
// find_idlest_group_cpu() 从上述调度组中找出一个负载最小的 CPU 作为最佳候选者
new_cpu = find_idlest_group_cpu(group, p, cpu);
if (new_cpu == cpu) {
sd = sd->child;
continue;
}
...
}
return new_cpu;
}
4. wake affine 特性
select_task_rq_fair()
函数中的 wake affine 特性希望把被唤醒进程尽可能地运行在 wakeup_cpu 上,这样可以让—些有相关性的进程尽可能地运行在具有高速缓存共享的调度域中,获得一些高速缓存命中带来的性能提升。在task_struct
数据结构中增加了两个成员——last_wakee
和wakee_flips
。wakee_flips
表示 waker(唤醒进程) 切换不同的 wakee(被唤醒进程) 的个数,即如果 waker 发现上次唤醒的进程不是 wakee 则 wakee_flips++。
但这也是一把“双刃剑”,如果一个 wakee 的 wakee_flips 值比较大,那么 waker 把这种 wakee 放到自身的 CPU 中来运行是比较危险的事情(类似于“引狼入室”),把 wakee 的下线 wakee 进程都放到自身的CPU上,加剧了CPU调度的竞争。另外, waker 的 wakee_flips 值比较大,说明很多进程依赖它来唤醒,waker的调度延迟会增大,再把新的 wakee 放进来显然不是好办法。因此代码中会通过判断来过滤上述情况。
static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
{
...
// want_affine 表示有机会采用 wake_up 或者 prev_cpu 来唤醒这个进程,这是一个快速优化路径
want_affine = !wake_wide(p) && !wake_cap(p, cpu, prev_cpu) &&
cpumask_test_cpu(cpu, &p->cpus_allowed);
...
}
static int wake_wide(struct task_struct *p)
{
// 从当前任务结构体中获取 wakee_flips 值,存储在 master 变量中
unsigned int master = current->wakee_flips;
// 从传入的任务结构体中获取 wakee_flips 值,存储在 slave 变量中
unsigned int slave = p->wakee_flips;
// 从当前 CPU 的 sd_llc_size 变量中读取值,存储在 factor 变量中
int factor = this_cpu_read(sd_llc_size);
// 如果当前任务的 wakee_flips 值小于传入任务的 wakee_flips 值,则交换它们的值
if (master < slave)
swap(master, slave);
// 如果传入任务的 wakee_flips 值小于 factor,或者当前任务的 wakee_flips 值小于传入任务的 wakee_flips 值乘以 factor,则返回 0
if (slave < factor || master < slave * factor)
return 0;
// 否则返回 1
return 1;
}