阶段 | 核心代码/函数 | 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_context 15 |
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要使用这些寄存器,它必须在自己的栈帧中保存它们的原始值,并在返回前恢复。
步骤:
函数A(调用者)准备调用函数B:
函数B(被调用者)开始执行:
函数B执行完毕,准备返回:
函数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 指向新线程的返回地址 |
调用关系 | 层级返回 | 非对称跳转(切换到全新上下文) |
正是这种绕过标准栈帧、直接操控寄存器和返回地址的行为,使上下文切换和系统调用成为"特殊流程",而非普通函数调用