当前位置:   article > 正文

idle thread向其他任务线程的上下文切换过程

idle thread向其他任务线程的上下文切换过程

        以下文章讲解均以arm64架构、Linux 5.10.158源码为例子。

        在Linux kernel中,idle thread向其他任务线程的上下文切换,需要经历两大过程,分别是进程地址空间的切换、以及堆栈寄存器的转换。

        1、Linux kernel任务线程切换的过程context_switch

        context_switch切换过程大致如下所示。文章的这一part主要借鉴参考了这篇文章:深入理解Linux内核进程上下文切换[1],读者们可以阅读这篇文章更清楚的了解进程上下文切换的过程。

  1. context_switch()
  2. ->switch_mm_irqs_off() //上下文页表切换
  3. ->switch_to() //进程堆栈寄存器切换

        1.1 进程地址空间的切换

        在arm64架构中,寄存器ttbr0_el1指向非特权模式下的地址空间转换的页表基地址,指向的是初始转换表,用于较低的虚拟地址(VA)范围。寄存器ttbr1_el1指向特权模式下的地址空间转换的页表基地址,用于较高的虚拟地址范围。进程切换前后,需要改写寄存器ttbr0_el1。

  1. switch_mm_irqs_off()
  2. ->switch_mm()
  3. ->__switch_mm()
  4. ->check_and_switch_context()
  5. ->cpu_switch_mm()
  6. ->cpu_do_switch_mm()
  7. ->write_sysreg(ttbr1,ttbr1_el1)
  8. ->write_sysreg(ttbr0,ttrb0_el1)

        在切换过程中,实际完成寄存器改写的是函数write_sysreg(ttbr1,ttbr1_el1)和write_sysreg(ttbr0,ttbr0_el1)。 这两个函数将ttbr1和ttbr0的值各自写入到ttbr1_el1寄存器和ttbr0_el1寄存器中。

        1.2 堆栈进程寄存器的切换

        下图代码介绍了堆栈寄存器切换过程的函数路径以及关键汇编代码。需要说明的是,语句 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成员。

  1. switch_to()
  2. ->__switch_to(struct task_struct *prev, struct task_struct *next)
  3. ->cpu_switch_to(prev,next)
  4. SYM_FUNC_START(cpu_switch_to)
  5. mov x10, #THREAD_CPU_CONTEXT
  6. add x8, x0, x10
  7. mov x9, sp
  8. stp x19, x20, [x8], #16 // store callee-saved registers
  9. stp x21, x22, [x8], #16
  10. stp x23, x24, [x8], #16
  11. stp x25, x26, [x8], #16
  12. stp x27, x28, [x8], #16
  13. stp x29, x9, [x8], #16
  14. str lr, [x8] //x8寄存器加了6次16后,指向的是cpu_context中的pc寄存器
  15. add x8, x1, x10
  16. ldp x19, x20, [x8], #16 // restore callee-saved registers
  17. ldp x21, x22, [x8], #16
  18. ldp x23, x24, [x8], #16
  19. ldp x25, x26, [x8], #16
  20. ldp x27, x28, [x8], #16
  21. ldp x29, x9, [x8], #16
  22. ldr lr, [x8]
  23. mov sp, x9
  24. msr sp_el0, x1
  25. ptrauth_keys_install_kernel x1, x8, x9, x10
  26. scs_save x0, x8
  27. scs_load x1, x8
  28. ret
  29. SYM_FUNC_END(cpu_switch_to)
  30. 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结构体成员代码,可进一步明晰以上汇编代码的含义和意图。

  1. struct cpu_context {
  2. unsigned long x19;
  3. unsigned long x20;
  4. unsigned long x21;
  5. unsigned long x22;
  6. unsigned long x23;
  7. unsigned long x24;
  8. unsigned long x25;
  9. unsigned long x26;
  10. unsigned long x27;
  11. unsigned long x28;
  12. unsigned long fp;
  13. unsigned long sp;
  14. unsigned long pc;
  15. };

        2、idle thread向其他任务线程的切换过程函数路径图。

        下图为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的函数调用过程。

引用:[1] 深入理解Linux内核进程上下文切换-腾讯云开发者社区-腾讯云

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号