运“芯”帷幄 —CMP的操作系统技术
2005-05-19
■ 清华大学计算机科学与技术系 董渊 林昊翔
■ 清华大学信息技术研究院 汪东升 李鹏
单芯片多处理器(CMP),特别是在一个芯片上集成了多个相同通用处理器的单芯片对称多处理器(同构CMP)的发展,是上世纪90年代以来集成电路制造工艺的进步与微处理器体系结构的发展所带来的必然发展方向。
和目前在服务器领域广泛采用的对称多处理器(Symmetric Multi-Processor, SMP)结构类似,在CMP系统中,位于同一个芯片内部所有处理器内核以平等的身份参与任务调度和中断处理,共享内存和外部设备,而且也可以共享片内的 (部分或全部)高速缓存。
CMP的结构相对简单,可以直接使用现有的处理器内核,因此开发周期与成本相对较低,结构简单带来的另一个好处是更易获得高的主频。由于多个处理器集成在 一块芯片上,且共享cache,微处理器之间的通信延迟会明显降低,有利于提高系统的整体性能。因此,CMP具有良好的发展前景和广泛的应用空间,众多著 名大学、科研机构和商业公司都展开了广泛而积极的研究。
而要想真正发挥CMP的优势,软件,特别是操作系统和编译工具等系统软件的支持至关重要,没有这些软件,CMP将处于“空转”状态。因此,每一个CMP系统都需要为其量身打造的系统软件。
CMP对操作系统提出的挑战
系统软件对于CMP广泛、深入的应用有重要的意义,这里我们讨论操作系统。操作系统是计算机系统的基本系统软件,在整个计算机系统中处于核心地位,负责控 制、管理计算机的所有软件、硬件资源,是惟一直接和硬件系统打交道的软件,是整个软件系统的基础部分,同时还为计算机用户提供良好的界面。
对于普通用户而言,操作系统是一个资源管理者,通过它提供的系统命令和界面操作等工具,以某种易于理解的方式完成系统管理功能,有效地控制各种硬件资源,组织自己的数据,完成自己的工作并和其他人共享资源。
对于程序员来讲,操作系统提供了一个与计算机硬件等价的扩展或虚拟的计算平台。操作系统提供给程序员的工具除了系统命令、界面操作之外,还有系统调用,系 统调用抽象了许多硬件细节,程序可以以某种统一的方式进行数据处理,程序员可以避开许多具体的硬件细节,提高程序开发效率,改善程序移植特性。
并行是计算机科学与技术的重要分支之一,其核心思想是通过任务的合理划分和分配,使得多个处理器可以同时执行一个或多个任务,以达到系统整体计算能力的大 幅度提升。CMP的意义在于能够提供任务并行执行的一个新思路,支持在一个芯片内的多个处理器内核之间任务的划分和分配(也就是调度),而任务的调度则需 要操作系统来完成。
CMP的发展对操作系统提出了新的挑战。首先,如何合理组织、调度任务才能最大程度地发挥CMP结构的性能?其次,如何保持操作系统的外部接口的相对稳 定?对于一般用户而言,大家希望的是平滑的过渡,一方面界面最好和以前的操作系统完全相同,另一方面以前能用的应用程序最好还能够不做任何修改就直接在 CMP的机器上直接运行,也就是说CMP对于用户来讲最好是透明的,这需要操作系统在用户界面和编程接口方面都保持不变。
如何更好地组织和调度任务以便将CMP结构的性能发挥到极致是核心的问题,这是人们对CMP最大的期望。要解决这个问题,需要软硬件共同协作,从任务调 度、中断分配、资源共享等几个方面入手,硬件方面则要求CMP系统提供全新的同步与互斥、中断分配以及CPU内核之间的中断等机制。
支持CMP操作系统的关键技术
目前国际上对于CMP的研究还处于探索阶段,相关操作系统也处在积极研究时期。研究、分析和借鉴支持CMP操作系统的关键技术对于我们认识、理解和设计用 于CMP的操作系统有着非常重要的意义,这里,我们简单介绍支持CMP操作系统的引导和初始化、调度,中断处理和同步、互斥技术。
系统引导和初始化
操作系统的引导和初始化是指从系统加电到能够在多个处理器内核之间平等地进行任务调度的过程,这一过程是建立平等调度实施的基础,对于整个系统的运行具有 重要意义。虽然说对称多处理器系统中,各处理器可以平等地并行工作,但这是建立在系统有多个可并行执行任务的基础之上,而在引导和初始化过程中,由于很多 工作只能串行地执行,所以在这个阶段处理器内核是不平等的,是有主次之分的。系统加电之后,受到硬件控制,只启动其中一个处理器,称为主CPU或者引导处 理器(Booting Processor, BP),而其他处理器,称为次CPU或者应用处理器(Application Processor, AP),则处于停机等待状态。
加电启动之后,主CPU跳转到特定的内存地址,通常映射到只读存储器,这里保存着整个计算机的引导程序bootloader,其任务是进行简单的硬件检 测、初始化环境参数、将操作系统内核装载到内存中,跳转到操作系统的起始地址并开始执行。这段引导过程完全由BP完成。
进入操作系统内核之后,BP需要进行最初的草创性工作,完成运行环境准备、各种初识状态设置、基本读写数据段清零、bootloader传递过来的各种环 境参数保存、内存栈的开辟以及栈指针、全局指针设置。前面这部分工作全部由底层汇编代码完成,之后,BP跳转到由高级语言编写的函数,开始第二个阶段 CPU本身的初始化。
在CPU初始化过程中,BP首先开始自检,收集CPU相关的指令集、存储管理、高速缓存以及协处理器等基本信息。接着为次CPU准备运行环境,同时为AP 准备一个锁,之后唤醒AP,AP转入主CPU设置好的地址,开始锁测试而进入等待状态。唤醒AP之后,BP输出自身信息之后,继续进行内存等各种资源的初 始化。
接下来的工作主要有BP进行开发板以及外部设备初始化,之后准备用于所有CPU的空闲进程,这是一个不参与调度的进程,当某个CPU没有需要执行的任务, 就转入这个进程。准备好空闲进程之后,由BP解除对AP的锁,各AP逐个启动,进行各种关于各自CPU的初始化,将自身的状态填写到适当的数据结构,最后 相继进入空闲状态。
所有的AP都完成初始化并进入空闲状态后,由BP来完成整个系统最后阶段的初始化,并执行系统的第一个进程,之后真正步入对称多处理器环境,所有的CPU进入正常、平等的调度。
调度
调度系统对于操作系统乃至整个计算机系统的整体性能有非常重要的影响,嵌入式、桌面和高端服务器系统对于调度器的要求是很不一样的。在CMP结构中调度机 制的重点在于更好地满足多处理机并行性上,核心思想是通过降低CPU间调度竞争和选择下一个运行进程的开销,以及提高系统整体负载平衡的能力,从而大幅度 提高多处理机系统的执行效率。其特性如下:
1.调度算法
在传统单CPU结构的调度系统中,所有就绪进程(状态为TASK_RUNNING)被组织到同一个双向链表之中,称为全局任务队列,调度过程中将遍历此链 表中的所有进程,调用计算每一个进程的权值,从中选择权值最大的进程投入运行。由于调度器要遍历所有就绪进程,因此选择下一个运行进程的时间复杂度是O (n)(n为就绪进程的个数)。同时,因为就绪队列是全局性的,对单CPU系统来讲只可能有一个CPU访问这个队列,而在多处理器结构中,必须通过一个全 局的自旋锁保证同一时刻只有一个CPU进行访问,这样导致系统中其他CPU的等待。如果就绪进程个数比较多,那么就绪队列就会成为一个明显的瓶颈。
在支持CMP的操作系统中,每个CPU维护一个自己的就绪进程队列,称为局部任务队列,这样大大降低了CPU间的竞争。就绪进程按时间片是否用完分为 active和expired两大类,active类包括那些时间片没用完、当前可被调度的就绪进程,expired类包括那些时间片已用完的就绪进程。 同时,每类中的进程按照其优先级的不同处于不同的优先级链表中。
调度时,active队列中非空的最高优先级链表的第一项被作为候选进程,使得选择下一个运行进程的操作可以在固定时间内完成。同时内核建立了一个位映射 数组来对应每一个优先级链表,使用标志位的方式极大降低寻找非空的链表所需要的时间。当一个进程耗尽其时间片后,内核重新计算它的优先级,并把它放置在 expired队列的相应优先级链表中。进程优先级的计算过程还可以分散到进程的执行过程中进行。这种将集中计算过程分散的做法,保证了调度器运行的时间 上限,降低了不必要的开销;同时在内存中保留更加丰富的信息的做法也加速了候选进程的定位过程。当active队列中没有可调度进程时,内核简单地对调 active和 expired队列,将原来的expired队列作为新的active队列后即可进行新一轮调度。
此调度算法选择下一个运行进程的时间复杂度是O(1),与就绪进程的个数无关,调度效率大大提高。
2.系统负载平衡
支持CMP操作系统内核的调度系统需要很好地解决进程与CPU之间的“亲和”问题。如果不考虑亲和,一个进程可能在CPU之间比较频繁地迁移,交互式进程 (或高优先级的进程)可能还会在CPU之间不断“跳跃”,这样,每一次迁移之后,都可能造成频繁的内存访问,导致整体性能下降。
支持CMP操作系统内核的调度系统尽量使得每个进程一直在固定的CPU上执行,这样可以提高Cache的命中率;但是如果某个CPU的就绪队列过长,不断 的进程切换反而造成Cache命中率的下降,而且还造成其他CPU不能充分发挥效能。无论当前CPU是繁忙或空闲,时钟中断每隔一段时间都会启动一次以平 衡负载。当然,一旦当前CPU发现自己的就绪队列为空时,也会主动进行负载平衡。
为了进行有效的负载平衡,操作系统内核根据系统结构的特点,引入调度域(struct sched_domain)的概念将全体CPU一层一层地划分成不同的区域,每个调度域中的CPU分成若干个CPU组,且满足任一CPU惟一存在于一个组 中。每个CPU属于一个基本的调度域(该域至少包括本CPU),但是CPU同时还属于一个或多个更大的调度域。CPU的多个调度域通过构成一个单向链表, 且必须满足:1.父调度域是子调度域的超集;2.每个CPU的最高层调度域必须包括系统中的全部处理器。例如在一个支持超线程的CMP系统中,每个逻辑 CPU的基本调度域包含所在传统物理CPU上的全部的逻辑CPU,基本域的每个CPU分组包含一个逻辑CPU;基本调度域的父调度域是这个系统的最高层调 度域,它包含系统中所有的逻辑CPU,该域的每个CPU分组包含一个物理CPU上的全部逻辑CPU。
对于CMP系统,每个芯片上的多个核天然地构成一层调度域。一个单芯片CMP系统和一个普通SMP系统的基本调度域的差别仅在于CMP基本调度域的CPU 组包含的对象是一个“CPU核”,而SMP基本调度域的CPU组包含的是一个传统的物理CPU。对于像支持超线程和多核SMP这样复杂的CMP系统,必须 增加一个基于芯片层面的新调度域。
进行负载平衡时,从当前CPU的基本调度域出发,遍历所有的调度域。如果某个域有一段时间没有进行过负载平衡,先寻找域中负载最大的CPU组(CPU组的 负载等于组中所有CPU的负载之和),然后寻找组中最繁忙的CPU(该CPU的负载超过本CPU至少25%),更新双方的负载记录,确定需要迁移的进程数 为源CPU负载与本CPU负载之差的一半(经过更新后的值),然后按照从 expired 队列到 active 队列、从低优先级进程到高优先级进程的顺序进行迁移,但实际上真正执行迁移的进程往往少于计划迁移的数目。
中断处理
传统的单处理器通常都采用一个外部中断控制器来解决外部设备到CPU的通信,对于多处理器系统,处理器之间也需要通过中断方式进行通信,对于CMP而言, 多个处理器之间的中断控制器(本地中断控制器,简称本地控制器)也需要和处理器一起封装到芯片内部。此外,还需要一个全局的中断控制器(简称全局控制器) 负责各个处理器内核之间的中断分配,也要设计在芯片内部。
1.中断分配
全局中断控制器担负着把来自外部设备的中断请求提交和分配给片内各CPU的任务。对于每一个中断向量,可以采用静态或动态两种不同模式之一。如果某个中断 向量是静态分配的,则控制器把这种中断请求提交给预设的一个或多个CPU;动态模式则可以发送给所有CPU,或者随机地发送给某个处理器。
本地控制器处理本处理器内部产生的中断请求、来自外部中断控制器的中断请求以及其他处理器发送过来的中断请求。
2.处理器间中断
在CMP系统中,芯片内部一个处理器常常要有目标地向系统中的其他处理器发出中断请求,这种中断被称为处理器间中断(IPI, Inter Processor Interrupt)。IPI至少应该包含以下两种:
“重新调度”中断。当前CPU可以发送该中断来指示目标CPU可能需要一次进程调度,至于目标CPU在处理完该中断以后是否进行进程调度,那得要看事先或者在处理中断的过程中是否把当前进程设置为需要调度。
“请求执行”中断。这个中断被用来请求目标CPU执行一个指定的函数。之所以要通过IPI请其他CPU执行,是因为某个函数必须由目标CPU才能完成,而 不能由别的CPU代替。比如某个处理器改变了内存中某个页面映射目录或页面映射表的内容,从而可能引起其他处理器的TLB与其不一致时,就向系统中正在使 用这个映射表的处理器发送这个中断,请它们自己执行代码,废弃各自TLB的内容。
3.时钟中断
在所有中断请求中,时钟中断扮演着特殊的重要角色,每一个处理器都有自身的时钟发生器。在系统初始化阶段,系统先设置一个外部时钟中断源供所有CPU共 享,并且以此基准测算各个CPU的运算速度,并校准自身的时钟中断发生间隔。由于各处理器设计完全一致,所有CPU都有基本相同的时钟脉冲周期,为了不让 所有处理器都在同一时刻发生时钟中断,操作系统应该使各个CPU的时钟中断在相位上相互错开,把这些中断均匀地分布在时钟中断的周期中。
同步与互斥技术
在多任务系统中,不同的任务可能需要同时访问某些共享变量和共享资源,形成了竞争的关系。这就需要系统提供同步与互斥机制,使得这些共享变量和共享资源同 一时刻只能被一个任务以独占的方式访问。传统单处理器系统中在宏观上是并行,微观上,由于只有一个处理器,同一时刻只能执行一个任务,同步和互斥的问题相 对比较容易解决,而CMP的操作系统在运行时,同一时刻有可能有多个任务执行,传统的方法有时不能满足这种特定的情况,需要引入新的机制。
同步和互斥机制需要底层硬件提供“读-修改-写”类型的访存原子操作。这样的操作能够让CPU从主存储器中读取一个值,修改之,再将修改过的值保存到存储 器的相同位置中。整个过程是一次完整的总线交易,不能被其他CPU内核的访存操作所打断。“读-修改-写”类型的原子操作有多种实现方式,最常见的包括 test_and_set,swap,load-linked/store-conditional等方式。
Test_and_set指令从主存储器中读取一个值(通常是一个字节或一个字),将它和0比较并根据比较的结果设置条件码,最后将1无条件地存放到存储 器的相应单元。一旦一条test_and_set指令开始了它的总线周期,那么任何其他CPU都不能访问主存储器。
在Intel的处理器中,CPU芯片有一个LOCK引脚,如果汇编语言的程序中在一条指令前加上前缀“LOCK”,经过汇编厚的机器代码就使CPU在执行 这条指令时将LOCK引脚拉低,将总线锁上,这时总线上的其他CPU暂时不能进行总线操作,从而保证了操作的原子性。X86系列的CPU还提供一条 xchg指令,提供了原子的交换操作,这条指令不论前面是否加LOCK前缀,都是一个原子操作。
在MIPS等RISC处理器中,对原子操作进行了简化,他们提供了一对指令(load-linked/store-conditional),一起使用这 一对指令就能执行一次原子的“读-修改-写”操作。Load-linked指令执行原子“读-修改-写”操作的前半部分,它从存储器中读取一个值,在硬件 中设置一个标志,表示正在进行原子操作,并设置读取的地址。然后使用store-conditional指令完成修改操作。这样对于相应的存储器位置来 说,从load-linked开始到store-conditional结束的整个指令序列都是以原子的方式执行的。
操作系统可以使用根据上述原理编写的自旋锁(spin-lock)。它的作用是取得一个变量(称之为锁)的访问权限,如果成功,则进行下一步操作,如对共 享变量或共享资源的访问,如果没有成功,则一直查询该锁,直到成功取得该锁的访问权限为止。自旋锁是保证同步和互斥操作的重要操作。
利用硬件提供的“读-修改-写”原子,操作系统可以完成各种同步和互斥操作,正确地解决资源的共享问题。