本文主要分析平台相关的CPU睡眠和唤醒,即下电和上电流程,以及ARM底层汇编代码实现。
内核版本:3.1.0
CPU:ARM Cortex-A7
1 平台相关函数执行流程
上图最后调入suspend_ops->enter,这是个平台相关的函数。
平台相关的cpu suspend_enter函数:
如果有中断挂起,则直接返回;
读取设备的idle状态;
设置CPU0的热跳转寄存器,睡眠唤醒后,跳转到这个地址
允许cpu0睡眠
不屏蔽SCU断电、自断电功能
不屏蔽CPU自断电功能,当CPU进入WFI状态,CPU自动断电
改变CPU的频率、电压输出
调用cpu_suspend函数
恢复CPU的频率、电压输出
屏蔽自断电功能
屏蔽SCU断电、自断电功能
不允许CPU0睡眠
cpu_suspend函数调用流程图
2 睡眠过程详细分析
cpu_suspend:(arch/arm/kernel/suspend.c)
int cpu_suspend(unsigned long arg, int (*fn)(unsigned long))
函数携带两个参数,第二个参数是函数指针(参数是unsigned long型的,返回值是int型的),第一个参数就是前面的函数指针被调用时需要的参数,故为unsigned long型的
if (!idmap_pgd)
return -EINVAL;
调用 __cpu_suspend
__cpu_suspend:(arch/arm/kernel/sleep.S)
携带的参数就是调用cpu_suspend函数时传入的参数。
R0(unsigned long arg,实际上是个地址,地址存放的类型是suspend_args,是个参数,供R1函数调用时使用)
R1(睡眠函数,执行时需要的参数就是R0)
#define cpu_suspend_size __glue(CPU_NAME,_suspend_size) arch/arm/include/asm/glue-proc.h
#define __glue(name,fn) ____glue(name,fn) arch/arm/include/asm/glue.h
#define ____glue(name,fn) name##fn
define CPU_NAME cpu_v7
.equ cpu_v7_suspend_size, 4 * 8(arch/arm/mm/proc.v7.S)
故 cpu_suspend_size == cpu_v7_suspend_size
入栈R4-R11,LR
没有定义MULTI_CPU,R4赋值cpu_suspend_size,为32
R5就是入栈后堆栈的地址,图中的1处
R4加上12
然后将堆栈地址减去(32+12),就是让开11个寄存器的值,图中的3处
入栈R0,R1
R0赋值堆栈地址加上8,图中的3处
R1赋值R4,就是44
R2赋值R5,就是图中的1处
R3赋值临时栈地址,文件下面定义了数个(看内核配置了多少个CPU来定)unsigned long 型的地址空间
定义了多核,根据CPU的ID,获取当前CPU的临时栈地址
跳转到__cpu_suspend_save
__cpu_suspend_save:(arch/arm/kernel/suspend.c)
R3即当前CPU的临时栈地址,存入R0的值(物理的地址),图中的3处
以R0为基地址,入栈idmap_pgd的物理地址、入栈当前的堆栈(图中的1处,是个虚拟地址)、入栈唤醒函数( cpu_do_resume 的物理地址)
cpu_do_suspend(glue-proc.h),-> cpu_v7_do_suspend (arch/arm/mm/proc.v7.S)携带的参数R0就是刚入完3个寄存器后的堆栈地址
入栈R4-R10,LR;
以传入的参数R0为栈基地址,入栈R4、R5(PID、线程ID)
入栈R6-R11(域ID,页表基地址寄存器1、页表控制寄存器、系统控制寄存器、辅助寄存器、协处理器访问控制寄存器),加上上面的2个正好是8个;
弹出R4-R10,PC;
刷新cache、二级cache,保证数据确实写到了内存,栈空间也是内存的一部分
LR赋值cpu_suspend_abort
弹出r0,PC ,就是跳转到上面的fn函数指针处
若这个fn函数执行过程中返回了,则调转到lr处,就是cpu_suspend_abort
cpu_suspend_abort:
此时的SP就是图中的3处,弹出到R1,R2,R3。
判断R0的值,若不是0,则将其赋值1
堆栈SP赋值R2的值,图中的1处
堆栈弹出R4-R11,PC。实际是返回到cpu_suspend函数中,调用__cpu_suspend函数的地方。
fn函数执行过程,若最终执行成功,则执行wfi指令,CPU顺利下电
清除Icache,刷dcache。
关闭SMP位
关闭对应的CCI端口
然后进入WFI睡眠,低功耗状态
如果中间出现了差错,则直接返回1后,接着下面的cpu_suspend_abort执行,仍然能够返回到cpu_suspend函数,其中__cpu_suspend函数的返回值强行变为了1
3 唤醒过程详细分析
低功耗模式被唤醒后,跳转到唤醒地址
设置SVC模式,关闭IRQ、FIQ
使能对应的CCI端口
清除SMP位,清除跳转预测等,开启I cache
跳转到cpu_resume的物理地址
cpu_resume:
获取当前CPU的临时栈地址,保存到R0中,图中的3处
设置SVC模式,关闭I、F
以R0为基地址,弹出R1、SP、PC,R1就是 idmap_pgd
跳转到cpu_do_resume,就是cpu_v7_do_resume
cpu_v7_do_resume
清除TLB、I cache、上下文ID
再次以R0为基地址,弹出R4-R5,并恢复PID、线程ID,
弹出R6-R11,
恢复域ID
将R1的内容设置到页表基地址寄存器0,为开启MMU做准备
恢复页表基地址寄存器1,页表控制寄存器
恢复辅助寄存器,协处理器访问控制寄存器
设置内存属性间接寄存器
将系统控制寄存器R8内容赋值与R0,跳转到cpu_resume_mmu
cpu_resume_mmu:
将R0设置系统控制器寄存器,开启MMU后,跳转到下面的虚拟地址,cpu_resume_after_mmu
cpu_resume_after_mmu:
cpu_init 执行CPU的初始化
R0赋值为0,
此时的SP是虚拟的,然后弹出R4-R11,PC。接着返回到cpu_suspend函数,__cpu_suspend函数的返回值是0
cpu_switch_mm(mm->pgd, mm);
cpu_v7_switch_mm: proc-v7-2level.S
设置context ID
设置TTB,页表转换基地址 将基地址由idmap_pgd变为mm->pgd
4、idmap_pgd变量
pgd_t *idmap_pgd;
typedef pmdval_t pgd_t\[2\];
typedef u32 pmdval_t;
故 pgd_t是一个无符号32位的数组, idmap_pgd 是个指针,指向有两个变量的的数组,变量类型是无符号32位的数
init_static_idmap函数
idmap_pgd = pgd_alloc(&init_mm);赋值重新分配后的一个内存地址,4K的大小,且里面已经填充了很多东西了
idmap_start获取__idmap_text_start对应的物理地址
immap_end获取__idmap_text_end对应的物理地址
identity_mapping_add(idmap_pgd, idmap_start, idmap_end); 建立1:1的隐射,从idmap_start开始到idmap_end结束
从我编译的内核来看,是下面的这4段代码,都是跟开启、关闭MMU相关的。
c055dcb8 T __idmap_text_start
c055dcb8 T __kprobes_text_end
c055dcb8 T __kprobes_text_start
c055dcb8 T __turn_mmu_on
head.s 开启MMU
c055dcd8 t __turn_mmu_on_end
c055dcd8 Tcpu_resume_mmu sleep.s 有开启MMU的过程
c055dcfc Tcpu_v7_reset proc-v7.s 关闭MMU
c055dd40 Tcomip_mmu_off sleep.s
c055dd80 T __idmap_text_end
idmap_pgd指向内存中的4K大小的一段空间,实际是页表基地址c000 4000对应的物理地址开始的一个备份(部分的是一样的),而且还将开启、关闭MMU的代码都1:1映射了。这样在cpu_switch_mm之前,就可以启用MMU了,有页表基地址idmap_pgd,且建立了隐射。
其实CPU0在从uboot跳转到压缩内核处,解压缩完毕,跳转到arch/arm/kernel/head.s 。
__create_page_tables时,也曾经为__turn_mmu_on到__turn_mmu_on_end这一段,建立了1:1的隐射
在init_static_idmap函数的结尾打印c000 4000开始的页表地址内容,备份的页表地址idmap_pgd的内容,如下所示,只打印不是0的内容。
c000 4000开始的内容如下:
0xd7c6b570 : 1c01140e 1c11140e 1c21140e 1c31140e
0xd7c6b580 : 1c41140e 1c51140e 1c61140e 1c71140e
0xd7c6b590 : 1c81140e 1c91140e 1ca1140e 1cb1140e
0xd7c6b5a0 : 1cc1140e 1cd1140e 1ce1140e 1cf1140e
0xd7c6b5b0 : 1d01140e 1d11140e 1d21140e 1d31140e
0xd7c6b5c0 : 1d41140e 1d51140e 1d61140e 1d71140e
0xd7c6b5d0 : 1d81140e 1d91140e 1da1140e 1db1140e
0xd7c6b5e0 : 1dc1140e 1dd1140e 1de1140e 1df1140e
0xd7c6b5f0 : 1e01140e 1e11140e 1e21140e 1e31140e
0xd7c6b600 : 1e41140e 1e51140e 1e61140e 1e71140e
0xd7c6b610 : 1e81140e 1e91140e 1ea1140e 1eb1140e
0xd7c6b620 : 1ec1140e 1ed1140e 1ee1140e 1ef1140e
0xd7c6b640 : 1e023811 1e023c11 00000000 00000000
0xd7c6be00 : a0011452 a0111452 a0211452 a0311452
0xd7c6be10 : a0411452 a0511452 a0611452 a0711452
0xd7c6be20 : a0811452 a0911452 a0a11452 00000000
0xd7c6bf40 : e1011452 e1111452 00000000 00000000
0xd7c6bff0 : 00000000 00000000 1effe821 1effec21
可以看出,两者的内容除了0xd7c681a0处,两个隐射不一样外,其它内容都是相同的。
而0xd7c681a0处,减去0xd7c68000,算出来对应的虚拟地址是0680 0000开始的2M内容,而此处实际的物理地址填充的是06800402 06900402,则真好是1:1的段隐射。
在cpu_suspend函数时,先判断idmap_pgd是否为空,若不为空,则CPU在上电完成后开启MMU时,用到的页表基地址就是idmap_pgd。后面判断ret是否为0,若为0,则CPU经历了下电又上电,需要替换页表基地址。
其实多核CPU的启动,其它CPU启动,开启MMU时,用到的页表基地址也是idmap_pgd,跳转到C语言的secondary_start_kernel函数后,执行cpu_switch_mm(mm->pgd, mm),这个也是为了替换页表基地址.
原作者:wangyw