0.约定

为了便于理解,根据最新版的《龙芯3A处理器用户手册》约定以下术语:

处理器节点:包含4GS464核的13A处理器称为1个处理器节点

处理器核: 3A中的每个GS464核就是一个处理器核

主处理器核:4GS464中最先进入内核并负责初始化外设和引导从处理器核的处理器核

从处理器核:被主处理器核引导的处理器核

1.BIOS的引导

       操作系统一般都是要由BIOS启动。龙芯的处理器芯片同样要先进入相应的BIOS也即PMON。在BIOS这一层面上的多核系统中,主处理器核的启动和从处理器核的启动有所不同。主处理器核的启动流程,跟单核系统的启动过程基本一样,主要完成处理器核、TLBMMU及相关外设的启动。而从处理器则根据自身的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寄存器,该寄存器的格式如下:

wKiom1W9gziS-GAsAABfuHYDruc703.jpg

上面CPUNum字段的值在处理器出厂时由硬件固定。在单核系统中,CPUNum始终为0;在多核系统中,该字段用来区别不同的处理器核。代码中的23~24行读出当前处理器核(执行到这段代码的处理器核)的处理器号到t0中,如果t00则表示当前的是主处理器核,接着往下执行;否则表示当前是从处理器核,则调用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控制寄存器、LPCSCacheTLB,完成这些准备工作之后,执行如下的代码进入内核。

    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段可以缓存,然后初始化MMUTLB相关的寄存器,完了之后执行如下的代码:

    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

 

执行366367375行可以得到当前从处理器核的id号,该id号码的范围是0~3(对单个节点的3A处理器而言),375378行实现把当前的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

 

383388行执行一段循环等待,然后检查自己对应的邮箱里的值是否为0,如果是则跳到waitforint继续等待,否则接着往下这行。硬件重启后邮箱里面的值会自动为0boot.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

       ...............

首先394395v0里的32位函数地址扩展到64位地址的扩展,然后接着设置堆栈spgp的指针(位什么要这样设置呢?),完成这些操作之后执行跳转指令jalr v0跳到那个函数。这样从处理器核就从biospmon中进入了kernel

2.Kernel的启动

       多核系统的Kernel启动过程,同样可以分为主处理器核的启动和从处理器核的启动这两个阶段,因此可以按照先后顺序分别描述。

2.1主处理核的启动

       主处理器核从kernel_entry开始执行,完成cpu自身的初始化及MMUTLB和外设的初始化,之后的初始化过程中会调用__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)循环中进行,366367行检查当前要初始化的处理器核Id号码是否越界,没有的话继续判断ID号为i的处理器核是否已经初始化,如果没有则调用cpu_upi)把它初始化。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_idleID为参数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正好对应IDcpu的处理器核的CoreN_MailBox0寄存器的物理地址,因此该函数也就是往IDcpu的处理器核的四个Mail_Box寄存器里面写入gpspal和一个函数的地址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类型、处理器idfpu类型

     99     cpu_report(); //打印cpuversion

    100     per_cpu_trap_init();//初始化MMUTLB

    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);//在位图中设置idcpu的处理器核启动标志

    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