arm64 中的 spin-table 和 psci 两种启动多核流程分析

http://blog.chinaunix.net/uid-25000873-id-5587284.html

spin-table启动方法

我们都知道,一个系统的启动的基本流程是先bootloader然后运行kernel。当对所有CPU上电后,那么所有的CPU都会从bootrom里面开始执行代码,为了防止并发的一些问题,有必要将除了primary cpu以外的cpu拦截下来。使boot的过程是顺序的,而不是并发的。

在 启动的过程中,bootloader中有一道栅栏,它拦住了除了cpu0外的其他cpu。cpu0直接往下运行,进行设备初始化以及运行Linux Kernel。其他cpu0则在栅栏外进入睡眠状态。cpu0在初始化smp的时候,会在cpu-release-addr里面填入一个地址并唤醒其他 cpu。这时候,在睡眠的这个cpu接受到了信号,醒来的时候先检查下cpu-release-addr这个地址里面的数据是不是不等于0。如果不等于 0,意味着主cpu填入了地址,该它上场了。它就会直接填入该地址执行。

下面我们看看arm64里面的实现,在arch/arm64/boot/dts/xxx.dts中有如下描述:
                cpu@0 {
                        device_type = "cpu";
                        compatible = "arm,armv8";
                        reg = <0x0 0x0="">;
                        enable-method = "spin-table";
                        cpu-release-addr = <0x0 0x8000fff8="">;
                };

在arch/arm64/kernel/smp_spin_table.c中有相关的处理。我们现在只关注一下,怎么给其他cpu发信号,关注一下下面函数中的几行关键代码:

static int smp_spin_table_cpu_prepare(unsigned int cpu):

        ..........

        release_addr[0] = (void *) cpu_to_le64(__pa(secondary_holding_pen));
        __flush_dcache_area(release_addr, sizeof(release_addr[0]));
        /*
         * Send an event to wake up the secondary CPU.
         */
        sev();

       ...........

简单描述下上面的行为:

第一行写入其他cpu应该跳转的地址。因为其他cpu还没有使能MMU,所以必须使用物理地址。

第二行将写内存操作,由cache刷入DDR。以使其他CPU能及时看到第一行中写入的数据。

第三行给其他CPU发信号,告诉它该起来了。

 

bootloader部分,现在以boot-wrapper-aarch64(老版本)中的代码做示例,在boot.S中有如下代码段:
        /*
         * Secondary CPUs
         */
1:      wfe
        ldr     x4, mbox
        cbz     x4, 1b
        br      x4                              // branch to the given address

这几行代码轮询检查mbox(其地址等同cpu-release-addr)中的值。当其值为0的时候继续睡眠,否则跳入该地址执行。

 

PSCI方法

是不是发现了上面所介绍的方法非常简单?是的。简直简单的不像是社区这些高智商的人写的。所以,一份更难的版本出来了。现在社区已经抛弃上面的方法,转而投向另外一种enable-method。那就是psci。

