Arm64架构-1-上下文切换-3-各种上下文简介
一、各种上下文简介
1. 分类总览
ARM64 上所有进入内核的路径可以归纳为:
------------------------------------------------------------------------------------------------------------------------------------ 入口类型 触发方式 Linux 上下文分类 in_interrupt() 可否睡眠 ------------------------------------------------------------------------------------------------------------------------------------ 系统调用 (SVC) 用户态主动 svc 指令 进程上下文 false 可以 Page Fault (Data/Instruction Abort) 当前指令触发 进程上下文 false 可以 未定义指令 (Undefined) 当前指令触发 进程上下文 false 可以 调试异常 (BRK/Watchpoint) 断点/单步 进程上下文 false 通常可以 对齐异常 (Alignment Fault) 当前指令触发 进程上下文 false 可以 IRQ 外部设备异步信号 中断上下文 true 不可以 FIQ 快速中断 中断上下文 true 不可以 SError 异步外部 abort 特殊处理(NMI 级别) — 不可以 ------------------------------------------------------------------------------------------------------------------------------------
2. 核心判断标准
Linux 内核判断"中断上下文"的唯一标准是 preempt_count 中的 hardirq/softirq/NMI 位:
#define in_interrupt() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK))
(1) IRQ 入口,会调用 irq_enter() --> 增加 hardirq 计数 --> in_interrupt() 为 true
(2) 同步异常入口,不会修改这些位 --> in_interrupt() 为 false
这是内核的设计决策:同步异常代表当前进程的执行流,由当前进程"负责",所以归属进程上下文。
3. 为什么这么设计
同步异常的本质特征:
(1) 因果确定 —— 是当前指令直接导致的,如果再执行一次相同指令,异常会再次发生;
(2) 代表当前进程 —— 处理结果会直接影响当前进程(映射页面、发 signal、杀进程等);
(3) 可以安全睡眠 —— 当前进程的内核栈完整可用,调度器可以切换走再切回来继续处理;
异步中断的本质特征:
(1) 与当前指令无关 —— 外部设备随时可能触发;
(2) 不代表当前进程 —— 只是碰巧打断了某个进程;
(3) 不能睡眠 —— 如果睡眠了,被打断的进程会莫名被挂起,整个调度语义崩溃;
同步异常 vs 中断的本质区别:
--------------------------------------------------------------------------------------------------------------------------- 特性 同步异常(Data Abort/Page Fault) 中断(IRQ) --------------------------------------------------------------------------------------------------------------------------- 触发方式 当前指令执行直接引起 外部设备异步信号 与当前进程的关系 代表当前进程执行 与当前进程无关 是否可以睡眠 可以 不可以 是否有 current 进程 有,就是触发异常的进程 借用被打断进程的栈,但不代表它 内核中的判断 in_interrupt() 返回 false in_interrupt() 返回 true ---------------------------------------------------------------------------------------------------------------------------
4. 异常与中断上下文对比
--------------------------------------------------------------------------------- 阶段 Page Fault (同步异常) IRQ (异步中断) --------------------------------------------------------------------------------- 入口 el0_da/el1_da el0_irq / el1_irq 保存寄存器 相同的 kernel_entry 相同的 kernel_entry 是否开中断 很快开中断 视情况,可能嵌套也可能不开 preempt_count 不增加 hardirq 计数 irq_enter() 增加 hardirq 计数 可否睡眠 可以 绝对不可以 in_interrupt() false true 可否调 schedule() 可以 会触发 BUG ---------------------------------------------------------------------------------
保存寄存器的机制是一样的(都用 kernel_entry 宏),但进入 C 代码后的行为完全不同。中断上下文通过 irq_enter() / irq_exit() 标记自己,而 page fault 不会做这个标记。
5. 一个容易混淆的点
虽然在硬件层面,ARM64 把 synchronous exception 和 IRQ 都统一放在"异常向量表"中处理,入口机制几乎一样(都经过 kernel_entry 保存寄存器),但 硬件入口机制相同不等于内核执行上下文相同。
硬件视角: 同步异常 ──┐ ├──--> 异常向量表 --> kernel_entry --> 保存寄存器 异步中断 ──┘ 内核视角: 同步异常 --> 进程上下文 (不调 irq_enter, 允许睡眠) 异步中断 --> 中断上下文 (调 irq_enter, 禁止睡眠)
6. 殊情况:内核态异常
如果异常发生在内核态(EL1),情况稍有不同:
--------------------------------------------------------------------------------------------------------------------------- 场景 上下文 能否睡眠 --------------------------------------------------------------------------------------------------------------------------- EL1 page fault(内核访问用户空间,如 copy_from_user) 进程上下文 可以(前提是不在原子区间) EL1 page fault 发生在 中断处理中 仍然是中断上下文 不可以 — 这种情况通常直接 oops EL1 page fault 发生在 持有 spinlock 时 进程上下文但 preempt 禁用 不应该睡眠 — fixup 或 oops ---------------------------------------------------------------------------------------------------------------------------
关键:异常本身不改变上下文类型。如果异常发生时已经在中断上下文中(比如 IRQ handler 里触发了 page fault),那处理这个异常时仍然是中断上下文——因为 preempt_count 的 hardirq 位已经被置位了。
7. 总结
Linux 用 preempt_count 定义上下文类型,而非用"是否通过异常向量进入内核"。同步异常(trap/fault)不修改 hardirq 计数,所以属于进程上下文;只有 IRQ/FIQ 路径会调用 irq_enter() 置位 hardirq,从而进入中断上下文。
缺页异常属于同步异常,运行在进程上下文,可休眠。
Linux 中 异常/陷阱 都属于进程上下文。
结论:在 Linux 内核中,同步异常(exception/trap)属于进程上下文,不属于中断上下文。
二、缺页异常发生时的状态保存机制
ARM64 缺页异常的状态保存分为两个层面:硬件自动完成 和 软件(内核)完成。
1. 硬件自动保存(CPU 在异常发生瞬间完成)
当 EL0(用户态)发生 Data Abort 时,ARM64 处理器在跳转到异常向量之前 自动完成以下动作:
--------------------------------------------------------------------------------------------------------------- 硬件动作 说明 --------------------------------------------------------------------------------------------------------------- PSTATE --> SPSR_EL1 将当前处理器状态(中断使能位、条件标志、执行模式等)保存到 SPSR_EL1 PC --> ELR_EL1 将触发异常的指令地址保存到 ELR_EL1(Exception Link Register) 异常原因 --> ESR_EL1 填写 Exception Syndrome Register,包含异常类型(Data Abort)、访问大小、读/写方向等 故障地址 --> FAR_EL1 将引起 abort 的虚拟地址写入 FAR_EL1(Fault Address Register) 切换异常级别 从 EL0 切换到 EL1 切换栈指针 开始使用 SP_EL1(内核栈) 屏蔽中断 自动设置 PSTATE.DAIF 中的相关位(默认屏蔽 IRQ/FIQ) ---------------------------------------------------------------------------------------------------------------
关键点:硬件只保存了最少量的状态(PC、PSTATE、异常信息),通用寄存器(X0-X30)不会被硬件自动保存。
2. 软件保存(Linux 内核异常入口代码完成)
跳转到异常向量表后,Linux 内核的汇编入口代码负责保存完整的 CPU 上下文。
2.1 异常向量表入口
ARM64 异常向量表定义在 entry.S 中。EL0 同步异常的入口是:
/* 异常向量表 (VBAR_EL1 指向此处), 每个 2^11=2KB 对齐, 共16个 entry, 每个 128 字节 */
.align 11 ENTRY(vectors) ... kernel_ventry el0t_64_sync //EL0 64-bit 同步异常入口 ...
2.2 kernel_entry 宏 —— 保存全部通用寄存器
进入向量后立即执行 kernel_entry 宏,在内核栈上构造一个 struct pt_regs:
.macro kernel_entry, el /* 在内核栈(上面硬件保存已将其指向内核栈)上分配 pt_regs 大小的空间, 栈从高地址向低地址增长 */ sub sp, sp, #PT_REGS_SIZE /* 保存通用寄存器 x0-x29, 低位存编号数值低的寄存器 */ stp x0, x1, [sp, #16 * 0] stp x2, x3, [sp, #16 * 1] stp x4, x5, [sp, #16 * 2] ... stp x28, x29, [sp, #16 * 14] /* 保存 x30 (LR) */ str x30, [sp, #S_LR] /* 从系统寄存器读取并保存到 pt_regs */ mrs x22, elr_el1 //保存返回地址 mrs x23, spsr_el1 //保存处理器状态 mrs x21, sp_el0 //保存用户态栈指针 stp x22, x23, [sp, #S_PC] //pt_regs->pc, pt_regs->pstate str x21, [sp, #S_SP] //pt_regs->sp (用户态SP 存储到 x21) /* 如果从 EL0 进入,保存用户态的 SP_EL0 */ .if \el == 0 /* 切换到当前进程的内核栈 */ ldr x19, [tsk, #TSK_TI_FLAGS] ... .endif .endm
2.3 pt_regs 结构体
所有保存的状态最终形成一个 struct pt_regs:
struct pt_regs { //ptrace.h union { struct user_pt_regs user_regs; struct { u64 regs[31]; //x0-x30 u64 sp; //用户态栈指针 u64 pc; //异常返回地址 (来自 ELR_EL1) u64 pstate; //处理器状态 (来自 SPSR_EL1) }; }; u64 orig_x0; //系统调用原始参数 s32 syscallno; //系统调用号 u32 unused2; u64 sdei_ttbr1; u64 pmr_save; u64 stackframe[2]; //用于回溯的 fp/lr u64 lockdep_hardirqs; u64 exit_rcu; };
2.4 完整流程时序
用户态线程执行 --> 访问未映射地址 │ ▼ ① 硬件自动 (CPU 微架构完成,纳秒级) ┌─────────────────────────┐ │ PSTATE --> SPSR_EL1 │ │ PC --> ELR_EL1 │ │ 异常信息 --> ESR_EL1 │ │ 故障地址 --> FAR_EL1 │ │ 切换到 EL1, 使用 SP_EL1 │ │ 屏蔽 DAIF 中断 │ └─────────────────────────┘ │ ▼ ② 跳转到 VBAR_EL1 + offset (EL0 64-bit sync) ┌─────────────────────────┐ │ kernel_ventry │ │ --> kernel_entry 宏 │ │ --> 保存 x0-x30 到栈 │ │ --> 保存 ELR/SPSR/SP_EL0│ │ --> 构造 pt_regs │ └─────────────────────────┘ │ ▼ ③ C 代码入口 ┌─────────────────────────┐ │ el0t_64_sync_handler() │ │ --> el0_da() │ │ --> 读取 ESR_EL1/FAR │ │ --> local_daif_restore│ <-- 重新开中断!恢复为进程上下文 │ --> do_mem_abort() │ │ --> do_page_fault() │ └─────────────────────────┘ │ ▼ ④ 缺页处理(进程上下文,中断已开) ┌─────────────────────────┐ │ handle_mm_fault() │ │ --> 分配页面/建立映射 │ │ --> 可能睡眠、调度 │ └─────────────────────────┘ │ ▼ ⑤ 返回用户态 ┌─────────────────────────┐ │ kernel_exit 宏 │ │ --> 恢复 x0-x30 │ │ --> 恢复 ELR/SPSR │ │ --> eret 指令 │ <-- 返回 EL0,恢复 PC 和 PSTATE └─────────────────────────┘
3. 关于"关中断"的进一步说明
你之前提到"执行过程中是关中断的",这里需要精确区分阶段:
(1) 硬件进入异常时 —— 确实自动屏蔽 IRQ(PSTATE.I = 1)
(2) kernel_entry 保存寄存器期间 —— 仍然关中断(保证保存过程原子性)
(3) 进入 C 代码后很快开中断 —— el0_da() 中调用 local_daif_restore() 恢复中断使能
//arch/arm64/kernel/entry-common.c static void noinstr el0_da(struct pt_regs *regs, unsigned long esr) { unsigned long far = read_sysreg(far_el1); enter_from_user_mode(regs); // 标记从用户态进入 local_daif_restore(DAIF_PROCCTX); // <-- 开中断!DAIF_PROCCTX 允许 IRQ/FIQ do_mem_abort(far, esr, regs); exit_to_user_mode(regs); }
所以:关中断窗口只存在于异常入口的很短一段(保存寄存器 --> 进入 C 函数初期)。do_page_fault 及之后的整个处理过程都是开中断的,可以被抢占、可以睡眠。
这正是 page fault 属于进程上下文的关键特征 —— 如果整个处理过程关中断,那内核中无数在 page fault 路径上的 might_sleep() 断言早就触发 BUG 了。
posted on 2026-08-14 15:34 Hello-World3 阅读(2) 评论(0) 收藏 举报
浙公网安备 33010602011771号