0.约定
为了便于理解,根据最新版的《龙芯3A处理器用户手册》约定以下术语:
处理器节点:包含4个GS464核的1个3A处理器称为1个处理器节点
处理器核: 3A中的每个GS464核就是一个处理器核
主处理器核:4个GS464中最先进入内核并负责初始化外设和引导从处理器核的处理器核
从处理器核:被主处理器核引导的处理器核
1.BIOS的引导
操作系统一般都是要由BIOS启动。龙芯的处理器芯片同样要先进入相应的BIOS也即PMON。在BIOS这一层面上的多核系统中,主处理器核的启动和从处理器核的启动有所不同。主处理器核的启动流程,跟单核系统的启动过程基本一样,主要完成处理器核、TLB、MMU及相关外设的启动。而从处理器则根据自身的Id号码,执行各自的轮询循环,直到某个寄存器值已经改变,跳到各自的内核代码人口。
系统重启后,所有的的处理器核都会进入pmon中位于boot.S中的reset_exception执行,代码如下:
reset_exception:
22 .set mips64
23 mfc0 t0, $15, 1 #mfc0, t0, CP0_PRID, 1
24 andi t0, 0x3ff #clear bit [31]~bit[12]
25 .set mips3
26 1:
27 bnez t0, slave_main #slave cpu
28 nop
代码中23行的数字1表示是mips64 中的EBase Rigister selector 1寄存器,该寄存器的格式如下:
上面CPUNum字段的值在处理器出厂时由硬件固定。在单核系统中,CPUNum始终为0;在多核系统中,该字段用来区别不同的处理器核。代码中的23~24行读出当前处理器核(执行到这段代码的处理器核)的处理器号到t0中,如果t0为0则表示当前的是主处理器核,接着往下执行;否则表示当前是从处理器核,则调用slave_main执行。
1.1主处理器核的引导
先看主处理器核的启动过程,它会接着执行:
32 li t0, 0xbfe00100 #it meansGODSON_CONFREG_ADDR
33 lb t2, 0x02(t0)
34 srl t2, t2, 5
35 li t3, 0x3
36 and t2, t2, t3
37 li t1, 0x0483 #'b (0) (0_0100)_(1) (000_00) (11) liocfg
38 addiu t3, $0, 0
39 1:
40 addiu t3, t3, 2 # 00->1 01->3 10->5 11->7 +1 (60ns+1cyc)
41 bnez t2, 1b
42 addiu t2, t2, -1
43
44 sll t3, t3, 2
45 or t1, t3, t1
46 1: sw t1, 0x8(t0) # speed up localio
47 */
48 beq $0, $0, cp0_main
49 addu k0, $0, $0
它首先初始化cpi0的配置寄存器、时钟频率、localio寄存器,然后跳转到cp0_main,去执行主处理器核的初始化工作,包括设置中断、初始化DDR控制寄存器、LPC、SCache、TLB等,完成这些准备工作之后,执行如下的代码进入内核。
270 ## v0 -- kernel entry
271 dli v0, KERNEL_ENTRY # define inlinux.h 0xffffffff80314000
272 subu sp, 32
273 sd $0, 24(sp)
274 sd $0, 16(sp)
275 sd $0, 8(sp)
276 sd $0, 0(sp)
277 move a0, zero
278 move v1, sp
279 addiu a1, v1, 24
280 addiu a2, v1, 16
281 addiu a3, v1, 8
282 jr v0
在上面的这段代码中,v0指向代码入口,它的值是KERNEL_ENTRY,也就是编译支持多核的linux 内核的入口地址,通过反汇编vmlinux.32可以看到:
540137 ffffffff80314000 <kernel_entry>:
540138 _sinittext():
540139 ffffffff80314000: 400c6000 mfc0 $t4,$12
540140 ffffffff80314004: 3c015000 lui $at,0x5000
540141 ffffffff80314008: 3421009f ori $at,$at,0x9f
540142 ffffffff8031400c: 01816025 or $t4,$t4,$at
540143 ffffffff80314010: 398c001f xori $t4,$t4,0x1f
..................................
1.2从处理器核的引导
完成上面这些步骤后,主处理器核在pmon中的工作完成了,而从处理器核则从slave_main的代码开始,代码如下:
296 mfc0 t0, CP0_CONFIG
297 ori t0, t0, 7 # set bit[0~2] to be '1'
298 xori t0, t0, 4 # set bit[2] to be '0' 011 K0=11b means cached kseg0,
299 mtc0 t0, CP0_CONFIG
300
301 la t0, next
302 dli t1, 0xfffff
303 and t0, t1 # set bit[64~32] to be'0'
304 dli t1, 0xffffffff9fc00000 #set virtualbit address of 'next',why plus 0x9fc0?
305 or t0, t1
306
307 jr t0
308 nop
首先还是从初始化从处理核的配置寄存器,设置处理器缓存一致性协议和KSEG0段访问的缓存行为,接着取得标号为”next”的代码的虚拟地址,然后跳到该地址去。标号为next处的代码首先设置KSEG0段可以缓存,然后初始化MMU和TLB相关的寄存器,完了之后执行如下的代码:
366 mfc0 t2, $15, 1 # mfc0, t0, CP0_PRID,1
367 andi t2, 0x3ff
368 .set mips3
375 dli t0, 0x900000003ff01000 # IPI_Status Base
376 andi t3, t2, 0x3 #local cpuid
377 sll t3, 8
378 or t0, t0, t3
379
380 andi t4, t2, 0xc #node id
381 dsll t4, 42
382 or t0, t0, t4
执行366/367/375行可以得到当前从处理器核的id号,该id号码的范围是0~3(对单个节点的3A处理器而言),375~378行实现把当前的id左移8位然后加上 0x900000003ff01000的值,这样刚好得到当前从处理器核对应的IPI_Status的地址,该地址保存在t0中。接着执行:
383waitforinit:
384 li a0, 0x1000
385idle1000:
386 addiu a0, -1
387 bnez a0, idle1000
388 nop
389
390 lw v0, FN_OFF(t0) #FN_OFF = 0x020,from mailbox
391 beqz v0, waitforinit #v0 is zero orpointer to a function
392 nop
383~388行执行一段循环等待,然后检查自己对应的邮箱里的值是否为0,如果是则跳到waitforint继续等待,否则接着往下这行。硬件重启后邮箱里面的值会自动为0,boot.S代码里面并没有哪个去设置它,那它什么时候变为非0的呢?前面提到主处理器核完成pmon里面的任务之后进入内核,它会在完成相关的初始化并及从处理器核的启动准备后,再回头给各个从处理器核的Mail_Box寄存器发送一个函数的地址,告诉它们不用等待而可以跳到这个函数去执行。具体的过程在内核的启动过程中会详细描述。从处理器跳到那个函数入口的过程如下:
394 dli t1, 0xffffffff00000000
395 or v0, t1
396
397 dli t1, 0x9800000000000000
398 lw sp, SP_OFF(t0)
399 or sp, t1
400 lw gp, GP_OFF(t0)
401 or gp, t1
402 lw a1, A1_OFF(t0)
403
404 jalr v0 #byebye
405 nop
...............
首先394~395把v0里的32位函数地址扩展到64位地址的扩展,然后接着设置堆栈sp和gp的指针(位什么要这样设置呢?),完成这些操作之后执行跳转指令jalr v0跳到那个函数。这样从处理器核就从bios/pmon中进入了kernel。
2.Kernel的启动
多核系统的Kernel启动过程,同样可以分为主处理器核的启动和从处理器核的启动这两个阶段,因此可以按照先后顺序分别描述。
2.1主处理核的启动
主处理器核从kernel_entry开始执行,完成cpu自身的初始化及MMU/TLB和外设的初始化,之后的初始化过程中会调用__smp_init这一函数。在单核的系统引导过称中并没有这一过程,因此这是单核和多核引导的主要区别。该函数的内容如下:
355 /*Called by boot processor to activate the rest. */
356 staticvoid __init smp_init(void)
357 {
......................
358 for_each_present_cpu(i) {
363 if (num_online_cpus() >= max_cpus)
364 break;
365 if (!cpu_online(i)) //TODO tmp for init
366 cpu_up(i);
367 }
368
369 /* Any cleanup work */
370 printk(KERN_INFO "Brought up %ldCPUs\n", (long)num_online_cpus());
371 smp_cpus_done(max_cpus);
372 printk(KERN_INFO "smp_init:all_cpus_done \n");
.................
上面主要的工作在for_each_present_cpu(i)循环中进行,366~367行检查当前要初始化的处理器核Id号码是否越界,没有的话继续判断ID号为i的处理器核是否已经初始化,如果没有则调用cpu_up(i)把它初始化。cpu_up代码的主要部分如下:
200 ret =blocking_notifier_call_chain(&cpu_chain, CPU_UP_PREPARE, hcpu);
201 if (ret == NOTIFY_BAD) {
202 printk("%s: attempt to bring upCPU %u failed\n",
203 __FUNCTION__, cpu);
204 ret = -EINVAL;
205 goto out_notify;
206 }
207
208 /* Arch-specific enabling code. */
209 mutex_lock(&cpu_bitmask_lock);
210 ret = __cpu_up(cpu);
211 mutex_unlock(&cpu_bitmask_lock);
212 if (ret != 0)
213 goto out_notify;
214 BUG_ON(!cpu_online(cpu));
215
216 /* Now call notifier in preparation. */
217 prom_printf("CPU_ONLINE(%d)\n",cpu);
218 blocking_notifier_call_chain(&cpu_chain, CPU_ONLINE, hcpu);
...............
首先会调用blocking_notifier_call_chain(&cpu_chain,CPU_UP_PREPARE, hcpu),CPU_UP_PREPAR最终会传递给kernel/softirq.c文件中cpu_callback函数的参数,它为从CPU生成ksoftirqd等线程。而218行的blocking_notifier_call_chain(&cpu_chain,CPU_ONLINE, hcpu)的作用是通过CPU_ONLINE 标志,唤醒从处理器核上的ksoftirqd线程。为了避免外设中断的影响,在启动从处理器核之前需要对临界代码__cpu_up(cpu)上锁,启动完成之后需要解锁。关键代码如下所示:
276 int__devinit __cpu_up(unsigned int cpu)
277 {
278 struct task_struct *idle;
279
280 /*
281 * Processor goes to start_secondary(),sets online flag
282 * The following code is purely to make sure
283 * Linux can schedule processes on thisslave.
284 */
285 idle = fork_idle(cpu);
286 if (IS_ERR(idle))
287 panic(KERN_ERR "Fork failed forCPU %d", cpu);
288
289 prom_boot_secondary(cpu, idle);
...............
第285行首先调用fork_idle为ID为参数cpu的处理器核创建一个空闲的idle的任务结构,以备从处理器启动后执行。该函数首先调用 copy_process()在主处理器核0号进程(也就是idle进程)的基础上复制一个进程,只是还没有让它运行起来。接着调用 init_idle()函数把新创建的进程跟要被初始化的从处理器核关联起来。fork_idle()执行完成之后,接着调用prom_boot_secondary():
294 voidprom_boot_secondary(int cpu, struct task_struct *idle)
295 {
296 int retval;
297
298 printk("\n BOOT CPU#%d...\n",cpu);
299 retval =godson3_cpu_start(cpu_logical_map(cpu), &smp_bootstrap,
300 __KSTK_TOS(idle),
301 (unsignedlong)task_thread_info(idle), 0);
302 if (retval != 0)
303 printk("godson3_start_cpu(%i)returned err%i \n", cpu,retval);
304 }
可以看到:主要是 godson3_cpu_start()完成从处理器核的引导工作,这个函数的定义如下:
234 intgodson3_cpu_start(int cpu, void(*fn)(void), long sp, long gp, long a1)
235 {
236 int res;
237 unsigned long long startargs[4];
238
239 startargs[0] = (long)fn;
240 startargs[1] = sp;
241 startargs[2] = gp;
242 startargs[3] = a1;
.................
251
252 godson3_raw_writeq(startargs[3],mailbox_buf[cpu]+0x18);
253 godson3_raw_writeq(startargs[2],mailbox_buf[cpu]+0x10);
254 godson3_raw_writeq(startargs[1],mailbox_buf[cpu]+0x8);
255 godson3_raw_writeq(startargs[0],mailbox_buf[cpu]+0x0);
256
257 res = 0;
258
259 return res;
260 }
根据函数的参数不难想象该函数可能是来设置从处理器核进入到内核后的地址的。事实正是如此,它通过调用godson3_raw_writeq来把从处理器核启动后执行对mailbox_buf的写的。
上面用到的 godson3_raw_writeq函数定义如下:
26 voidgodson3_raw_writeq(unsigned int action, void * addr)
27 { // the value is action
28 *((unsigned int *)addr) = action;
29 };
而mailbox_buf是一组处理器核的寄存器地址的集合,如下:
113 static void *mailbox_buf[] = { 114 (void*)(smp_core_group0_base + smp_core0_offset + BUF), 115 (void *)(smp_core_group0_base +smp_core1_offset + BUF), 116 (void*)(smp_core_group0_base + smp_core2_offset + BUF), 117 (void *)(smp_core_group0_base +smp_core3_offset + BUF),
.........
}
其中相关定义在头文件arch/mips/godson/godson3/smp.h中:
#defineBUF 0x20
3 #define smp_core_group0_base 0x900000003ff01000
9 #define smp_core0_offset 0x0
10#define smp_core1_offset 0x100
11#define smp_core2_offset 0x200
12#define smp_core3_offset 0x300
...............
所以 godson3_raw_writeq(startargs[0],mailbox_buf[cpu]+0x0)实际上就可以替代为:
godson3_raw_writeq(fn,0x900000003ff01020+cpu*0x100),而 0x900000003ff01020+cpu*0x100正好对应ID为cpu的处理器核的CoreN_MailBox0寄存器的物理地址,因此该函数也就是往ID为cpu的处理器核的四个Mail_Box寄存器里面写入gp、sp、al和一个函数的地址fn的。一旦fn写入到从处理器核的CoreN_MailBox0,还在pmon中循环查询的从处理器核就可以跳出循环,进入该函数执行。
2.2从处理器核的启动
从处理器核就从pmon中直接进入了内核,会都跳到smp_bootstrap执行。smp_bootstrap并非一个C函数,而是一个在arch/mips/kernel/head.S里定义的一个具有函数类型的入口标志,如下:
208NESTED(smp_bootstrap, 16, sp)
209 #ifdefCONFIG_MIPS_MT_SMTC
210 /*
211 * Read-modify-writes of Status must beatomic, and this
212 * is one case where CLI is invokedwithout EXL being
213 * necessarily set. The CLI andsetup_c0_status will
214 * in fact be redundant for all but thefirst TC of
215 * each VPE being booted.
216 */
217 DMT 10 # dmt t2 /* t0, t1 are used by CLI and setup_c0_status() */
218 jal mips_ihb
219 #endif/* CONFIG_MIPS_MT_SMTC */
220 setup_c0_status_sec
221 smp_slave_setup
222 #ifdefCONFIG_MIPS_MT_SMTC
223 andi t2, t2, VPECONTROL_TE
224 beqz t2, 2f
225 EMT # emt
226 2:
227 #endif/* CONFIG_MIPS_MT_SMTC */
228 j start_secondary
...............
在这段代码里面,主要是调用setup_c0_status_sec去初始化状态寄存器,然后跳转到start_secondary去执行。
88 asmlinkagevoid start_secondary(void)
89 {
90 unsigned int cpu;
91
92 printk("Slave CPU#%d: I'mcoming!!!!!\n", smp_processor_id());
93
94 #ifdefCONFIG_MIPS_MT_SMTC
95 /* Only do cpu_probe for first TC of CPU*/
96 if ((read_c0_tcbind() & TCBIND_CURTC) == 0)
97 #endif/* CONFIG_MIPS_MT_SMTC */
98 cpu_probe(); //检测CPU类型、处理器id、fpu类型
99 cpu_report(); //打印cpuversion
100 per_cpu_trap_init();//初始化MMU和TLB
101 prom_init_secondary();//实际没有做任何事情
102
103 /*
104 * XXX parity protection should be foldedin here when it's converted
105 * to an option instead of something basedon .cputype
106 */
108 calibrate_delay();//测试并设置时钟滴答
109 preempt_disable();//禁止可抢占
110 cpu = smp_processor_id();
111 cpu_data[cpu].udelay_val =loops_per_jiffy;
112
113 prom_smp_finish();//调用godson3_smp_finish(void)开中断
114
115 cpu_set(cpu, cpu_callin_map);//在位图中设置id为cpu的处理器核启动标志
116
117 cpu_idle();// 调用schedule();进入之前创建的idle进程
118 }
该函数中间调用的一些函数的功能可以参考注释。执行到cpu_idle()后从处理器核也完全启动了。至此,整个多处理器核的引导完成。
3.参考文献
1.MIPS TECHNOLOGIES Corp.MIPS64@Architecture For ProgrammersVoloum III: The MIPS64
@Privilged Resource Architecture.2005.1:112
2.北京中科龙芯技术服务中心有限公司.龙芯3A处理器用户手册.2009.05
3.毛德操,胡希明.Linux内核源代码分析(下).浙江大学出版社:607~642.
4.Dominic Sweetman.MIPS 处理器设计透视.赵俊良,张福新译.北京航空航天大学出版社.2005.6
转载于:https://blog.51cto.com/xiamachao/1680935