linux ARM64架构下进程切换核心代码分析

一、概述 

阶段 核心代码/函数 ARM64实现细节 相关数据结构 作用
调度入口 __schedule() 调用context_switch()完成实际切换16 struct rq 触发调度流程,选择下一个运行进程
地址空间切换 switch_mm_irqs_off() 通过ttbr0_el1寄存器更新进程页表基址(PGD)3,处理ASID和TLB失效410 struct mm_struct(含pgd_t成员) 切换用户空间虚拟地址映射,保证新进程访问正确的物理内存
处理器状态切换 switch_to() → __switch_to() 保存/恢复x19-x28, sp, pc等寄存器到task_struct->thread.cpu_context15 struct cpu_context 保存旧进程硬件上下文,加载新进程执行环境
TLB处理 flush_tlb_all()或惰性模式 根据ASID决定TLB全部刷新或部分失效,内核线程使用active_mm共享TLB410 struct tlb_state 避免旧进程TLB条目干扰新进程地址翻译
内核栈切换 task_thread_info(next)->stack 通过sp_el0切换用户态栈,sp_el1切换内核态栈8 struct thread_info 确保中断和异常处理使用正确的内核栈
调度完成处理 finish_task_switch() 清理旧进程资源(如释放mm_struct),处理惰性TLB状态的mmdrop()610 struct mm_struct 资源回收及后续状态维护

        ARM64架构下通用代码部分与64位x86架构完全相同,比如context_switch函数就是通用代码,参见kernel/sched/core.c文件。从中可以看到进程地址空间mm的切换和进程关键上下文的切换switch_to.这里将重点放在switch_to在ARM64架构下的具体实现代码的分析上。

        在ARM64架构下使用的switch_to仍然是通用代码,在include/asm-generic/switch_to.h文件中定义,而其中调用的__switch_to则是架构相关的代码了

 27 #define switch_to(prev,next,last)                   \
 28 do {                                    \
 29     __complete_pending_tlbi();                  \
 30     if (IS_ENABLED(CONFIG_CURRENT_POINTER_IN_TPIDRURO) || is_smp()) \
 31         __this_cpu_write(__entry_task, next);           \
 32     last = __switch_to(prev,task_thread_info(prev), task_thread_info(next));    \
 33 } while (0)

        ARM64架构下__switch_to的实现见arch/arm64/kernel/process.c文件,摘录代买如下,其中cpu_switch_to是需要特别关心的进程的CPU上下文切换的关键代码

523 struct task_struct *__switch_to(struct task_struct *prev,
524                 struct task_struct *next)
525 {
526     struct task_struct *last;
527 
528     fpsimd_thread_switch(next);
529     tls_thread_switch(next);
530     hw_breakpoint_thread_switch(next);
531     contextidr_thread_switch(next);
532     entry_task_switch(next);
533     ssbs_thread_switch(next);
534     erratum_1418040_thread_switch(next);
535     ptrauth_thread_switch_user(next);
536 
537     /*
538      * Complete any pending TLB or cache maintenance on this CPU in case
539      * the thread migrates to a different CPU.
540      * This full barrier is also required by the membarrier system
541      * call.
542      */
543     dsb(ish);
544 
545     /*
546      * MTE thread switching must happen after the DSB above to ensure that
547      * any asynchronous tag check faults have been logged in the TFSR*_EL1
548      * registers.
549      */
550     mte_thread_switch(next);
551     /* avoid expensive SCTLR_EL1 accesses if no change */
552     if (prev->thread.sctlr_user != next->thread.sctlr_user)
553         update_sctlr_el1(next->thread.sctlr_user);
554 
555     /* the actual thread switch */
556     last = cpu_switch_to(prev, next);
557 
558     return last;
559 }

         cpu_switch_to的实现代码是一段ARM64的汇编语言代码,参见arch/arm64/kernel/entry.S文件,摘录代码如下。

 826 SYM_FUNC_START(cpu_switch_to)
 827     mov x10, #THREAD_CPU_CONTEXT
 828     add x8, x0, x10
 829     mov x9, sp
 830     stp x19, x20, [x8], #16     // store callee-saved registers
 831     stp x21, x22, [x8], #16
 832     stp x23, x24, [x8], #16
 833     stp x25, x26, [x8], #16
 834     stp x27, x28, [x8], #16
 835     stp x29, x9, [x8], #16
 836     str lr, [x8]
 837     add x8, x1, x10
 838     ldp x19, x20, [x8], #16     // restore callee-saved registers
 839     ldp x21, x22, [x8], #16
 840     ldp x23, x24, [x8], #16
 841     ldp x25, x26, [x8], #16
 842     ldp x27, x28, [x8], #16
 843     ldp x29, x9, [x8], #16
 844     ldr lr, [x8]
 845     mov sp, x9
 846     msr sp_el0, x1
 847     ptrauth_keys_install_kernel x1, x8, x9, x10
 848     scs_save x0
 849     scs_load_current
 850     ret
 851 SYM_FUNC_END(cpu_switch_to)
 852 NOKPROBE(cpu_switch_to)

        这段代码和中断处理(包括系统调用)一样不遵守函数调用框架结构,在整个内核代码中比较关键的,值得仔细分析。

