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)    收藏  举报

导航