赞
踩
以下文章讲解均以arm64架构、Linux 5.10.158源码为例子。
在Linux kernel中,idle thread向其他任务线程的上下文切换,需要经历两大过程,分别是进程地址空间的切换、以及堆栈寄存器的转换。
context_switch切换过程大致如下所示。文章的这一part主要借鉴参考了这篇文章:深入理解Linux内核进程上下文切换[1],读者们可以阅读这篇文章更清楚的了解进程上下文切换的过程。
- context_switch()
- ->switch_mm_irqs_off() //上下文页表切换
- ->switch_to() //进程堆栈寄存器切换
在arm64架构中,寄存器ttbr0_el1指向非特权模式下的地址空间转换的页表基地址,指向的是初始转换表,用于较低的虚拟地址(VA)范围。寄存器ttbr1_el1指向特权模式下的地址空间转换的页表基地址,用于较高的虚拟地址范围。进程切换前后,需要改写寄存器ttbr0_el1。
- switch_mm_irqs_off()
- ->switch_mm()
- ->__switch_mm()
- ->check_and_switch_context()
- ->cpu_switch_mm()
- ->cpu_do_switch_mm()
- ->write_sysreg(ttbr1,ttbr1_el1)
- ->write_sysreg(ttbr0,ttrb0_el1)
在切换过程中,实际完成寄存器改写的是函数write_sysreg(ttbr1,ttbr1_el1)和write_sysreg(ttbr0,ttbr0_el1)。 这两个函数将ttbr1和ttbr0的值各自写入到ttbr1_el1寄存器和ttbr0_el1寄存器中。
下图代码介绍了堆栈寄存器切换过程的函数路径以及关键汇编代码。需要说明的是,语句 mov x10, #THREAD_CPU_CONTEXT,表示x10存储了thread.cpu_context成员相对于结构体struct task_struct的偏移量。struct task_struct有一个数据类型为struct thread_struct的thread成员,而thread有一个数据类型为struct cpu_context的cpu_context成员。
- switch_to()
- ->__switch_to(struct task_struct *prev, struct task_struct *next)
- ->cpu_switch_to(prev,next)
-
- SYM_FUNC_START(cpu_switch_to)
- mov x10, #THREAD_CPU_CONTEXT
- add x8, x0, x10
- mov x9, sp
- stp x19, x20, [x8], #16 // store callee-saved registers
- stp x21, x22, [x8], #16
- stp x23, x24, [x8], #16
- stp x25, x26, [x8], #16
- stp x27, x28, [x8], #16
- stp x29, x9, [x8], #16
- str lr, [x8] //x8寄存器加了6次16后,指向的是cpu_context中的pc寄存器
- add x8, x1, x10
- ldp x19, x20, [x8], #16 // restore callee-saved registers
- ldp x21, x22, [x8], #16
- ldp x23, x24, [x8], #16
- ldp x25, x26, [x8], #16
- ldp x27, x28, [x8], #16
- ldp x29, x9, [x8], #16
- ldr lr, [x8]
- mov sp, x9
- msr sp_el0, x1
- ptrauth_keys_install_kernel x1, x8, x9, x10
- scs_save x0, x8
- scs_load x1, x8
- ret
- SYM_FUNC_END(cpu_switch_to)
- NOKPROBE(cpu_switch_to)
在上述代码中,x0和x1寄存器分别存储了prev和next的结构体开头地址。add x8,x0,x10语句,使得x8寄存器存储了prev结构体中thread.cpu_context地址值。stp x19, x20, [x8], #16语句,表示将x19和x20寄存器的值存储到x8寄存器所指的地址中(即prev的thread.cpu_context成员的x19、x20),随后x8寄存器向高位移动16个字节。str lr,[x8]语句中的lr寄存器,用于存储程序返回地址。
在最后的ret汇编语句中,CPU将返回至lr所指的地址,在此处代指next线程的pc寄存器。到此为止,CPU从next线程上一次退出的地址开始运行。结合以下struct cpu_context结构体成员代码,可进一步明晰以上汇编代码的含义和意图。
- struct cpu_context {
- unsigned long x19;
- unsigned long x20;
- unsigned long x21;
- unsigned long x22;
- unsigned long x23;
- unsigned long x24;
- unsigned long x25;
- unsigned long x26;
- unsigned long x27;
- unsigned long x28;
- unsigned long fp;
- unsigned long sp;
- unsigned long pc;
- };
下图为idle thread与其他任务线程的切换过程函数路径示意图。在这幅图中,idle thread即是0号线程。蓝色箭头代表函数内完整执行函数,如if need_resched()和if (cpu_idle_force_poll || tick_check_broadcast_expired() ),这两条语句的函数都是发生在do_idle()函数中。红色箭头代表函数与函数间的关系是递进式的,如schedule_idle()、__schedule()、context_switch()的关系,schedule_idle()调用函数__schedule(),而__schedule()调用context_switch()。context_switch()同时调用switch__mm_irqs_off()和switch_to()。虚线箭头表示省略调用过程中的嵌入递进关系。
在上图中,外层循环的判断语句if need_resched() 是判断当前运行队列是否存在有需要调度的任务,注意这里是0号线程在执行这条判断语句,若need_resched()判定为真,即有其他任务线程需要切入,则CPU跳出内层循环,不执行idle state的状态。
在内层循环中的判断语句if (cpu_idle_force_poll || tick_check_broadcast_expired()),有个变量cpu_idle_force_poll,这个变量是指CPU空闲时是否会进入轮询状态,若该变量为真,则CPU不会进入睡眠状态,而是会执行语句asm volatile("" : : : "memory")。这种模式通常在需要CPU快速响应时使用,但是会使CPU使用率达到100%,因此需要在低耗能和高实时性之间做出抉择。若使该变量为真,则需要使能编译选项CONFIG_GENERIC_IDLE_POLL_SETUP,同时在grub文件内配置nohlt选项。
内层循环的判断语句还有个函数tick_check_broadcast_expired(),检查是否有任何CPU需要被唤醒。处于唤醒CPU的需要,系统会使用一个“广播设备”的特殊定时器来发送唤醒信号。tick_check_broadcast_expire()函数就是用于检查是否有CPU需要通过广播设备来唤醒。
若条件cpu_idle_force_poll || tick_check_broadcast_expired()一直为真,则程序会一直进行内层循环,不断执行函数cpu_idle_poll()。当程序退出内层循环,开始进行切换线程的工作,idle thread执行函数schedule_idle(),选择合适的任务线程,并在函数context_switch()中进行进程地址空间的切换和堆栈进程寄存器的切换。当idle thread执行到ret汇编语句时,此时链接寄存器lr存储的是目标任务线程的返回地址,也就是在这时,idle thread在真正意义上完成了向目标任务线程的切换。
在下一篇文章中,我将介绍其他任务线程切换回idle thread的函数调用过程。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。