2、同一物理CPU上的不同核心。现在的多核CPU大多属于这种情况,每个CPU核心都有独立执行程序的能力,而它们之间也会共享着一些cache;
3、同一NUMA结点上的CPU;
4、不同NUMA结点上的CPU;在NUMA(非一致性内存体系)中,CPU和RAM以“结点”为单位分组。当CPU访问与它同在一个结点的“本地”RAM芯片时,几乎不会有竞争,访问速度通常很快。相反的,CPU访问它所属结点之外的“远程”RAM芯片就会非常慢。
(调度域可以支持非常复杂的硬件系统,但是我们通常遇到的SMP一般是:一个物理CPU包含N个核心。这种情况下,所有CPU之间的亲缘性都是相同的,引入调度域的意义其实并不大。)
进程在两个很亲近的CPU之间迁移,代价较小,因为还有一部分cache可以继续使用;在属于同一NUMA结点上的两个CPU之间迁移,虽然cache会全部丢失,但是好歹内存访问的速度是相同的;如果进程在属于不同NUMA结点的两个CPU之间迁移,那么这个进程将在新NUMA结点的CPU上被执行,却还是要访问旧NUMA结点的内存(进程可以迁移,内存却没法迁移),速度就要慢很多了。
通过调度域的描述,内核就可以知道CPU与CPU的亲缘关系。对于关系远的CPU,尽量少在它们之间迁移进程;而对于关系近的CPU,则可以容忍较多一些的进程迁移。
对于实时进程的负载均衡,调度域的作用比较小,主要是在push_rt_task将当前run_queue中的实时进程推到其他run_queue时,如果有多个run_queue可以接收实时进程,则按照调度域的描述,选择亲缘性最高的那个CPU对应的run_queue(如果这样的CPU有多个,那么约定选择编号最小那一个)。所以,下面着重讨论普通进程的负载均衡。
首先,调度域具体是如何描述CPU之间的亲缘关系的呢?假设系统中有两个物理CPU、每个物理CPU有两个核心、每个核心又通过超线程技术虚拟出两个CPU,则调度域的结构如下:
1、一个调度域是若干CPU的集合,这些CPU都是满足一定的亲缘关系的(比如至少是属于同一NUMA结点的);
2、调度域之间存在层次关系,一个调度域可能包括多个子调度域,每个子调度域包含了父调度域的一个CPU子集,并且子调度域中的CPU满足比父调度域更严格的亲缘关系(比如父调度域中的CPU至少是属于同一NUMA结点的,子调度域中的CPU至少是属于同一物理CPU的);
3、每个CPU分别具有其对应的一组sched_domain结构,这些调度域处于不同层次,但是都包含了这个CPU;
4、每个调度域被依次划分成多个组,每个组代表调度域的一个CPU子集;
5、最低层次的调度域包含了亲缘性最近的几个CPU、而最低层次的调度组则只包含一个CPU;
对于普通进程的负载均衡来说,在一个CPU上,每次触发load_balance总是在某个sched_domain上进行的。低层次的sched_domain包含的CPU有着较高的亲缘性,将以较高的频率被触发load_balance;而高层次的sched_domain包含的CPU有着较低的亲缘性,将以较低的频率被触发load_balance。为了实现这个,sched_domain里面记录着每次load_balance的时间间隔,以及下次触发load_balance的时间。
前面讨论过,普通进程的load_balance第一步是需要找出一个最繁忙的CPU,实际上这是通过两个步骤来实现的:
1、找出sched_domain下最繁忙的一个sched_group(组内的CPU对应的run_queue的load之和最高);
2、从该sched_group下找出最繁忙的CPU;
可见,load_balance实际上是实现了对应sched_domain下的sched_group之间的平衡。较高层次的sched_domain包含了很多CPU,但是在这个sched_domain上的load_balance并不直接解决这些CPU之间的负载均衡,而只是解决sched_group之间的平衡(这又是load_balance的一大简化)。而最底层的sched_group是跟CPU一一对应的,所以最终还是实现了CPU之间的平衡。
其他问题
CPU亲和力
linux下的进程可以通过sched_setaffinity系统调用设置进程亲和力,限定进程只能在某些特定的CPU上运行。负载均衡必须考虑遵守这个限制(前面也多次提到)。
迁移线程
前面说到,在普通进程的load_balance过程中,如果负载不均衡,当前CPU会试图从最繁忙的run_queue中pull几个进程到自己的run_queue来。
3、同一NUMA结点上的CPU;
4、不同NUMA结点上的CPU;在NUMA(非一致性内存体系)中,CPU和RAM以“结点”为单位分组。当CPU访问与它同在一个结点的“本地”RAM芯片时,几乎不会有竞争,访问速度通常很快。相反的,CPU访问它所属结点之外的“远程”RAM芯片就会非常慢。
(调度域可以支持非常复杂的硬件系统,但是我们通常遇到的SMP一般是:一个物理CPU包含N个核心。这种情况下,所有CPU之间的亲缘性都是相同的,引入调度域的意义其实并不大。)
进程在两个很亲近的CPU之间迁移,代价较小,因为还有一部分cache可以继续使用;在属于同一NUMA结点上的两个CPU之间迁移,虽然cache会全部丢失,但是好歹内存访问的速度是相同的;如果进程在属于不同NUMA结点的两个CPU之间迁移,那么这个进程将在新NUMA结点的CPU上被执行,却还是要访问旧NUMA结点的内存(进程可以迁移,内存却没法迁移),速度就要慢很多了。
通过调度域的描述,内核就可以知道CPU与CPU的亲缘关系。对于关系远的CPU,尽量少在它们之间迁移进程;而对于关系近的CPU,则可以容忍较多一些的进程迁移。
对于实时进程的负载均衡,调度域的作用比较小,主要是在push_rt_task将当前run_queue中的实时进程推到其他run_queue时,如果有多个run_queue可以接收实时进程,则按照调度域的描述,选择亲缘性最高的那个CPU对应的run_queue(如果这样的CPU有多个,那么约定选择编号最小那一个)。所以,下面着重讨论普通进程的负载均衡。
首先,调度域具体是如何描述CPU之间的亲缘关系的呢?假设系统中有两个物理CPU、每个物理CPU有两个核心、每个核心又通过超线程技术虚拟出两个CPU,则调度域的结构如下:
1、一个调度域是若干CPU的集合,这些CPU都是满足一定的亲缘关系的(比如至少是属于同一NUMA结点的);
2、调度域之间存在层次关系,一个调度域可能包括多个子调度域,每个子调度域包含了父调度域的一个CPU子集,并且子调度域中的CPU满足比父调度域更严格的亲缘关系(比如父调度域中的CPU至少是属于同一NUMA结点的,子调度域中的CPU至少是属于同一物理CPU的);
3、每个CPU分别具有其对应的一组sched_domain结构,这些调度域处于不同层次,但是都包含了这个CPU;
4、每个调度域被依次划分成多个组,每个组代表调度域的一个CPU子集;
5、最低层次的调度域包含了亲缘性最近的几个CPU、而最低层次的调度组则只包含一个CPU;
对于普通进程的负载均衡来说,在一个CPU上,每次触发load_balance总是在某个sched_domain上进行的。低层次的sched_domain包含的CPU有着较高的亲缘性,将以较高的频率被触发load_balance;而高层次的sched_domain包含的CPU有着较低的亲缘性,将以较低的频率被触发load_balance。为了实现这个,sched_domain里面记录着每次load_balance的时间间隔,以及下次触发load_balance的时间。
前面讨论过,普通进程的load_balance第一步是需要找出一个最繁忙的CPU,实际上这是通过两个步骤来实现的:
1、找出sched_domain下最繁忙的一个sched_group(组内的CPU对应的run_queue的load之和最高);
2、从该sched_group下找出最繁忙的CPU;
可见,load_balance实际上是实现了对应sched_domain下的sched_group之间的平衡。较高层次的sched_domain包含了很多CPU,但是在这个sched_domain上的load_balance并不直接解决这些CPU之间的负载均衡,而只是解决sched_group之间的平衡(这又是load_balance的一大简化)。而最底层的sched_group是跟CPU一一对应的,所以最终还是实现了CPU之间的平衡。
其他问题
CPU亲和力
linux下的进程可以通过sched_setaffinity系统调用设置进程亲和力,限定进程只能在某些特定的CPU上运行。负载均衡必须考虑遵守这个限制(前面也多次提到)。
迁移线程
前面说到,在普通进程的load_balance过程中,如果负载不均衡,当前CPU会试图从最繁忙的run_queue中pull几个进程到自己的run_queue来。