7.4 多核启动过程
上面几节主要讨论了从处理器核初始化、总线初始化、外设初始化到操作系统加载的启动过程。启动过程中多处理器核间的相互配合将在本节进行讨论。
实现不同处理器核之间相互同步与通信的一种手段是核间中断与通信信箱机制。在龙芯3号处理器中,为每个处理器核实现了一组核间通信寄存器,包括一组中断寄存器和一组信箱寄存器。这组核间通信寄存器也属于IO寄存器的一种。实际上,信箱寄存器完全可以通过在内存中分配一块地址空间实现,这样CPU访问延迟更短。而专门使用寄存器实现的信箱寄存器更多是为了在内存还没有初始化前就让不同的核间能够有效通信。
7.4.1 初始化时的多核协同
在BIOS启动过程中,为了简化处理流程,实际上并没有用到中断寄存器,对于各种外设也没有使用中断机制,都是依靠处理器的轮询来实现与其他设备的协同工作。
为了简化多核计算机系统的启动过程,我们将多核处理器中的一个核定为主核,其他核定为从核。主核除了对本处理器核进行初始化之外,还要负责对各种总线及外设进行初始化;而从核只需要对本处理器核的私有部件进行初始化,之后在启动过程中就可以保持空闲状态,直至进入操作系统再由主核进行调度。
从核需要初始化的工作包括哪些部分呢?首先是从核私有的部分。所谓私有,就是其他处理器核无法直接操纵的部件,例如核内的私有寄存器、TLB、私有Cache等,这些器件只能由每个核自己进行初始化而无法由其他核代为进行。其次还有为了加速整个启动过程,由主核分配给从核做的工作,例如当共享Cache的初始化操作非常耗时的时候,可以将整个共享Cache分为多个部分,由不同的核负责某一块共享Cache的初始化,通过并行处理的方式进行加速。
主核的启动过程与前三节介绍的内容基本是一致的。但在一些重要的节点上则需要与从核进行同步与通信,或者说通知从核系统已经到达了某种状态。为了实现这种通知机制,可以将信箱寄存器中不同的位定义为不同的含义,一旦主核完成了某些初始化阶段,就可以给信箱寄存器写入相应的值。例如将信箱寄存器的第0位定义为“串口初始化完成”标志,第1位定义为“共享Cache初始化完成”标志,第2位定义为“内存初始化完成”标志。
在主核完成串口的初始化后,可以向自己的信箱寄存器写入0x1。从核在第一次使用串口之前需要查询主核的信箱寄存器,如果第0位为0,则等待并轮询,如果非0,则表示串口已经初始化完成,可以使用。
在主核完成了共享Cache的初始化后,向自己的信箱寄存器写入0x3。而从核在初始化自己的私有Cache之后,还不能直接跳转到Cache空间执行,必须等待信号,以确信主核已将全部的共享Cache初始化完成,然后再开始Cache执行才是安全的。
在主核完成了内存初始化后,其他核才能使用内存进行数据的读写操作。那么从核在第一次用到内存之前就必须等待表示内存初始化完成的0x7标志。
7.4.2 操作系统启动时的多核唤醒
当从核完成了自身的初始化之后,如果没有其他工作需要进行,就会跳转到一段等待唤醒的程序。在这个等待程序里,从核定时查询自己的信箱寄存器。如果为0,则表示没有得到唤醒标志。如果非0,则表示主核开始唤醒从核,此时从核还需要从其他几个信箱寄存器里得到唤醒程序的目标地址,以及执行时的参数。然后从核将跳转到目标地址开始执行。
以下为龙芯3A5000的BIOS中从核等待唤醒的相关代码。
slave_main: dli t2, NODE0_CORE0_BUF0 #NODE0_CORE0_BUF0为0号核的信箱寄存器地址,其他核的 dli t3, BOOTCORE_ID #信箱寄存器地址与之相关,在此根据主核的核号,确定主核信 sll.d t3, 8 #箱寄存器的实际地址 or t2, t2, t3 wait_scache_allover: ld.w t4, t2, FN_OFF #等待主核写入初始化完成标志 dli t5, SYSTEM_INIT_OK bne t4, t5, wait_scache_allover bl clear_mailbox #对每个核各自的信箱寄存器进行初始化 waitforinit: li a0, 0x1000 idle1000: addiu a0, -1 bnez a0, idle1000 ld.w t2, t1, FN_OFF #t1为各个核的信箱寄存器地址,轮询等待 beqz t2, waitforinit ld.d t2, t1, FN_OFF #通过读取低32位确定是否写入,再读取64位得到完整地址 ld.d sp, SP_OFF(t1) #从信箱寄存器中的其他地方取回相关启动参数 ld.d gp, GP_OFF(t1) ld.d a1, A1_OFF(t1) move ra, t2 #转至唤醒地址,开始执行 jirl zero, ra, 0x0
在操作系统中,主核在各种数据结构准备好的情况下就可以开始依次唤醒每一个从核。唤醒的过程也是串行的,主核唤醒从核之后也会进入一个等待过程,直到从核执行完毕再通知主核,再唤醒一个新的从核,如此往复,直至系统中所有的处理器核都被唤醒并交由操作系统管理。
7.4.3 核间同步与通信
操作系统启动之前,利用信箱寄存器进行了大量的多核同步与通信操作,但在操作系统启动之后,除了休眠唤醒一类的操作,却基本不会用到信箱寄存器。Linux内核中,只需要使用核间中断就可以完成各种核间的同步与通信操作。
核间中断也是利用一组IO寄存器实现的。通过将目标核的核间中断寄存器置1来产生一个中断信号,使目标核进入中断处理。中断处理的具体内容则是通过内存进行交互的。内核中为每个核维护一个队列(内存中的一个数据结构),当一个核想要中断其他核时,它将需要处理的内容加入目标核的这个队列,然后再向目标核发出核间中断(设置其核间中断寄存器)。当目标核被中断之后,开始处理其核间通信队列,如果其间还收到了更多的核间中断请求,也会一并处理。
为什么Linux内核中的核间中断处理不通过信箱寄存器进行呢?首先信箱寄存器只有一组,也就是说如果通过信箱寄存器发送通信消息,在这个消息没被处理之前,是不能有其他核再向其发出新的核间中断的。这样无疑会导致核间中断发送方的阻塞。另外,核间中断寄存器实际上是IO寄存器,前面我们提到,对于IO寄存器的访问是通过不经缓存这种严格访问序的方式进行的,相比于Cache访问方式,不经缓存读写效率极其低下,本身延迟开销很大,还可能会导致流水线的停顿。因此在实际的内核中,只有类似休眠唤醒这种特定的同步操作才会利用信箱寄存器进行,其他的同步通信操作则是利用内存传递信息,并利用核间中断寄存器产生中断的方式共同完成的。