2. ARM64架构基础(上)
在将 Linux 内核调度器移植到 ARM64 平台之前,必须深入理解 ARM64 架构的核心基础。本章聚焦于 AArch64 执行状态、异常级别模型以及寄存器组体系,这些是操作系统运行和上下文切换的硬件基石。
2.1 AArch64 执行状态
ARMv8 架构引入了两种执行状态:AArch64 和 AArch32。调度器移植主要面向 AArch64 状态,它提供了 64 位虚拟地址空间和全新的异常模型。
| 特性 | AArch64 | AArch32 |
|---|---|---|
| 寄存器宽度 | 64 位通用寄存器 (X0-X30) | 32 位通用寄存器 (R0-R15) |
| 指令集 | A64 指令集(固定 32 位长度) | A32/T32 指令集(可变长度) |
| 虚拟地址空间 | 最大 48 位(可扩展至 52 位) | 最大 32 位 |
| 异常级别 | EL0 - EL3(4 个级别) | PL0 - PL2(3 个级别) |
| 兼容性 | 原生 64 位应用 | 向后兼容 ARMv7 32 位应用 |
调度器视角: AArch64 状态下的 64 位寄存器使得内核可以管理更大的进程地址空间,同时异常级别的清晰划分让调度器能够在 EL1(内核态)安全地执行上下文切换,而用户进程运行在 EL0。
2.2 异常级别(Exception Levels: EL0 - EL3)
ARM64 架构定义了 4 个异常级别,数字越大特权越高。调度器主要工作在 EL1,而用户进程运行在 EL0。异常级别的设计为操作系统提供了强大的隔离和保护机制。
| 异常级别 | 名称 | 运行内容 | 调度器相关 |
|---|---|---|---|
| EL0 | 用户态 (User) | 用户应用程序 | 进程上下文被调度,无权访问内核数据结构 |
| EL1 | 内核态 (Kernel) | 操作系统内核 | 调度器主代码运行于此,执行上下文切换 |
| EL2 | 虚拟机监视器 (Hypervisor) | 虚拟化层 (KVM, Xen) | 调度虚拟 CPU,管理虚拟机调度 |
| EL3 | 安全监视器 (Secure Monitor) | 安全固件 (ATF, TrustZone) | 切换安全/非安全世界,调度器不直接涉及 |
关键概念: 异常级别之间的切换通过异常(Exception)或返回指令(ERET)实现。当用户进程(EL0)触发系统调用或中断时,CPU 会提升到 EL1 执行内核代码,调度器借此机会进行进程选择与切换。
2.3 寄存器组
ARM64 架构的寄存器组是调度器进行上下文切换的核心操作对象。理解寄存器分类和用途,是编写正确且高效的 switch_to 汇编代码的前提。
2.3.1 通用寄存器(X0 - X30)
AArch64 提供了 31 个 64 位通用寄存器 X0 - X30。每个寄存器也可作为 32 位使用(W0 - W30),低 32 位有效,高 32 位清零。
| 寄存器 | 别名 | 用途说明 | 调度器上下文保存 |
|---|---|---|---|
| X0 - X7 | 参数寄存器 | 传递函数参数和返回值 | 需要保存(调用者保存) |
| X8 | 间接结果寄存器 | 返回大结构体指针 | 需要保存 |
| X9 - X15 | 临时寄存器 | 函数内部临时变量 | 需要保存(调用者保存) |
| X16 - X17 | IP0, IP1 | 过程内调用临时寄存器 | 需要保存 |
| X18 | 平台寄存器 | 平台特定用途(如 TCB) | 通常不保存 |
| X19 - X28 | 被调用者保存寄存器 | 函数必须保存并恢复 | 必须保存 |
| X29 | FP (帧指针) | 栈帧基址指针 | 必须保存 |
| X30 | LR (链接寄存器) | 函数返回地址 | 必须保存 |
调度器上下文切换要点: 在
switch_to 汇编中,必须保存并恢复 X19-X30、SP、PC(通过 LR 和栈操作)。X0-X18 由编译器生成的调用者保存代码处理,调度器无需显式保存。
2.3.2 特殊寄存器
特殊寄存器控制 CPU 状态、异常处理、内存管理和系统配置。调度器移植需要重点关注以下寄存器:
| 寄存器 | 全称 | 功能描述 | 调度器相关性 |
|---|---|---|---|
| SP_EL0 / SP_EL1 | 栈指针 | 不同异常级别的栈指针 | 切换进程时需切换 SP_EL0(用户栈) |
| ELR_EL1 | 异常链接寄存器 | 异常返回地址 | 保存被中断进程的 PC |
| SPSR_EL1 | 保存的程序状态寄存器 | 异常发生时的 PSTATE | 保存 CPU 状态标志(条件码、中断掩码) |
| TPIDR_EL1 | 硬件线程 ID 寄存器 | 存储 per-CPU 变量基址 | 调度器用于快速访问当前 CPU 数据 |
| TTBR0_EL1 / TTBR1_EL1 | 页表基址寄存器 | 用户/内核地址空间页表 | 进程切换时需切换 TTBR0_EL1 |
| CONTEXTIDR_EL1 | 上下文 ID 寄存器 | 标识当前进程 ASID | TLB 管理,调度器写入新进程 ASID |
2.4 上下文切换的寄存器保存策略
在 Linux 内核调度器中,switch_to 宏负责保存当前进程的寄存器状态并恢复下一个进程的状态。以下是 ARM64 平台上下文切换的核心汇编逻辑:
/*
* switch_to 核心流程 (arch/arm64/kernel/entry.S)
* 参数:
* x0 - prev (当前进程 task_struct)
* x1 - next (下一个进程 task_struct)
* x2 - last (返回时保存 prev 的地址)
*/
ENTRY(cpu_switch_to)
// 保存被调用者保存寄存器 (callee-saved)
stp x19, x20, [x8, #16 * 0] // 保存到 prev->thread.cpu_context
stp x21, x22, [x8, #16 * 1]
stp x23, x24, [x8, #16 * 2]
stp x25, x26, [x8, #16 * 3]
stp x27, x28, [x8, #16 * 4]
stp x29, x30, [x8, #16 * 5] // FP 和 LR
// 保存栈指针
mov x9, sp
str x9, [x8, #16 * 6] // 保存 SP
// 切换 TTBR0_EL1 (用户地址空间)
ldr x10, [x1, #TSK_TI_TTBR0] // 从 next->thread_info 加载页表
msr ttbr0_el1, x10
isb // 指令同步屏障
// 恢复下一个进程的寄存器
ldp x19, x20, [x9, #16 * 0] // 从 next->thread.cpu_context 恢复
ldp x21, x22, [x9, #16 * 1]
ldp x23, x24, [x9, #16 * 2]
ldp x25, x26, [x9, #16 * 3]
ldp x27, x28, [x9, #16 * 4]
ldp x29, x30, [x9, #16 * 5] // 恢复 FP 和 LR
// 恢复栈指针
ldr x10, [x9, #16 * 6]
mov sp, x10
// 返回,此时 LR 指向新进程的返回地址
ret
ENDPROC(cpu_switch_to)
移植要点: 上述代码展示了 ARM64 上下文切换的核心。注意
TTBR0_EL1 的切换必须在恢复寄存器之前完成,以确保新进程的用户空间页表生效。同时,isb 指令保证了页表切换的可见性。
2.5 本章小结
- AArch64 执行状态 是 ARM64 的原生 64 位模式,提供 64 位寄存器、48 位虚拟地址和 4 级异常模型。
- 异常级别 EL0-EL3 为调度器提供了清晰的隔离边界:调度器运行在 EL1,用户进程在 EL0,虚拟化在 EL2,安全固件在 EL3。
- 通用寄存器 X0-X30 中,X19-X30 和 SP 是上下文切换必须保存的寄存器,X0-X18 由调用者负责。
- 特殊寄存器 如 TTBR0_EL1、ELR_EL1、SPSR_EL1 和 TPIDR_EL1 在进程切换和异常处理中起关键作用。
- 理解这些硬件基础是编写正确、高效的调度器移植代码的前提,后续章节将深入调度器数据结构与算法在 ARM64 上的具体实现。