Linux一直坚持采用对称多处理模式,这意味着,与其他CPU相比,内核不对一个CPU有任何偏向,但是,多处理器机器具有很多不同的风格,而且调度程序的实现随硬件特征的不同而有所不同,我们将特别关注下面三种不同类型的多处理器机器:
(1)标准的多处理器体系结构
直到最近,这是多处理器机器最普通的体系结构。这些机器所共有的RAM芯片集被所有CPU共享。
(2)超线程
超线程芯片是一个立刻执行几个执行线程的微处理器;它包括几个内部寄存器的拷贝,并快速在它们之间切换。这种由Intel发明的技术,使得当前线程在访问内存的间隙,处理器可以使用它的机器周期去执行另外一个线程。一个超线程的物理CPU可以被Linux看作几个不同的逻辑CPU。
(3)NUMA
把CPU和RAM以本地“结点”为单位分组,(通常一个结点包括一个CPU和几个RAM芯片)。内存仲裁器(一个使系统中的CPU以串型方式访问RAM的专用电路)是典型的多处理器系统的瓶颈。在NUMA体系结构中,当CPU访问与它同在一个结点中的“本地”RAM芯片时,几乎没有竞争,因此访问通常是非常快的。另一方面,访问它所属结点外的“远程”RAM芯片就非常慢。
这些基本的多处理器系统类型经常被组合使用。例如,内核把一个包括两个不同超线程CPU的主板看作四个逻辑CPU。
正如我们在上一节所看到的,schedule( )函数从本地CPU的运行队列挑选新进程运行。因此,一个指定的CPU只能执行它相应的运行队列中的可运行进程。另外,一个可运行进程总是存放在某一个运行队列中:任何一个可运行进程都不可能同时出现在两个或多个运行队列中。因此,一个保持可运行状态的进程通常被限制在一个固定的 CPU上。
这种设计通常对系统性能是有益的,因为,运行队列中的可运行进程所拥有的数据可能填满每个CPU的硬件高速缓存。但是,在有些情况下,把可运行进程限制在一个指定的CPU上可能引起严重的性能损失。例如,考虑频繁使用CPU的大量批处理进程:如果他们绝大多数都在同一个运行队列中,那么系统中一个CPU将会超负荷,而其他一些CPU几乎处于空闲状态。
因此,内核周期性地检查运行队列的工作量是否平衡,并在需要的时候,把一些进程从一个运行队列迁移到另一个运行队列。但是,为了从多处理器系统获得最佳性能,负载平衡算法应该考虑系统中CPU的拓扑结构。从内核2.6.7版本开始,Linux提出一种基于“调度域”概念的复杂的运行队列平衡算法。正是有了调度域这一概念,使得这种算法能够很容易适应各种已有的多处理器体系结构(甚至诸如那些基于“多核”微处理器的新近出现的体系结构)。
调度域实际上是一个CPU集合,他们的工作量应当由内核保持平衡。一般来说,调度域采取分层的组织形式:最上层的调度域(通常包括系统中的所有CPU)包括多个子调度域,每个子调度域包括一个CPU子集。正是调度域的这种分层结构,使工作量的平衡能以如下有效方式来实现:
每个调度域被依次划分成一个或多个组,每个组代表调度域的一个CPU子集。工作量的平衡总是在调度域的组之间来完成。换而言之,只有在一些调度域的某些组的总工作量远远低于同一个调度域的另一个组的工作量时,才把进程从一个CPU迁移到另一个CPU。
下图说明三个调度域分层实例,对应三种主要的多处理器机器体系结构:
图中(a)表示具有两CPU的标准多处理器体系结构中由单个调度域组成的一个层次结构,该调度域包括两个组,每个组有一个CPU。
图中(b)表示一个两层的层次结构,用在使用超线程技术、有两CPU的多处理器结构中。最上层的调度域包括了系统中所有四个逻辑CPU,它由两个组构成。上层域的每个组对应一个子调度域并包括一个物理CPU。底层的调度域(也被称为基本调度域)包括两个组,每个组一个逻辑CPU。
最后,图中(c)表示有两个结点,每个结点有四个CPU的8-CPUNUMA体系结构上的两层层次结构。最上层的域由两个组构成,每个组对应一个不同的结点。每个基本调度域包括一个结点内的CPU,包括四个组,每个组包括一个CPU。
每个调度域由一个 sched_domain描述符表示,而调度域中的每个组由sched_group 描述符表示。每个sched_domain 描述符包括一个groups字段,它指向组描述符链表中的第一个元素。此外,sched_domain 结构的parent字段指向父调度域的描述符(如果有的话)。
struct sched_domain {
/* These fields must be setup */
struct sched_domain *parent; /* top domain must be null terminated */
struct sched_group *groups; /* the balancing groups of the domain */
cpumask_t span; /* span of all CPUs in this domain */
unsigned long min_interval; /* Minimum balance interval ms */
unsigned long max_interval; /* Maximum balance interval ms */
unsigned int busy_factor; /* less balancing by factor if busy */
unsigned int imbalance_pct; /* No balance until over watermark */
unsigned long long cache_hot_time; /* Task considered cache hot (ns) */
unsigned int cache_nice_tries; /* Leave cache hot tasks for # tries */
unsigned int per_cpu_gain; /* CPU % gained by adding domain cpus */
unsigned int busy_idx;
unsigned int idle_idx;
unsigned int newidle_idx;
unsigned int wake_idx;
unsigned int forkexec_idx;
int flags; /* See SD_* */
/* Runtime fields. */
unsigned long last_balance; /* init to jiffies. units in jiffies */
unsigned int balance_interval; /* initialise to 1. units in ms. */
unsigned int nr_balance_failed; /* initialise to 0 */
#ifdef CONFIG_SCHEDSTATS
/* load_balance() stats */
unsigned long lb_cnt[MAX_IDLE_TYPES];
unsigned long lb_failed[MAX_IDLE_TYPES];
unsigned long lb_balanced[MAX_IDLE_TYPES];
unsigned long lb_imbalance[MAX_IDLE_TYPES];
unsigned long lb_gained[MAX_IDLE_TYPES];
unsigned long lb_hot_gained[MAX_IDLE_TYPES];
unsigned long lb_nobusyg[MAX_IDLE_TYPES];
unsigned long lb_nobusyq[MAX_IDLE_TYPES];
/* Active load balancing */
unsigned long alb_cnt;
unsigned long alb_failed;
unsigned long alb_pushed;
/* SD_BALANCE_EXEC stats */
unsigned long sbe_cnt;
unsigned long sbe_balanced;
unsigned long sbe_pushed;
/* SD_BALANCE_FORK stats */
unsigned long sbf_cnt;
unsigned long sbf_balanced;
unsigned long sbf_pushed;
/* try_to_wake_up() stats */
unsigned long ttwu_wake_remote;
unsigned long ttwu_move_affine;
unsigned long ttwu_move_balance;
#endif
};
系统中所有物理CPU的sched_domain 描述符都存放在每CPU变量phys_domains中。如果内核不支持超线程技术,这些域就在域层次结构的最底层,运行队列描述符rq的sd字段指向它们,即它们是基本的调度域。相反,如果内核支持超线程技术,底层调度域存放在每CPU变量cpu_domains中。
为了保持系统中运行队列的平衡,每次经过一次时钟节拍scheduler_tick( ),就调用rebalance_tick( )函数:
static void rebalance_tick(int this_cpu, struct rq *this_rq, enum idle_type idle)
{
unsigned long this_load, interval, j = cpu_offset(this_cpu);
struct sched_domain *sd;
int i, scale;
this_load = this_rq->raw_weighted_load;
/* Update our load: */
for (i = 0, scale = 1; i < 3; i++, scale <<= 1) {
unsigned long old_load, new_load;
old_load = this_rq->cpu_load[i];
new_load = this_load;
/*
* Round up the averaging division if load is increasing. This
* prevents us from getting stuck on 9 if the load is 10, for
* example.
*/
if (new_load > old_load)
new_load += scale-1;
this_rq->cpu_load[i] = (old_load*(scale-1) + new_load) / scale;
}
for_each_domain(this_cpu, sd) {
if (!(sd->flags & SD_LOAD_BALANCE))
continue;
interval = sd->balance_interval;
if (idle != SCHED_IDLE)
interval *= sd->busy_factor;
/* scale ms to jiffies */
interval = msecs_to_jiffies(interval);
if (unlikely(!interval))
interval = 1;
if (j - sd->last_balance >= interval) {
if (load_balance(this_cpu, this_rq, sd, idle)) {
/*
* We've pulled tasks over so either we're no
* longer idle, or one of our SMT siblings is
* not idle.
*/
idle = NOT_IDLE;
}
sd->last_balance += interval;
}
}
}
它接受的参数有:本地CPU的索引this_cpu 、本地运行队列的地址this_rq 以及一个标志idle,该标志可以取下面的值:
SCHED_IDLE:CPU当前空闲,即current是swapper进程。
NOT_IDLE:CPU当前不空闲,即current不是swapper进程。
rebalance_tick( )函数首先确定运行队列中的进程数,并更新进程队列的平均工作量,为了完成这个工作,函数要访问运行队列描述符的nr_running and cpu_load字段。
随后,rebalance_tick( )开始在所有调度域上的循环,其路径是从基本域(本地运行队列描述符的sd字段所引用的域)到最上层的域。在每次循环中,函数确定是否已到调用函数load_balance( )的时间,从而在调度域上执行重新平衡的操作。由存放在sched_domain域中的参数和idle值决定调用load_balance( )的频率。如果idle等于SCHED_IDLE,那么运行队列为空,rebalance_tick( )就以很高的频率调用load_balance( )(大概每一到两个节拍处理一次对应于逻辑和物理CPU 的调度域)。相反,如果idle 等于 NOT_IDLE,rebalance_tick( )就以很低的频率调度load_balance( )(大概每10ms处理一次逻辑CPU对应的调度域,每100ms处理一次物理CPU对应的调度域)。
load_balance( )函数检查是否调度域处于严重的不平衡状态。更确切地说,它检查是否可以通过把最繁忙的组中的一些进程迁移到本地CPU的运行队列来减轻不平衡的状况,如果是,函数尝试实现这个迁移。
static int load_balance(int this_cpu, struct rq *this_rq,
struct sched_domain *sd, enum idle_type idle)
{
int nr_moved, all_pinned = 0, active_balance = 0, sd_idle = 0;
struct sched_group *group;
unsigned long imbalance;
struct rq *busiest;
cpumask_t cpus = CPU_MASK_ALL;
if (idle != NOT_IDLE && sd->flags & SD_SHARE_CPUPOWER &&
!sched_smt_power_savings)
sd_idle = 1;
schedstat_inc(sd, lb_cnt[idle]);
redo:
group = find_busiest_group(sd, this_cpu, &imbalance, idle, &sd_idle,
&cpus);
if (!group) {
schedstat_inc(sd, lb_nobusyg[idle]);
goto out_balanced;
}
busiest = find_busiest_queue(group, idle, imbalance, &cpus);
if (!busiest) {
schedstat_inc(sd, lb_nobusyq[idle]);
goto out_balanced;
}
BUG_ON(busiest == this_rq);
schedstat_add(sd, lb_imbalance[idle], imbalance);
nr_moved = 0;
if (busiest->nr_running > 1) {
/*
* Attempt to move tasks. If find_busiest_group has found
* an imbalance but busiest->nr_running <= 1, the group is
* still unbalanced. nr_moved simply stays zero, so it is
* correctly treated as an imbalance.
*/
double_rq_lock(this_rq, busiest);
nr_moved = move_tasks(this_rq, this_cpu, busiest,
minus_1_or_zero(busiest->nr_running),
imbalance, sd, idle, &all_pinned);
double_rq_unlock(this_rq, busiest);
/* All tasks on this runqueue were pinned by CPU affinity */
if (unlikely(all_pinned)) {
cpu_clear(cpu_of(busiest), cpus);
if (!cpus_empty(cpus))
goto redo;
goto out_balanced;
}
}
if (!nr_moved) {
schedstat_inc(sd, lb_failed[idle]);
sd->nr_balance_failed++;
if (unlikely(sd->nr_balance_failed > sd->cache_nice_tries+2)) {
spin_lock(&busiest->lock);
/* don't kick the migration_thread, if the curr
* task on busiest cpu can't be moved to this_cpu
*/
if (!cpu_isset(this_cpu, busiest->curr->cpus_allowed)) {
spin_unlock(&busiest->lock);
all_pinned = 1;
goto out_one_pinned;
}
if (!busiest->active_balance) {
busiest->active_balance = 1;
busiest->push_cpu = this_cpu;
active_balance = 1;
}
spin_unlock(&busiest->lock);
if (active_balance)
wake_up_process(busiest->migration_thread);
/*
* We've kicked active balancing, reset the failure
* counter.
*/
sd->nr_balance_failed = sd->cache_nice_tries+1;
}
} else
sd->nr_balance_failed = 0;
if (likely(!active_balance)) {
/* We were unbalanced, so reset the balancing interval */
sd->balance_interval = sd->min_interval;
} else {
/*
* If we've begun active balancing, start to back off. This
* case may not be covered by the all_pinned logic if there
* is only 1 task on the busy runqueue (because we don't call
* move_tasks).
*/
if (sd->balance_interval < sd->max_interval)
sd->balance_interval *= 2;
}
if (!nr_moved && !sd_idle && sd->flags & SD_SHARE_CPUPOWER &&
!sched_smt_power_savings)
return -1;
return nr_moved;
out_balanced:
schedstat_inc(sd, lb_balanced[idle]);
sd->nr_balance_failed = 0;
out_one_pinned:
/* tune up the balancing interval */
if ((all_pinned && sd->balance_interval < MAX_PINNED_INTERVAL) ||
(sd->balance_interval < sd->max_interval))
sd->balance_interval *= 2;
if (!sd_idle && sd->flags & SD_SHARE_CPUPOWER &&
!sched_smt_power_savings)
return -1;
return 0;
}
它接受四个参数:
this_cpu:本地CPU的索引
this_rq:本地运行队列的描述符的地址
sd:指向被检查的调度域的描述符
idle:取值为SCHED_IDLE(本地CPU空闲)或NOT_IDLE
函数执行下面的操作:
1.获取this_rq->lock自旋锁。
2.调用find_busiest_group( )函数分析调度域中各组的工作量。函数返回最繁忙的组的sched_group描述符,假设这个组不包括本地CPU,在这种情况下,函数还返回为了恢复平衡而被迁移到本地运行队列中的进程数。另一方面,如果最繁忙的组包括本地CPU或所有的组本来就是平衡的,函数返回NULL。这个过程不是微不足到的,因为函数试图过滤掉统计工作量中的波动。
3.如果find_busiest_group( )在调度域中没有找到既不包括本地CPU又非常繁忙的组,就释放this_rq->lock 自旋锁,调整调度域描述符的参数,以延迟本地CPU 上下一次对load_balance( )的调度,然后函数终止。
4.调用find_busiest_queue( )函数查找在第2步中找到的组中最繁忙的CPU,函数返回相应运行队列的描述符地址busiest。
5.获取另一个自旋锁,也就是busiest->lock自旋锁。为了避免死锁,这一操作必须非常小心:首先释放this_rq->lock,然后通过增加CPU下标获得这两个锁。
6.调用move_tasks( ) 函数,尝试从最繁忙的运行队列中把一些进程迁移到本地运行队列this_rq中(见下一节)。
7.如果函数move_task( )没有成功地把某些进程迁移到本地运行队列,调度域还是不平衡。把busiest->active_balance标志设置为1,并唤醒migration内核线程,它的描述符存放在busiest->migration_thread中。Migration内核线程顺着调度域的链搜索-从最繁忙运行队列
的基本域到最上层域,寻找空闲CPU。如果找到一个空闲CPU,该内核线程就调用move_tasks( )把一个进程迁移到空闲运行队列。
8.释放busiest->lock 和this_rq->lock自旋锁
9.结束
move_tasks( )函数把进程从源运行队列迁移到本地运行队列。它接受6个参数:this_rq 和this_cpu(本地运行队列描述符和本地CPU 下标)、busiest(源运行队列描述符)、max_nr_move(被迁移进程的最大数)、sd(在其中执行平衡操作的调度域的描述符地址)以及idle标志(除了可以被设置为SCHED_IDLE 和NOT_IDLE,在函数被idle_balance( )间接调用时,该标志还可以被设置为NEWLY_IDLE。见前面“schedule( )函数”博文)。
static int move_tasks(struct rq *this_rq, int this_cpu, struct rq *busiest,
unsigned long max_nr_move, unsigned long max_load_move,
struct sched_domain *sd, enum idle_type idle,
int *all_pinned)
{
int idx, pulled = 0, pinned = 0, this_best_prio, best_prio,
best_prio_seen, skip_for_load;
struct prio_array *array, *dst_array;
struct list_head *head, *curr;
struct task_struct *tmp;
long rem_load_move;
if (max_nr_move == 0 || max_load_move == 0)
goto out;
rem_load_move = max_load_move;
pinned = 1;
this_best_prio = rq_best_prio(this_rq);
best_prio = rq_best_prio(busiest);
/*
* Enable handling of the case where there is more than one task
* with the best priority. If the current running task is one
* of those with prio==best_prio we know it won't be moved
* and therefore it's safe to override the skip (based on load) of
* any task we find with that prio.
*/
best_prio_seen = best_prio == busiest->curr->prio;
/*
* We first consider expired tasks. Those will likely not be
* executed in the near future, and they are most likely to
* be cache-cold, thus switching CPUs has the least effect
* on them.
*/
if (busiest->expired->nr_active) {
array = busiest->expired;
dst_array = this_rq->expired;
} else {
array = busiest->active;
dst_array = this_rq->active;
}
new_array:
/* Start searching at priority 0: */
idx = 0;
skip_bitmap:
if (!idx)
idx = sched_find_first_bit(array->bitmap);
else
idx = find_next_bit(array->bitmap, MAX_PRIO, idx);
if (idx >= MAX_PRIO) {
if (array == busiest->expired && busiest->active->nr_active) {
array = busiest->active;
dst_array = this_rq->active;
goto new_array;
}
goto out;
}
head = array->queue + idx;
curr = head->prev;
skip_queue:
tmp = list_entry(curr, struct task_struct, run_list);
curr = curr->prev;
/*
* To help distribute high priority tasks accross CPUs we don't
* skip a task if it will be the highest priority task (i.e. smallest
* prio value) on its new queue regardless of its load weight
*/
skip_for_load = tmp->load_weight > rem_load_move;
if (skip_for_load && idx < this_best_prio)
skip_for_load = !best_prio_seen && idx == best_prio;
if (skip_for_load ||
!can_migrate_task(tmp, busiest, this_cpu, sd, idle, &pinned)) {
best_prio_seen |= idx == best_prio;
if (curr != head)
goto skip_queue;
idx++;
goto skip_bitmap;
}
#ifdef CONFIG_SCHEDSTATS
if (task_hot(tmp, busiest->timestamp_last_tick, sd))
schedstat_inc(sd, lb_hot_gained[idle]);
#endif
pull_task(busiest, array, tmp, this_rq, dst_array, this_cpu);
pulled++;
rem_load_move -= tmp->load_weight;
/*
* We only want to steal up to the prescribed number of tasks
* and the prescribed amount of weighted load.
*/
if (pulled < max_nr_move && rem_load_move > 0) {
if (idx < this_best_prio)
this_best_prio = idx;
if (curr != head)
goto skip_queue;
idx++;
goto skip_bitmap;
}
out:
/*
* Right now, this is the only place pull_task() is called,
* so we can safely collect pull_task() stats here rather than
* inside pull_task().
*/
schedstat_add(sd, lb_gained[idle], pulled);
if (all_pinned)
*all_pinned = pinned;
return pulled;
}
函数首先分析busiest运行队列的过期进程,从优先权高的进程开始。当扫描完所有过期进程后, 函数扫描busiest 运行队列的活动进程。函数对所有的后选进程调用can_migrate_task( ),如果下列条件都满足can_migrate_task( )返回1:
* 进程当前没有在远程CPU上执行
* 本地CPU包含在进程描述符的cpus_allowed位图中
* 至少满足下列条件之一:
* 本地CPU空闲。如果内核支持超线程技术,所有本地物理芯片中的逻辑CPU必须空闲。
* 内核在平衡调度域时因反复进行进程迁移都不成功而陷入困境。
* 被迁移的进程不是"高速缓存命中"的(最近不曾在远程CPU上执行,因此可以 设想远程CPU上的硬件高速缓存中没有该进程的数据)。
如果can_migrate_task( )返回1,move_tasks( )就调用pull_task( )函数把后选进程迁移到本地运行队列中。实际上,pull_task( )执行dequeue_task( )从远程运行队列删除进程,然后执行enqueue_task( )把进程插入本地运行队列,最后,如果刚被迁移的进程比当前进程拥有更高的动态优先权,就调用resched_task( )抢占本地CPU的当前进程。