Switch_to调用机制及调用过程
在/kernel/sched/core.c中有内核调度的核心函数:__schedule,在调度函数中通过调用context_switch进行进程上下文的切换。
context_switch对函数的调用见上图所示,主要完成的工作是mm的切换和硬件上下文的切换,我们主要讨论硬件上下文切换过程。
整体的调用过程如下图:
在arch/arm/include/asm/switch_to.h中有对switch_to的定义:
在源码中可以发现,switch_to(prev,next,last)是个宏定义,真正的操作在do{}中。
根据do{}的内容可知:
首先执行了__complete_pending_tlbi()函数,根据其定义的解释便知其作用是:在armV7架构中,运行的是抢占式内核,因此在TLB维护操作时需要进行抢占,因此为了能够在转移至另一个CPU工作前将维护工作完成,需要执行dsb(ish)来进行保证。因此说,__complete_pending_tlbi()只在armV7架构中其作用。
所以switch_to的重头戏便是:
last = __switch_to(prev,task_thread_info(prev),task_thread_info(next));
真正传递参数的为prev的task_struct、prev的task_thread_info以及next的task_thread_info,返回值为last。因此下边一节进入__switch_to的汇编进行此过程的详细分析。
通过源码的查找,在两个.S文件中发现了__switch_to的定义:arch/arm/kernel/entry-armv.S以及arch/arm/kernel/entry-v7m.S,这两个汇编文件中的__switch_to函数分别对应于ARMv3/4处理器和ARMv7处理器,首先对entry-armv.S进行分析。
ARMv3/4架构Switch_to汇编代码:
首先是函数注释,通过注释我们发现对应的处理器是ARMv3和ARMv4,并且传入的三个参数为:r0 = previous task_struct,r1 = previous thread_info, r2 = next thread_info。
ENTRY、ENDPROC分别是对函数头尾的声明,.fnstart是函数开始执行的标志,这些我们在这里不讨论。主要分析被上述两块包含的内容。
通过对代码的大致分析,姑且先将其分为4部分进行分块分析:
1、Store mostregs on previous thread_info
这块代码的工作就是定为ip即r1(preciousthread_info)中#TI_CPU_SAVE的位置,并将r4-sl,fp,sp,lr这几个寄存器的值存入ip所指向的相应的位置。
(1)第一句add ip, r1, #TI_CPU_SAVE
使用了伪指令TI_CPU_SAVE,通过定位,找到了TI_CPU_SAVE的声明位置:在arch/arm/kernel/asm-offset.c中有相应的定义,具体见下图:
即TI_CPU_DOMAIN指向了相应thread_info中的cpu_context,进一步对thread_info这一结构体进行分析:
位于arch/arm/include/asm/thread_info.h中
发现:cpu_context是另一个结构体,同样定义在此头文件中:
因此此时ip便指向r1àcpu_context的地址,即相应结构体cpu_context_save的首地址。
(2)存储寄存器的值:
在源码中发现对于寄存器的存储过程有ARM和THUMB两种操作方式:
经过查阅发现在THUMB状态下,stmia命令最多store8个字,即r4-sl,fp,剩余的两个寄存器(sp、lr)需要再调用str进行store。
因此,总的来说这段代码的功能就是将r4-sl,fp,sp,lr寄存器的值存入previous thread_info的cpu_context位置,来保存上下文寄存器的值。
2、Set r4 r5 r6 r7 r8
这块源码的主要目的是对r4、r5、r6、r7、r8赋值,过程大致可以分为对r4、r5赋值,对r6赋值,对r7、r8赋值。
(1)r4、r5赋值
在这段代码中用到了另一个伪指令:TI_IP_VALUE
在同样的位置(arch/arm/kernel/asm-offset.c),我们找到了TI_IP_VALUE的声明,其同样指向结构体thread_info,具体为tp_value,观察thread_info可知,tp_value为一个长度为2的一维数组,存储的值注释为TLSregister(TLS即Thread Local Storage,可以高效的访问TLS里面存储的信息而不用一次次的调用系统调用),具体源码如下:
因此,r4、r5分别被赋予r2(nextthread_info)的tp_value[0]和tp_value[1]。
这句话是执行switch_tls,通过对switch_tls的查找,定位到:arch/arm/include/asm/tls.h
通过对其中一种情况进行分析:
即将协处理器c13的值赋予tmp2(r7),并将tp(r4)、tpuser(r5)赋予c13,进而将tmp2(r7)的值存到[base]+#TI_IP_VALUE+4即(r1àtp_value[1])。
因此,这一步将协处理c13原来的值存入r1相应位置,同时将r4、r5的值赋给协处理器c13。同时通过对switch_tls的分析知道了,r4、r5存储的值其实是TLS寄存器和用户r/w寄存器的值。
(2)r6赋值
上述代码块的功能为寄存器r6和协处理器c3数据传递过程,在这里的代码是条件编译的,因此需先判断是否定义了CONFIG_CPU_USE_DOMAINS。
经过代码的查阅分析,发现了对CONFIG_CPU_USE_DOMAINS功能的介绍:Thisoption enables or disables the use of domain switching via the set_fs()function.也就是说CONFIG_CPU_USE_DOMAINS规定了CPU是否通过set_fs()使用domain开关。
因此在使用domain时,首先执行第一条语句:mrc p15, 0, r6, c3, c0, 0即通过mrc语句将协处理器c3的内容传递给寄存器r6,也就是说协处理器c3存储的就是domain寄存器的值。
接下来就是:str r6, [r1,#TI_CPU_DOMAIN]
ldr r6, [r2, #TI_CPU_DOMAIN]
这里定义了伪指令TI_CPU_DOMAIN,跟上述伪指令一样,这条指令同样执行了thread_info结构体:
即这两句的作用就是讲r6(prev domainreg)的值赋给r1结构体cpu_domain,并且将r2(next thread_info)对应的cpu_domain传递给r6(为接下来更新domain寄存器(c3)的值做准备)。
接下来的小代码块就是执行指令:mcr p15, 0,r6, c3, c0, 0,即将r6(next domain reg)的值传递给协处理器c3。
(3)r7 r8赋值
这段代码也是条件编译的,经过查阅可知CONFIG_CC_STACKPROTECTOR是:Enable-fstack-protector buffer overflow detection;CONFIG_SMP在arm架构中的定义为:
即如果定义了栈保护并没有定义CONFIG_SMP则进行块中的三部操作。
首先是ldr r7,[r2, #TI_TASK]:这里也用到了伪指令,同样这个伪指令指向thread_info结构体:
发现task指向结构体task_struct,即主task的结构体,task_struct在/include/linux/sched.h中定义。
即此句的工作是将r2指向的主结构体指针赋给r7。
接下来执行ldr r8,=__stack_chk_guard:即将__stack_chk_guard的值赋给r8寄存器(__stack_chk_guard =0x000a0dff)。
接下来是ldr r7, [r7,#TSK_STACK_CANARY]:这里又是伪指令TSK_STACK_CANARY,通过查找发现TSK_STACK_CANARY指向task_struct的stack_canary,即
即这句指令的作用是将r7(r2的*task_struct)的值替换为r2的task_struct结构体的stack_canary。
因此这块指令的结果就是在定义了栈保护时将r7赋值为r2的task_struct结构体的stack_canary。
最后一条指令为str r7,[r8]:即将r7(r2的task_struct结构体的stack_canary)的值赋给[r8(0x000a0dff)]。
3、bl atomic_notifier_call_chain
这里做的主要工作是将r0(prevtask_struct)的值赋给r5(r0接下来将为bl atomic_notifier_call_chain传递参数)。
第二条是将r4指向r2(nextthread_info)的cpu_context结构体。
第三条是给r0赋新值(thread_notify_head),经过查阅可知,
r0指向atomic_notifier_head结构体。
第四条是给r1赋值(#THREAD_NOTIFY_SWITCH),进查找发现#THREAD_NOTIFY_SWITCH定义为数值2。
接下来就是bl atomic_notifier_call_chain,跳转到atomic_notifier_call_chain进行操作,这部分的操作将在接下来的章节进行详细讲解。
4、load regs from next thread_info
mov r0, r5是将r5(prevtask_struct)的值重新赋值给r0(作为函数返回值)。
这块代码对应我们分析的第一块代码,也就是将从r4开始指向的结构体分别赋值给r4-sl,fp,sp,pc。也就是将r2(next thread_info)中cpu_context结构体的值赋值给r4-sl,fp,sp,pc。
这里最重要的就是结构体中lr赋值给pc,因为lr存储的是此进程在上次被切换时保留的下一条指令的地址,因此pc被赋值为lr时就能够继续上次被切换时要执行的位置继续执行。
因此,经过上述4块操作,完成的基本任务为:将旧的r4-sl,fp,sp,lr赋给prev thread_info中相应的结构体进行存储,同时将next thread_info的值进行。在此过程中用到了两个协处理器c3、c13,分别用来cpu domain和TLS reg的值。
armV7架构Switch_to代码流程:
在armV7架构中,运行的是抢占式内核,因此在TLB维护操作时需要进行抢占,因此为了能够在转移至另一个CPU工作前将维护工作完成,需要执行dsb(ish)来进行保证。
接下来进行的就是__switch_to的汇编代码。
大体流程跟ARMv3/4流程类似,因此我们在这里只讨论一下这两者间的区别。
很明显这里少了条件编译,因此不再有对协处理器c3、c13的处理(cpu domain和TLS reg)。同时没有了switch_tls的过程,即没有对TLS寄存器的处理。
总的来说因为ARMv7抢占式工作流程,需要先执行__complete_pending_tlbi()dsb(ish)以确保在CPU switch进行前完成维护工作。
arm 架构switch_to流程总结
总的来说,arm架构下,硬件上下文存储在各自thread_info结构体中包含的cpu_context_save结构体中:
structcpu_context_save {
__u32 r4;
__u32 r5;
__u32 r6;
__u32 r7;
__u32 r8;
__u32 r9;
__u32 sl;
__u32 fp;
__u32 sp;
__u32 pc;
__u32 extra[2]; /*Xscale 'acc' register, etc */
};
__switch_to完成的工作就是将ARM寄存器的信息保留在相应的prev thread_infoàcpu_context_save,并将next的thread_infoàcpu_context_save信息保存在寄存器中从而完成硬件上下文的切换。