说明:该系类文章更多的是从从哲学视角看 操作系统 这门学科。同时也是 操作系统的学习笔记总结。因为博主 这些年主要是以研究安卓系统和 嵌入式Linux为主,因此这个系类文章也是这两个领域不可或缺的基石之一,尤其是对操作系统感兴趣的伙伴可特别关注。
在多核环境下,原来基于单核的合理设计和实践无法适应多核环境;主要包括原语的修正、调度修正、能耗管理等。
多核环境带来的最大变化是进程的同步与调度。由于进程运行在不同的CPU或执行核上,其同步就不仅仅是线程的同步,而有可能是执行核或CPU之间的同步。这时进程的调度也将涉及到将何进程分配到何CPU或执行核上。由于不同的核在内存的共享方式上有可能不同,其运行有数据共享的进程和没有数据共享的进程的效率就会有很大不同。这就需要调度策略的合理选择与执行来保证系统的整体运行效率。
在单核情况下,一次只能有一个程序运行;但在多核环境下,多个程序可以真正地同时执行。因此,多核环境下的进程同步与单核环境有很大的不同。而这里面最需要的就是原子操作;这类似于锁的实现,需要硬件的支持,而多处理器原语原子操作也需要硬件的支持。
在单核环境下,硬件提供的原子操作有:中断的启用与禁止、加载存入指令、测试与设置。而这三种操作除中断启用与禁止不工作外,其他两种在多核环境下均可使用。
对于加载存入原子操作来说,下面的操作均是原子操作:
而测试与设置则需要针对共享内存单元进行。
在多核环境下,一种硬件原子操作称为总线锁。总线锁就是将总线锁住,只有持有该锁的CPU才能使用总线。这样,由于所有CPU均需要使用共享总线来访问共享内存,而总线的锁住将使得其他CPU没有办法执行任何与共享内存有关的指令,从而保护数据的访问是排他的。除此之外,硬件提供的另一种同步原语是交换指令:以原子操作完成在寄存器和内存单元之间的内容交换。
在硬件提供的同步原语基础上,我们可以构建软件同步原语。由于多核技术相对比较新,如何实现多CPU同步尚没有统一标准,这样造成不同的操作系统实现的软件同步原语不尽相同。Windows和Linux内核里提供的一些原子的操作。
Linux内核提供的原子操作包括如下几种:
Windows内核提供的原子操作包括如下几种:
注意:目前操作系统还没有为多核环境提供锁操作,因为这种操作代价比较大。
旋锁(spin_lock)是几乎所有多核操作系统均会提供的一种CPU互斥机制,是操作系统内核用于多处理器互斥的机制。(用户程序不能使用)
旋锁通常用于保护某个全局的数据结构,这里的互斥指的是多个处理器或执行核之间的互斥,即两个处理器或核不能在物理上同时访问同一个数据结构。对于局部数据结构来说,则因为只在一个CPU下而不需要使用旋锁。
旋锁通过获取和释放两个操作来保证任何时候只有一个拥有者。旋锁的状态有两种:要么是闲置的,要么被某个CPU所拥有。因此,如果一个CPU获得一个旋锁,那么运行在该CPU上的所有的线程都可以访问该旋锁所保护的寄存器和数据结构。
使用旋锁的过程如下:
Windows下使用旋锁保护对DPC队列的访问过程,如图所示。
两个处理器A和B均需要访问全局的DPC队列(延迟过程调用)。上要用于在中断时将那些不需要高优先级执行的代码放进一个队列,等有空时再执行的机制。因此我们用旋锁来进行处理器间的互斥。对于处理器A/B来说,如果要访问全局数据,就要先获得Spin_lock,直到成功,然后才访问。
旋锁的实现也必须在硬件提供的原子操作上进行。多处理器环境下的硬件原子操作有加载与存入、测试与设置。这两种方法皆可以用来实现旋锁。只是使用测试与设置更为简单。在使用测试与设置来实现旋锁时,旋锁是一个特定的内存单元。这个特定的内存单元必须位于整个系统的共享内存里面。这是旋锁的物理载体。如果一个处理器要使用旋锁,就必须检查这个特定内存单元的值。
流程如图所示:
注意:获得和释放旋锁的代码是用汇编语言写的。如果使用高级语言,有的动作就无法执行,即使能够执行,也很可能速度缓慢。而且体系结构的一些优点也只有汇编语言能利用。为了提高速度并且最大限度地利用底层处理器结构提供的各种锁机制,用来获取和释放旋锁的代码通常用汇编语言来写。
旋锁的缺陷:
而解决的方案就是队列旋锁。
在多核环境下使用的原语还有信号量与内核对象等。
对于多线程和多进程,多核环境和单核环境的最大不同是可以有多个线程/进程可以真正的执行。在调度目标上,单核环境下调度应该达到的目标,多核环境也应该达到,如下所示:
对于多核,需要考虑的还有负载平衡问题。
对单核有效的进程调度算法在多核上一样有效,多核多的一类算法就是线程迁移。
调度域主要针对linux下多核负载均衡问题,使得各CPU之间进行平衡,即繁忙程度类似。一个多核系统里面可以有多个调度域。所有的CPU均被映射到某个调度域。而调度域是一个层次架构,顶级调度域囊括所有的CPU,而子调度域则通常仅包括部分的CPU。
@1 负载平衡的目标:将进程均匀分配到每个CPU的就绪队列里面。一个负载均衡的系统的效能通常会优于一个负载不平衡的系统。对于Linux来说,负载平衡在调度域里面进行。
@2 负载平衡的方法有主动和被动两种:
@3 由于一个系统里面有不同的调度域,不同的调度域其繁忙程度有可能不同。因此在调度域里面进行负载平衡的情况下,也可能需要在不同调度域之间进行平衡。因此,在负载平衡时,我们需要找出最繁忙的调度域,在每个调度域里面找出最繁忙的队列,然后将任务从一个队列移动到另一个队列,或者另一个调度域。
在判断需要进行负载平衡后,就需要将一个进程从一个处理器队列移动到另一个处理器队列。而这个移动包括了整个上下文的移动,如页表、TLB、缓存等。
有时候,因为一个进程的特殊性,我们需要让它在一个特定的CPU上执行;这在非对称多处理器的多核环境下非常普遍。在以下情况下会使用钉子进程:
如果一个应用被分解为多个线程,山于多个线程需要共享许多资源,这个时候需要将这此线程尽货分配到同一个处理器核上执行,以提升缓存命中率。对于多核处理器系统的调度,目前还没有公认的或明确的标准。
对于多核计算机来说,降低能耗比在单核时更重要。通常情况下,CPU运行在正常状态,其主频时钟频率运行在最高值,此时的能耗也处于最高状态。除此之外还有C状态和P状态:
不过,在进行状态改变的时候,需要考虑到许多因素,这些因素包括:逻辑CPU之间的依赖性、不同物理CPU之间的相关性。
对于超线程结构来说:
关闭一部分执行核可以降低系统的能耗。但关闭执行核或降低其执行频率有可能对与其有关联的其他执行核产生影响。因此,为了既可以节能,又不对其他执行核产生影响,可以从连个方面来考虑:
多核系统不一定能提高计算机的性能;