我们以具体的例子来说明常规函数调用中栈帧和寄存器保存的过程。

假设有两个函数:函数A(caller)和函数B(callee)。函数A在调用函数B之前,正在使用寄存器x19和x20,并且希望在函数B返回后继续使用这些寄存器中的值。按照AAPCS64标准,x19和x20是被调用者保存寄存器(callee-saved),这意味着如果函数B要使用这些寄存器,它必须在自己的栈帧中保存它们的原始值,并在返回前恢复。

步骤:

  1. 函数A(调用者)准备调用函数B:

    • 将参数放入x0-x7(如果适用)。
    • 使用bl指令跳转到函数B(同时将返回地址存入lr/x30)。
  2. 函数B(被调用者)开始执行:

    • 首先,函数B需要保存它将要使用的被调用者保存寄存器(这里是x19和x20,以及帧指针x29和返回地址x30)到栈上。
    • 通常,函数开头会有序言(prologue)代码来保存这些寄存器。
  3. 函数B执行完毕,准备返回:

    • 函数B从栈中恢复之前保存的寄存器(包括x19和x20),确保它们的值和进入函数B时一样。
    • 然后返回到函数A。
  4. 函数A继续执行,它使用的x19和x20的值没有被改变。

具体代码示例:

场景设定
假设函数main()调用函数calc():

// C代码示例
void calc() {
    long b = 20;
    // ... 执行计算 ...
}

int main() {
    long a = 10;  // 假设存储在寄存器x19
    calc();
    printf("%ld", a);  // 需要确保a仍是10
}



常规函数调用流程(遵循AAPCS64标准)
1. main函数准备调用calc(调用者)
main:
    // 假设编译器将变量a分配到x19
    mov x19, #10          // a = 10
    
    // 调用前的准备工作(调用者负责)
    stp x29, x30, [sp, #-16]!  // 保存自己的帧指针(x29)和返回地址(x30/lr)
    bl  calc              // 跳转到calc(),同时保存返回地址到x30



2. calc函数开始执行(被调用者)
calc:
    // ====== 标准序言(编译器自动生成)====== 
    sub sp, sp, #32       // 在栈上分配32字节空间 (创建栈帧)
    stp x29, x30, [sp]    // 保存调用者的帧指针和返回地址
    stp x19, x20, [sp, #16] // 保存被调用者寄存器(x19,x20)
    
    // === 函数体(可能覆盖寄存器)===
    mov x19, #20          // 使用x19存储b=20(覆盖了main的a值!)
    
    // ====== 标准尾声(编译器自动生成)====== 
    ldp x19, x20, [sp, #16] // 恢复x19,x20到原来的值
    ldp x29, x30, [sp], #32  // 恢复帧指针和返回地址,销毁栈帧
    ret                   // 返回到main


3. 关键恢复过程详解
当calc执行到恢复指令时:
ldp x19, x20, [sp, #16]  // 从栈帧加载原始值到x19/x20

此时发生:

从栈帧偏移16字节处读取8字节 → 存入x19
从栈帧偏移24字节处读取8字节 → 存入x20
‌结果‌:x19的值从20变回10(即main函数中a的值)
4. main函数继续执行

    // calc返回后
    ldp x29, x30, [sp], #16  // main恢复自己的帧指针和返回地址
    // 此时x19的值仍然是10!
    adrp x0, .printf_str
    bl   printf              // 正确打印出10

违反常规框架的cpu_switch_to

对比问题中的上下文切换代码:

cpu_switch_to:
    // 直接保存寄存器到线程结构体而非栈帧
    stp x19, x20, [x8], #16  // 保存到内存位置x8(线程控制块)
    
    // ...(跳过标准序言/尾声)...
    
    // 直接加载新线程的寄存器值
    ldp x19, x20, [x8], #16  // 从新线程的内存位置加载
    
    ret  // 返回到新线程的地址(非原调用者)

核心区别总结:

特性 常规函数调用 上下文切换/系统调用
寄存器保存位置 当前栈帧 线程控制块/内核数据结构
保存/恢复责任方 被调用函数负责callee-saved寄存器 手动管理所有关键寄存器
栈帧管理 自动创建/销毁栈帧 直接操作栈指针(如mov sp, x9)
返回地址处理 保存/恢复x30 覆盖lr指向新线程的返回地址
调用关系 层级返回 非对称跳转(切换到全新上下文)

正是这种绕过标准栈帧、直接操控寄存器和返回地址的行为,使上下文切换和系统调用成为"特殊流程",而非普通函数调用 

         

         

你可能感兴趣的:(linux ARM64架构下进程切换核心代码分析)