我们依旧先从kernel开始分析。先看arch/arm64/boot/dts/rtsm_ve-aemv8a.dts文件,里面cpu节点选择了psci的方法:

 big0: cpu@0 {

        ........

        enable-method = "psci";

并且有一个psci的节点:

        psci {        
                compatible = "arm,psci";
                method = "smc";
                /*
                 * Function IDs usage and compliancy with PSCI v0.2 still
                 * under discussion.  Current IDs should be considered
                 * temporary for demonstration purposes.
                 */
                cpu_suspend = <0x84000001>;
                cpu_off = <0x84000002>;
                cpu_on = <0x84000003>;
        };

在 psci中的节点详细说明请参考文档:kernel/Documentation/devicetree/bindings/arm/psci.txt。 在此仅说一下method字段。该字段有两个可选值:smc和hvc。表示调用psci功能使用什么指令。smc、hvc、svc这些指令都是由低运行级 别向更高级别请求服务的指令。通俗地说,就是和系统调用一样。调用了该指令,cpu会进入异常切入更高的权限。异常处理程序根据下面传上来的参数决定给予 什么服务。smc陷入EL3,hvc陷入EL2,svc陷入EL1。在ARMv8里面,EL3总是是secure状态,EL2是虚拟机管态,EL1是普通 的系统态。

接下来可以看看arch/arm64/kernel/psci.c里面的代码,该文 件提供了一整套的电源管理方案。其每一个方法都使用到了invoke_psci_fn函数。这个函数待会分析。我们依旧拿唤醒CPU做分析。在 psci_cpu_on函数里面实质代码就是下面两行:

        fn = psci_function_id[PSCI_FN_CPU_ON];
        err = invoke_psci_fn(fn, cpuid, entry_point, 0);

第一行fn赋值为dts中的cpu_on数字即0x84000003。然后调用了__invoke_psci_fn_smc函数。该函数正文如下:

        asm volatile(
                        __asmeq("%0", "x0")
                        __asmeq("%1", "x1")
                        __asmeq("%2", "x2")
                        __asmeq("%3", "x3")
                        "smc    #0\n"
                : "+r" (function_id)
                : "r" (arg0), "r" (arg1), "r" (arg2));

前 面几行分别检查function_id == x0、arg0 == x1、 arg1 == x2、arg2 == x3,其实就是为了确保编译器确实是按照ARMv8的ABI来放参数的。检查完参数后,就调用了smc。对于异常向量这一部分是在bootloader里 面设置的。我们依旧以boot-wrapper-aarch64(新版本:http://linux-arm.org/git?p=boot-wrapper-aarch64.git;a=summary ) 作分析。

我们重点就看psci.S这个文件。里面有异常向量表。大部分都是未实现。我们重点看这个被实现的处理函数:

psci_call64:
        ldr     x7, =PSCI_CPU_OFF
        cmp     x0, x7
        b.eq    psci_cpu_off

        ldr     x7, =PSCI_CPU_ON
        cmp     x0, x7
        b.eq    psci_cpu_on

        mov     x0, PSCI_RET_NOT_IMPL
        eret

就是用x0和PSCI_CPU_OFF和PSCI_CPU_ON相比。通过上面kernel的代码,我们已经知道了,x0里面存的是fn,即0x84000002。当然boot-wrapper-aarch64这边也要重新定义一下:

 psci.S: 12 #define PSCI_CPU_ON             0x84000002

通过fn区别出调用号,类似系统调用的系统调用号,然后执行不同的程序。

arg0(即x1)里存的是目标CPU

arg1(即x2)里存的是目标跳转地址

boot-wrapper-aarch64按照和kernel约定的好参数列表。为目标cpu设置好跳转地址,然后返回到kernel执行。下面对关键代码给出简单注释:

123 psci_cpu_on:    
124         mov     x15, x30                                       
125         mov     x14, x2                                        
126         mov     x0, x1                                         
127
128         bl      find_logical_id // 检查目标CPU是否有效。本案例中id_table里面放的是0x0、0x1、0x2、0x3
129         cmp     x0, #-1
130         b.eq    1f
131

/*

由于本工程使用了大量的宏和shell脚本生成代码。可以通过objdump看id_table的内容。如下验证上面说的存放内容。

0000000000000788 :
 788:   00000000        .word   0x00000000
 78c:   00000000        .word   0x00000000
 790:   00000001        .word   0x00000001
 794:   00000000        .word   0x00000000
 798:   00000002        .word   0x00000002
 79c:   00000000        .word   0x00000000
 7a0:   00000003        .word   0x00000003
 7a4:   00000000        .word   0x00000000

*/
132         adr     x3, branch_table    // 每个cpu的跳转地址所存放的地址是唯一的。也就是4个cpu就有4个唤醒地址。虽然给他们设置的是同一个地址。我猜测这么做的原因是考虑到异构架构的SoC。
133         add     x3, x3, x0, lsl #3                             
134         
135         ldr     x4, =ADDR_INVALID                              
136
137         ldxr    x5, [x3]                   // 检查该地址内的内容是否还没被设置。                   
138         cmp     x4, x5                                         
139         b.ne    1f                                             
140         
141         stxr    w4, x14, [x3]          //  将x14存入该地址。 x14即上面保存的x2。也就是kernel传入的target address                      
142         cbnz    w4, 1f                                         
143         
144         dsb     ishst                                          
145         sev                                   // 向其他cpu发送消息,标志已经给target cpu的branch table写好了target address了
146         
147         mov     x0, #PSCI_RET_SUCCESS                          
148         mov     x30, x15                                       
149         eret                                  // 返回到ELR的地址,即kernel中调用svc处                
150
151 1:      mov     x0, #PSCI_RET_DENIED                           
152         mov     x30, x15

你可能感兴趣的:(linux驱动)