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