多核原理
多核原理
多核结构与内存
以量取胜
多处理器指的是在一个体系结构上放置多个CPU, 而多核则指在同一块芯片(CPU) 上放置多个核(core) , 即执行单元。 多核和多CPU的区别是多核结构更加紧凑, 成本在同等执行单元数量的情况下更便宜、 功耗更低。 为简单起见, 人们现在将多处理器和多核结构统称为多核结构。
多核计算机的出现, 打破了单核环境下的许多操作系统设计的正确性或可靠性。 事实上, 本书前面两大部分讲述的进程和内存管理的许多策略和机制针对的均是单CPU或单核的环境。 这些策略和机制在多核环境下要么不能正确运行, 要么效率太低, 当然也有可能二者兼而有之。 因此, 为了适应多核环境所提出的新要求, 也为了更好地利用多核技术提供的新方便, 操作系统需要做出相应调整。 本章就对多核环境对操作系统的影响进行讨论。
本书先对多核的体系结构做一简单介绍, 然后讨论多核环境下的进程和内存管理。 由于文件系统基本不受多核环境的影响, 我们将不予讨论。
多核基本概念
在x86体系结构下, 多处理功能芯片经过了对称多处理器结构(SMP Architecture) 、 超线程结构(Hyper Threading) 、 多核结构(Multi-core Architecture) 和多核超线程结构(Multi-core Hy-per Threading Architecture) 的4个演变阶段。
多处理器结构
多处理器结构说简单一点就是在一条总线上挂载多个处理器。
在多CPU的情况下, 以CPU之间的关系不同又可以分为对称和非对称多处理器结构。 在对称结构下, 多个CPU的角色功能平等, 没有主从之分, 这种多CPU结构称为对称多处理器结构(Symmetric Multi-Processor Architecture, SMP) 。 在非对称多处理器结构中, 则不同CPU的角色地位不同, 有主从CPU之分。 这种多CPU结构称为非对称多处理器结构(Asymmetric Multi-Processor Architecture, AMP) 。
超线程结构
虽然在一台电脑里安装多个CPU提升了计算机的性能, 但是付出的代价是高昂的成本和巨大的功耗。 而在实际中, 基于很多原因, CPU的执行单元并没有得到充分使用。 如果CPU不能正常读取数据(总线/内存的瓶颈) , 其执行单元利用率会明显下降。 另外就是目前大多数执行线程缺乏ILP(Instruction-Level Parallelism, 指令级并发, 即多条指令同时执行) 支持。 这些都造成了目前CPU的性能没有得到全部的发挥。 因此, 英特尔公司提出了超线程技术来让一个CPU同时执行多重线程, 从而提高CPU效率和用户满意度。
超线程技术是在一个CPU上同时执行多个程序而共享这个CPU内的资源, 理论上要像两个CPU一样在同一时间执行两个线程, 超线程技术可在同一时间里让应用程序使用芯片的不同部分。 而为了支持这种技术, 需要在处理器上多加入一个逻辑处理单元指针(Logical CPU Pointer) 。 因此新一代的P4 HT的模板的面积比以往的P4增大了5%。 而其余部分如ALU、 浮点运算单元、 二级缓存则保持不变。
每个CPU因超线程技术分解为两个逻辑CPU。 每个逻辑CPU可以执行一个线程序列。 这样一个物理CPU可以同时执行两个线程。
需要注意的是, 含有超线程技术的CPU需要芯片组和软件的支持, 才能比较理想地发挥该项技术的优势。 操作系统如Windows XP、2003以及Linux 2.4.x以后的版本均支持超线程技术。
虽然采用超线程技术能同时执行两个线程, 但它并不像两个真正的CPU那样, 每个CPU都具有独立的资源。当两个线程都同时需要某一资源时, 其中一个要暂时停止, 并让出资源, 直到这些资源闲置后才能继续。 因此超线程的性能并不等于两个CPU的性能
多核结构
多核结构就是在一个CPU里面布置两个执行核, 即两套执行单元, 如ALU、 FPU和L2缓存等。 而其他部分则两个核共享。 这样, 由于使用的是一个CPU, 其功耗和单CPU一样。 由于布置了多个核, 其指令级并行将是真正的并行, 而不是超线程结构的半并行。
多核超线程结构
在多核情况下, 我们也可以使用超线程技术, 从而形成多核超线程技术。 即每个物理执行核里面又分解为两个或多个逻辑执行单元
多核的内存结构
由于一台计算机里面有多个执行核, 而每个执行核均需要对内存进行访问。 那么这种访问在多个核之间是如何协调的呢? 或者说内存在多个核之间是如何分配的呢?
UMA
最简单的内存共享方式就是将内存作为与执行核独立的单元构建在核之外, 所有的核通过同一总线对内存进行访问。 由于每个核使用相同的方式访问内存, 其到内存的延迟也相同, 这种访问模式称为均匀内存访问(Uniform Memory Ac-cess, UMA) 。 在这种模式下, 最重要的是所有核的地位在内存面前平等。 其优点是设计简单, 实现容易。 缺点是大锅饭, 难以针对个体的程序进行访问优化, 以及扩展困难。 随着执行核数量的增加, 对共享内存的竞争将变得白热化, 从而造成系统效率急剧下降。
当前的对称多处理器共享存储系统基本上采用此种模式。 这种模式只能在处理器个数或执行核数量较少时方可使用。
NUMA结构
如果我们想构建CPU数量很多的多处理器系统, 或者欲构建执行核多于4个以上的多核系统, 则UMA结构因内存共享瓶颈而不能胜任。 在这种情况下, 一种自然的选择是使用多个分开的独立共享内存。 每个执行核或CPU到达不同共享内存的距离不同, 访问延迟也不一样。 这种访问延迟不一致的内存共享模式称为非均匀内存访问(Non-Uniform Memory Access, NUMA) 。 在这种模式下, 最重要的特点是执行核在不同的内存单元面前地位并不平等: 到近的内存具有优势地位, 到远的内存则处于劣势。
在NUMA下, 原则上应该将程序调度到离本地内存(程序存放的内存单元) 近的执行核上, 以提升程序的内存访问效率, 从而提高程序的执行效率。
NUMA结构的优点是灵活性高、 扩展容易。 在执行核的数量增加的时候, 其访问内存的效率可以保持不下降。 不过, 这种不下降的前提是优良的调度策略, 即在调度时能够将程序就近执行。 否则, 有可能因内存访问距离远而造成效率下降。 因此, NUMA对调度的要求很高。 但因为扩展容易, 所以NUMA得到了非常广泛的应用。
COMA
我们前面说过, NUMA具有灵活、 易扩展的优点, 但对调度的要求高。 如果不能或难以将程序调度到就近的执行核上, 那又怎么办呢? 答案是缓存。 即在每个执行核里面配置缓存, 其执行需要的数据均缓存在该缓存里面。 所有访问由缓存得到满足。 这样, 不论数据原来是处于哪个内存单元, 其对效率的影响均将不复存在。
这种完全由缓存满足数据访问的模式称为全缓存内存访问(Cache Only Memory Access, COMA) 。 在这种模式下, 每个执行核配备的缓存共同组成全局地址空间。
NORMA
如果内存单元为每个执行核所私有, 且每个执行核只能访问自己的私有内存, 对其他内存单元的访问通过消息传递进行, 则就是非远程内存访问模式(Non-Re-mote Memory Access, NORMA) 。 这种模式的优点是设计比NUMA还要简单, 但执行核之间的通信成本高昂。 这已经有一点像网络了。 因为效率问题, 这种模式在多核体现结构下使用甚少。
对称多处理器计算机的启动过程
计算机启动到底意味着什么? 是只要有一个CPU启动就算启动, 还是所有CPU都启动才叫启动? 多个CPU有什么启动顺序没有?
显然, 多个CPU不可能同时启动, 必定有先后次序之分, 因为我们不能让两个CPU同时执行BIOS(或EFI)里面的指令(BIOS不支持多线程) 。 因此, 除一个CPU外, 必须让其他CPU均处于中断屏蔽状态。 也就是说CPU的启动是有次序的。
问题是, 这个次序是固定的还是随机的?
对于对称多处理器结构来说, 这个顺序是固定的。 所有的CPU里面有一个被定为启动处理器(BootstrapProcessor, BSP) , 而其他的处理器则作为应用处理器(Application Processor, AP) 。 到底哪个CPU是BSP则由某一特定寄存器的值来决定。 例如, 对于英特尔公司的多处理器结构来说, 寄存器IA32_APIC_BASE_MSR里面有一个BSP标志位。 该位被设置则意味着该处理器是BSP处理器。 而在任何时候只能有一个CPU的BSP标志位被设置。 这一点可由系统总线裁决(arbitration on the system bus) 来实现。
有了BSP和AP的区别后, SMP的启动过程就比较清楚了:
1) BSP首先读取并执行BIOS(或EFI) 的初始化(boot-strap) 代码(通常处于物理地址FFFF FFF0H) 对自己进行初始化, 这种初始化包括设置APIC(Ad-vanced Programmable Interrupt Controllers, 高级可编程中断控制器) 环境, 建立全局的数据结构, 设置跳转代码(trampoline codes) 并准备AP的运行环境。
2) 而AP则在上电或重启后进行一个简单的自我设置后进入等待启动状态。
3) 然后BSP通过发送进程间中断来叫醒AP, 对AP进行启动并令其进行初始化。
4) AP在收到BSP发出的IPI启动信号后, 则将执行BIOS(或EFI) 里面的AP初始化代码或BSP准备的跳转代码。 该跳转代码将初始化AP处理器。 此后, AP再次进入等待BSP中断的状态。
5) 而BSP则继续执行BIOS或EFI的后续代码, 并负责启动操作系统。
值得一提的是, SMP的BIOS与单核或单处理器里的BIOS并不一样。 由于有多个处理器, SMP的BIOS里面包括了多个处理器的规格和信息, 每个处理器的APIC描述表。 而这些信息包括诸如CPU的数量与编号、 本地APIC信息、 I/O的APIC信息等。 由上面的启动过程也可以看出, SMP的BIOS里面不只有BSP的初始化代码,通常还包括AP的初始化代码。
多处理器之间的通信
在多CPU之间通信, 自然也可以发送信号。 不过这个信号不是内存的一个对象, 因为这样的话, 无法及时引起另外一个CPU的注意。 而要引起其注意, 需要发送的是中断。
用来协调这些CPU之间中断的机制就是高级可编程中断控制器(APIC) 。 这是实现SMP功能必不可少的,且是英特尔多处理规范的核心。 在此种规范下, 每个CPU内部必须内置APIC单元(成为那个CPU的本地APIC) 。 CPU通过彼此发送中断(IPI, 即处理器间中断) 来完成它们之间的通信。 通过给中断附加动作,不同的CPU可以在某种程度上彼此进行控制。
除了每个CPU自己本地的APIC外, 所有CPU通常还共享一个I/O APIC来处理由I/O设备引起的中断, 这个I/OAPIC是安装在主板上的。它们会收集来自I/O装置的中断信号且在当那些装置需要中断时传送信息至本机APIC。 每个I/O APIC有一个专有的中断输入(或IRQ) 号码。英特尔过去与目前的I/O APIC通常有24个输入, 其他的可能有多达64个。 而且有些机器拥有数个I/O APIC,每一个都有自己的输入号码, 加起来一台机器上会有上百个IRQ可供中断使用。
当然, 除了处理处理器间及输入输出的中断外, APIC也负责处理本地中断源发出的中断, 如本地连接的I/O设备、 时序中断、 性能监视计数器中断、 高温中断、 内部错误中断等。
SMP缓存一致性
由于在对称多处理器结构下, 每个处理器都有自己的缓存, 因此在一个系统里面存在多个缓存的情况下就有可能出现两个缓存的数据不一致的情况。 即两个CPU缓存同样的数据, 其中一个或两个CPU对数据进行了修改从而造成两个CPU缓存数据的不同。 而这有可能造成严重的后果。 因此确保SMP里面的缓存一致性十分重要。
SMP必须确保对内存地址的访问是最新的数据。 为此, 必须使用一些特定的缓存一致性策略或模型。 而缓存一致性策略在学术界得到了广泛的研究, 并存在许多现成的模型或方法。 例如, 英特尔公司的x86体系结构使用MESI模型来实现缓存的一致性。 MESI是英语4个单词的首字母缩写: Modified、 Exclusive、Shared、 Invalid。 这4个字母分别代表缓存的4个状态: 修改、 独享、 共享和无效。
多处理器、超线程和多核的比较
多处理器、 超线程和多核的共同点是均为了提升计算机性能而设计, 均可以同时执行多个指令序列。 但是区别也很明显, 主要体现在同时执行的两个线程之间共享物理资源的多少。 多处理器的共享物理资源最少, 每个线程有自己单独的处理器; 超线程共享最多, ALU、 FPU、 MSR、 缓存等均为共享物理资源; 而多核则介于两者之间, 共享处理器, 但不共享ALU、 FPU等。 具体来说, 我们有:
对于HT线程来说, 其共享的资源包括ALU、 某些MSR和缓存; 而其独享的资源有本地APIC、 通用寄存器、L1缓存、 CPUID等。
对于多核来说, 共享资源包括最后一级的缓存(如英特尔的智能缓存(Intel Smart Cache) ) 、 一少部分寄存器如MSR, 能供管理单元也有可能共享。 而独享的有CPUID、 APIC、 BIOS等。
因为共享资源数量不同, 多处理器、 超线程、 多核的成本自然也不相同。 多处理器成本最高、 独立性最高、功耗最大; 超线程的成本、 独立性和功耗最小; 而多核则处于中间。
多核环境下的进程同步与调度
多核环境下操作系统的修正
多核的出现对软件的设计产生了巨大的影响。 那些原来在单核环境下合理的设计和实践无法适应多核环境。例如, 由于多CPU或者多个执行核的存在, 我们在单核环境下的原语操作将不能适应, 而需要修正。 除此之外, 进程的调度也是一个大大需要修改的地方, 因为现在进程不只有一个可以选择的执行核可以选择了。
内存管理的变化是多核环境带来的另外一个重要变故。 此外, 能耗优化也是多核环境下必须考虑的问题, 如果一个核上没有执行程序, 是否需要让这个核停止工作?
多核环境下的进程同步与调度
多核环境带来的最大变化是进程的同步与调度。 由于进程运行在不同的CPU或执行核上, 其同步就不仅仅是线程的同步, 而有可能是执行核或CPU之间的同步。 而进程的调度也将涉及将哪个进程分配到哪个CPU或执行核上。 由于不同的核在内存的共享方式上有可能不同, 其运行有数据共享的进程和没有数据共享的进程的效率就会有很大不同。 这就需要调度策略的合理选择与执行来保证系统的整体运行效率。
多核进程同步
我们先来看多核环境下的一个具体的同步问题。 假定我们有两个处理器CPU0和CPU1, 而这两个处理器执行的程序均需要对内存地址%eax的内容进行加1的操作。
要保证上述两端程序执行的正确性, 我们需要在CPU0执行其程序时, CPU1不会执行上述程序, 或者CPU1执行的时候CPU0不会执行。 这就需要采取措施保证一段程序的执行是原子操作。 不过这里的原子操作与我们前面单核环境下的原子操作有所不同: 它必须保证跨越CPU的原子性。 即一个CPU执行时, 另一个CPU不
执行某段程序。
那么我们如何做出这种保证呢?
正如我们说过的, 所有软件原语操作均构建在硬件原子操作的基础上。下面我们看一下从处理器的架构上面提供了哪些原子操作。
硬件原子操作
在单核环境下, 我们说过, 硬件提供的原子操作有: 中断的启用与禁止、 加载存入指令、 测试与设置。 而这3种操作除中断启用与禁止不工作外, 其他两种在多核环境下均可使用。
对于加载存入原子操作来说, 下面的操作均是原子操作:
- 读写一个字节。
- 读写一个按16位对齐的16位的字。
- 读写一个按32位对齐的32位的双字。
而测试与设置则需要针对共享内存单元进行。
总线锁
在多核环境下, 还有一种硬件原子操作称为总线锁。 总线锁就是将总线锁住, 只有持有该锁的CPU才能使用总线。 这样, 由于所有CPU均需要使用共享总线来访问共享内存, 而总线的锁住将使得其他CPU没有办法执行任何与共享内存有关的指令, 从而使得数据的访问是排他的。
除此之外, 硬件提供的另外一种同步原语是交换指令, 即xchg(exchange) 。
该指令可以以原子操作完成在寄存器和内存单元之间的内容置换。 该指令的语义可由下述程序片段表示:
int cmpxchg(addr, v1, v2) {
int ret=0;
/*停止所有内存活动并忽略所有中断*/
if(*addr==v1) {*addr=v2; ret=1; }
/*重启内存活动和回应中断, 返回ret; */
}
多核环境下的软件同步原语
在硬件提供的同步原语基础上, 我们可以构建软件同步原语。 由于多核技术相对比较新, 如何实现多CPU同步尚没有统一标准, 这样造成不同的操作系统实现的软件同步原语不尽相同。 下面我们看一下Linux和Windows内核里提供的一些原子操作。
Linux内核提供的原子操作包括如下几种:
- 总线锁: 置换、 比较与置换、 原子递增操作。
- 原子算术操作: 原子读、 设置、 加、 减、 递增、 递减、 递减与测试。
- 原子位操作: 位设置、 位清除、 位测试与设置、 位测试与清除、 位测试与改变。
Windows内核提供的原子操作包括如下几种:
- 互锁操作(interlocked operation)
- 执行体互锁操作(executive interlocked operations)
这里需要注意的是, 目前操作系统还没有为多核环境提供锁操作, 因为这种操作代价比较大。 除了一些特殊的类型, 我们还可以对位操作保证原子性。 不过位运算并不是没有代价的, 如果能够不共享的话就不要共享, 这样可以提高性能。
旋锁
旋锁(spin lock) 是几乎所有多核操作系统均提供的一种CPU互斥机制, 是操作系统内核用于多处理器互斥的机制, 即用户程序不能使用旋锁进行互斥。 旋锁通常用于保护某个全局的数据结构, 如Windows里面的DPC(延迟过程调用) 队列。 这里的互斥指的是多个处理器或执行核之间的互斥, 即两个处理器或核不能
(物理上) 同时访问同一个数据结构; 而不是第8章中讲过的多线程之间的互斥。 对于局部数据结构来说,则因为只在一个CPU下而不需要使用旋锁。 例如, 设备驱动程序需要通过旋锁来保证对设备寄存器和其他全局数据结构访问的排他性, 即任何时候只能有设备驱动程序的一个部分, 从某一个处理器可以访问这些寄存器和数据结构。
旋锁通过获取和释放两个操作来保证任何时候只有一个拥有者。 旋锁的状态有两种: 要么是闲置的, 要么被某个CPU所拥有。 这里再次提醒, 旋锁的拥有者是CPU, 而不是线程。 因此, 如果一个CPU获得一个旋锁,那么运行在该CPU上的所有的线程都可以访问该旋锁所保护的寄存器和数据结构。 旋锁的作用与Win-dows API里面的mutex作用非常类似。
使用旋锁的过程如下:
1) 等待旋锁变为闲置。
2) 获得旋锁。
3) 访问寄存器和全局数据结构。
4) 释放旋锁。
21.7.1 旋锁的实现
旋锁的实现也必须在硬件提供的原子操作上进行。 我们前面说过, 多处理器环境下的硬件原子操作有加载与存入、 测试与设置。 这两种办法皆可以用来实现旋锁, 而使用测试与设置更为简单。
在使用测试与设置来实现旋锁时, 旋锁是一个特定的内存单元。 这个特定的内存单位必须位于整个系统的共享内存里面。 这是旋锁的物理载体。 如果一个处理器要使用旋锁, 就必须检查这个特定内存单元的值。 如果为0, 则将其设置为1, 表示获得该旋锁。 如果为1, 则表示该旋锁被其他处理器所占有, 则在该旋锁上进行繁忙等待, 即不停地循环, 这也是叫旋锁的缘故。
获得和释放旋锁的代码是用汇编语言写的。 如果使用高级语言, 则有的动作就无法执行, 即使能够执行, 也很可能速度缓慢。 而且体系结构的一些优点也只有汇编语言能利用。 为了提高速度并且最大限度的利用底层处理器结构提供的各种锁机制, 用来获取和释放旋锁的代码通常用汇编语言写成。
旋锁的缺点
旋锁有什么缺点呢?
很多读者可能马上注意到 旋锁的繁忙等待问题。 如果这个CPU没有获得旋锁, 就循环往复, 繁忙等待。 我们说过繁忙等待不好。 那CPU在这儿繁忙等待, 这是不是一种浪费? 本书前面提到过锁的两个缺点: 一是浪费资源, 二是可能造成优先级倒挂。虽然我们前面说过, 繁忙等待不是什么好事情, 但在旋锁的情况下, 繁忙等待并不会造成什么不良后果。 这是因为繁忙等待的主体在这里是CPU, 而不是线程。
CPU要获取旋锁是因为CPU需要使用旋锁才能继续执行, 它即使不等在旋锁上, 也干不了别的事情。 因此, 等在上面算不得什么巨大浪费。
这里需要注意的是, 竞争旋锁的主体不像线程或进程会阻碍别的线程运转。 这里的竞争主体是CPU。 而对于CPU来说, 你等待你的, 我运转我的, 并不会造成系统效率大幅度下降。
另外, 在旋锁上的繁忙等待时间通常较短。 因此持有旋锁的CPU肯定在往前推进(而不像线程持有锁的情况, 线程有可能没有推进) , 会很快释放旋锁。 切换到别的线程再回来, 反而慢了。 因此, 这种等待浪费也不是什么大不了的事情。
锁的另外一个缺点在旋锁上也不会发生。 因为旋锁由CPU持有, 不是线程持有, 因线程具有优先级关系而可能出现的优先级倒挂现象在旋锁上自然也不会出现。
那么旋锁有没有缺点呢?旋锁的问题是对总线的竞争, 而这是一个严重的问题。
旋锁的物理体现是什么? 内存单元。 这个内存单元在什么地方? 在全局内存里。 因为每个CPU在检查旋锁的状态时均需要使用系统总线来访问旋锁所在的共享全局内存单元。 如果它要测试并设置这个全局内存单元,就需要使用内存总线。 那么它不停地发信号到总线上, 会造成对内存总线的竞争。 这是效率非常低下的一种机制。 这就是旋锁的缺点。
那有什么办法解决旋锁的总线竞争问题吗? 有, 解决方案就是队列旋锁。
队列旋锁
队列旋锁的中心思想就是, 需要旋锁的CPU不要到全局内存去SPIN, 而是到自己的局部内存去SPIN。 这样就可以排除对总线的竞争。 在旋锁上排一个队, 就表示哪些CPU要这个旋锁。 释放的时候就去检查这个队列, 交给队列里的第一个CPU。
那么什么叫“交给对方”? 就是把局部变量改成释放状态。 你本来是在一个全局单元等着检查, 现在你不在全局上等, 而在局部单元上等着发生变化。 谁来做这个变化呢? 不是你自己, 而是释放旋锁的CPU来改变。 一发生改变, 你马上就知道了。
那么旋锁的队列在什么地方? 那当然在全局的地方。 别的CPU可以访问局部内存, 但是延迟时间长。 原则上不访问别的内存, 除非迫不得已。 这里就是迫不得已。 那么这也不会有总线竞争了, 因为不会把内存信号发到总线上去。 另外这还提供了先进先出的语义, 从而在某种程度上达到公平的效果, 即使用同一旋锁的CPU具有类似的性能。
使用队列旋锁的一个巨大优点是其扩展性。 由于每个CPU等在自己的本地内存单元上, 此种机制几乎可以无限扩展。
其他同步原语
在多核环境下使用的其他同步原语包括信号量、 内核对象等。 这些有的在第8章已经论述, 有些属于商业操作系统的实现, 本书就不再赘述。
多核环境下的进程调度
对于进程和线程来说, 多核环境与单核环境的最大不同是可以有多个线程或进程真正地同时执行, 而在单核情景下, 这个同时不是真正的物理同时, 而是虚拟的逻辑同时。 就是这个从逻辑同时到物理同时的变化, 使得多核环境下的调度与单核环境下的调度有所不同。
下面就从调度目标、 调度策略、 调度算法和调度手段上分别对多核环境下的调度进行讲解。
在调度目标上, 单核环境下调度应该达到的目标, 多核环境下也应该达到。 这些目标包括:
- 快速响应时间。
- 后台工作的高吞吐率。
- 防止进程饥饿。
- 协调高低优先级进程。
除此之外, 多核调度还需要考虑多核之间的负载平衡问题, 即每个核的工作量应该比较均衡。
21.9.1 调度策略
对于多个核来说, 每个CPU有着自己的就绪队列(runqueue) 。 该队列里面又可以按照不同的优先级分解为多个子队列, 就像单核环境下的情况一样。 一个进程可以排在任何一个CPU的队列上, 但也只能排在一个CPU的队列上。
显然, 对于不同优先级的进程来说, 其调度算法为优先级高的优先。 在同一优先级里面, 通常采用时间片轮转。
当然, 与单核情况下类似, 一个进程的优先级是可以随着时间的推移而变化的, 用以保证进程不会发生饥饿。 事实上, 对于一个CPU的这些队列里面的进程来说, 本书在第4章阐述过的所有进程调度算法均可用上。 这里不再赘述。
如果需要, 一个线程也可以从一个CPU上移动到另一个CPU上。
调度域
我们前面说过, 多核环境下需要考虑不同CPU之间的负载平衡问题。 比如有的CPU会很闲, 而有些CPU会很忙, 这就要平衡。 而对于Linux来说, 用于负载平衡的机制是调度域。 调度域指的是一组CPU。 而负载平衡就是在一个调度域里面的CPU之间进行平衡, 使得它们执行的进程数相同或类似, 或者它们的繁忙程度类
似。
一个多核系统里面可以有多个调度域。 所有的CPU均被映射到某个调度域。 而调度域是一个层次架构, 顶级调度域囊括所有的CPU, 而子调度域则通常仅包括部分CPU。 例如, 一个典型的调度域架构可以如下:
- CPU_domains(HT)
- Core_domains
- Phys_domains
- Node_domains
- Allnodes_domains
每个调度域设有多个标志, 这些标志用来表示该调度域的各种属性, 主要的标志如下:
●#define SD_LOAD_BALANCE 1/*对本域进行负载平衡*/
●#define SD_BALANCE_NEWIDLE 2/*在变为闲置时进行负载平衡*/
●#define SD_BALANCE_EXEC 4/*在调用程序时进行负载平衡*/
●#define SD_BALANCE_FORK 8/*在创建子进程时进行负载平衡*/
●#define SD_WAKE_IDLE 16/*任务叫醒时进入闲置状态*/
●#define SD_WAKE_AFFINE 32/*任务叫醒到醒来的CPU上*/
●#define SD_WAKE_BALANCE 64/*在叫醒时进行负载平衡*/
●#define SD_SHARE_CPUPOWER 128/*域成员共享CPU能供*/
这些标志都是Linux定义的。 每个CPU都有一个进程队列, 可以通过它计算每个CPU的负载。
负载平衡
负载平衡的目标是将进程均匀分配到每个CPU的就绪队列里面。 一个负载均衡的系统的效能通常会优于一个负载不平衡的系统。 对于Linux来说, 负载平衡在调度域里面进行。 负载平衡的方法有主动和被动两种。
主动负载平衡是队列里面进程数多的CPU将某些进程推出去(push) 。 被动负载平衡则是队列为空的CPU从别的CPU队列里面将进程拉出来(pull) 。
由于一个系统里面有不同的调度域, 不同的调度域其繁忙程度有可能不同。 因此, 在调度域里面进行负载平衡的情况下, 也可能需要在不同调度域之间进行平衡。
因此, 在负载平衡时, 我们需要找出最繁忙的调度域, 在每个调度域里面找出最繁忙的队列, 然后将任务从一个队列移动到另一个队列, 或者另一个调度域。
进程迁移
在判断需要进行负载平衡后, 就需要将一个进程从一个处理器队列移动到另外一个处理器。 而这个移动包括了整个上下文的移动, 如页表、 TLB、 缓存等
钉子进程
有时候, 因为一个进程的特殊性, 我们需要让它在一个特定的CPU上执行。 这在非对称多处理器的多核环境下非常普遍。 在这种情况下, 由于不同CPU的功能并不相同, 而一个进程可能需要某个特定CPU的功能才能执行。 即使这个CPU非常繁忙, 我们也不愿意将某个进程移动到另一个CPU上。 这时我们就将一个进程钉
(pined) 在一个CPU上。 一个被钉住的进程是不能移动的。
除了因功能不同而钉住一个进程外, 还可以因为缓存而钉住一个进程。 如果一个进程的许多信息已经缓存在一个CPU的缓存里面, 我们可能也不太愿意迁移该进程到别的CPU上。
关联线程的调度
如果一个应用被分解为多个线程, 由于多个线程需要共享许多资源, 这时需要将这些线程尽量分配到同一个处理器核上执行, 以提升缓存命中率。
最后需要指出的是, 对于多核处理器系统的调度, 目前还没有公认的或明确的标准。 因此, 各个相关的实体在此方面都是各说各话。 因此, 本书论述的调度方法只能作为一种参考。
多核环境下的能耗管理
能耗已经成为信息领域面临的一个重大问题, 近年来, 如何降低信息系统的能耗已经成为一个热门的研究课题。 对于多核计算机来说, 降低能耗方面可以比单核时更加重要。
通常情况下, CPU运行在正常状态, 其主频时钟频率运行在最高值, 此时的能耗也处于最高状态。 但如果一个CPU没有任务可执行, 我们可以令其执行一条所谓的终结(halt) 指令, 使其进入到C状态(一个耗能很低的状态) ; 如果一个CPU的工作量很少, 我们可以降低其主频频率, 使其运行在较低的速度上, 从而降低能耗。 这个低频率状态就是所谓的P状态。
不过, 在进行状态改变的时候, 需要考虑到许多因素。 这些因素包括: 逻辑CPU之间的依赖性、 不同物理CPU之间的相关性。 例如, 同属于一个物理CPU的逻辑CPU是否可以独立地改变状态? 一个CPU状态的改变是否会影响其他物理CPU的运行? 这些相关性因多核结构的不同而不同。 接下来看一下超线程和多核结构的情况。
21.10.1 超线程结构
对于超线程结构来说, C状态是独立的。 即一个逻辑CPU可以变为C状态, 而不影响其他逻辑CPU的运行。简单地说, 我们可以彻底关闭一个逻辑CPU, 而将其所用资源全部移交给其他逻辑CPU。 如果只有两个逻辑CPU, 则关闭一个逻辑CPU的结果相当于暂停多核环境。
P状态是不独立的。 因为一个物理CPU里面的逻辑CPU共享同一个时钟, 它们的P状态控制寄存器也是共享的。 因此, 一旦一个逻辑CPU进入P状态, 则另一个逻辑CPU也将进入P状态。 事实上, 所有共享P状态控制寄存器的逻辑CPU皆进入P状态。
21.10.2 多核结构
对于多核结构来说, C状态是独立的, 即一个核可以独立地进入到C状态而不影响另一个核的运转。 如果两个核同时进入C状态, 则两个核之间的共享部分也可以进入C状态。 如果双个核里面只有一个核进入C状态,则共享部分不能进入C状态。
多核之间拥有独立的P状态控制寄存器, 但是每个核之间的P状态是相互依赖的, 它们之间的依赖关系由软件协调。
21.10.3 调度上的考虑
关闭一部分执行核可以降低系统的能耗。 但关闭执行核或降低其执行频率有可能对与其有关联的其他执行核产生影响。 因此, 为了既可以节能, 又不对其他执行核产生影响, 我们可以从调度上予以考虑。 例如, 将任务分解到一个包装里面的逻辑CPU上, 使得这一个物理CPU上的所有逻辑CPU都处于忙碌状态, 而其他物理CPU保持空闲, 从而可以关闭或降低执行等级。 只有无法将任务都置于一个包装的逻辑CPU时, 才分解到其他物理CPU上。
还有就是多核的电源优化。 CPU没有执行任务的时候, 会执行一条指令, 使CPU进入不工作的状态。 但这个状态能够保留寄存器里面的数据。 例如, 在Linux操作系统下, 每个核可以有C1、 C2、 C3 3种状态。 C0为正常状态, C1、 C2为电源优化状态。 我们可以通过将不同的核设置为不同的状态而达到电源优化的目的。 比如一个可以在C3状态, 一个可以在C0状态。 即使是在C0工作状态, 也可以通过调节执行核的频率和电压来省电
21.11 讨论: 多核系统的性能
多核真的能提高计算机的性能吗? 答案是不一定。
对于多核来说, 每个核可以独立地执行任务, 而这些核之间是否可以协调和在何种程度上进行协调就决定了多核系统的性能提升到底有多少。
随着多核技术的普及, 越来越多的人拥有了自己的多核计算机。 可是很多人并没有觉得多核计算机在速度和性能上有着任何非同凡响的提高。 有的人甚至根本就没有觉得有任何效率上的变化, 有的人甚至觉得效率反而下降了。
这些人的感觉是对的吗?
我们当然知道, 感觉是靠不住的东西。 因为没有觉得效率的提升并不等于效率真的没有提升。 也许是我们的期望值太高, 在期望与现实的反差之下对效率的提高视而不见; 也许是在多核计算机上运行的软件更为复杂, 从而屏蔽了效率提高的现象; 也许我们十多年前就开始使用多核计算机了(例如, Sun的SPARC) , 因此, 对现在英特尔、 AMD等推出的多核技术不屑一顾; 也许我们对多核还有敌意, 故意诋毁多核在此方面的作用。
但是我们也知道, 感觉并不是无中生有的, 很多人说没有体会到效率的提高也不是空穴来风。 事实上, 多核真的不一定能提升软件执行效率。 对于某些特殊应用, 如嵌入式应用、 高性能计算, 多核非但不提高性能,反而降低效率。 例如, 对于嵌入式应用来说, 一家电信设备制造商将其应用从一个多处理器系统迁移到一个多核系统。 而这个迁移所带来的区别只不过是独立内存(多处理器) 到共享内存(多核) , 造成了巨大的性能下降。 而且这种下降不以应用本身的并发程度为转移(参看《IEEE Computer》 2008年11月刊《Measuring Multicore Performance》 , 第99~102页) 。
2008年, 位于新墨西哥州的美国圣地亚国家实验室(Sandia National Laborato-ries) 对多核技术进行了大规模的仿真试验, 他们对8核、 16核和32核计算机系统进行了试验。 试验结果令人失望。 结果表明, 随着核数量的增加, 多核计算机的性能很快就不再提升, 并在达到一定数量后, 性能呈下降趋势。 尤其是对于需要进行大量数据处理的应用如海量数据筛选、 安全控制系统等来说, 这种性能下降十分显著。 对于这些应用来说,核的数量增加并没有带来性能的提升。 根据圣地亚国家实验室的数据, 在核数达到8以后, 核数的增加丝毫也不会带来效率的提升(见图21-7) 。
当然, 多核技术并不是一点也不会带来性能的提升。 如果这样的话, 多核技术也不会发展到今天的广为人知的地步。 事实上, 多核技术对于那些可以自然分解为大量并发任务的应用来说是如庖丁解牛, 游刃有余。
由此可见, 多核技术是否带来益处完全取决于客户的应用。 如果是解微分方程, 请使用多核计算机, 如果是进行海量信息筛选和数据挖掘, 则最好不要使用多核计算机。
即使是可以利用多核技术的应用, 其利用程度也有赖于程序的编写方式。 只有在程序编写时就充分考虑到多核的结构特点的软件才能最大限度地从多核技术获得益处。 充分利用多核结构编写并发度高的程序却不是任何人都能够胜任的一个任务。 当然, 多核厂家或编译器的制作者可以在编译器这一层上进行操作, 将普通程序员编写的非并发程序尽可能编译为某种程度的并发代码, 从而对多核技术加以利用。
当然, 即使我们写不出并发程序, 也并不意味着多核技术就没有好处。 至少, 我们在不同的核上面运行不同的应用。 这些不同应用之间的联系最好别太多。
造成多核技术这个问题的核心是内存墙。 虽然CPU的速度不断提升, 但其访问数据的速度却没有同步提升。虽然核数不断增加, 但从这些核到计算机其余部件的链接并没有同步增加, 这样, 保持所有CPU获得充足的数据就是一个大问题。
对于多核技术来说, 现在的问题是, 在信息爆炸的年代, 数据挖掘和数据分析的应用越来越占据重要地位,如何将多核技术引导到能够为这些应用进行有意义的性能提升是摆在所有关心多核技术的人的面前的一个紧迫课题。 多核技术到底能发展到什么程度, 就让我们拭目以待吧。