《Linux 内核深度解析:基于 ARM64 架构的 Linux 4.x 内核》第四章:中断、异常与系统调用
中断与异常机制是连接硬件与内核逻辑的重要纽带,系统调用则是用户空间访问内核服务的主要入口。本章围绕 ARM64 架构上的这些机制,全面分析其实现原理、初始化流程与运行时行为。
一、ARM64 异常模型概览
1.1 异常级别(Exception Levels)
ARM64 架构定义了四个异常级别(EL):
异常级别 | 描述 | 用途 |
---|---|---|
EL3 | Secure Monitor | TrustZone 安全世界 |
EL2 | Hypervisor | 虚拟化层(KVM) |
EL1 | Kernel | 操作系统内核 |
EL0 | User | 应用程序(用户态) |
Linux 内核运行在 EL1,用户进程在 EL0,在某些系统中 KVM 使用 EL2。
二、异常类型与分类
ARM64 支持多种类型的异常,每种类型都对应一个特定的异常向量入口:
异常类型 | 描述 | 触发来源 |
---|---|---|
Synchronous | 同步异常,如 page fault、svc | 执行指令后立即触发 |
IRQ | 普通中断 | 外设中断,标记为 IRQ |
FIQ | 快速中断(ARMv8 可选) | 高优先级外设 |
SError | 系统错误异常,如总线错误 | 通常为硬件引发,不可恢复 |
三、异常向量表实现
ARM64 下异常向量表通过寄存器 VBAR_EL1
设置,指向异常向量基地址。该表必须 2048 字节对齐,分为四组,每组对应一个异常级别进入当前级别的场景(例如 EL0 → EL1, EL1 → EL1):
3.1 异常向量表布局
// arch/arm64/kernel/entry.S
ENTRY(vectors)
// 0x000: EL1t: 同步异常
b el1_sync
// 0x080: EL1t: IRQ 异常
b el1_irq
...
// 0x200: EL0t: 同步异常(用户发起 syscall)
b el0_sync
...
END(vectors)
- 每个向量入口都是一个固定大小的跳转指令,最终跳到具体处理函数。
- 早期启动阶段由
__cpu_setup()
设置VBAR_EL1 = vectors
。
四、中断架构初始化流程(IRQ Subsystem)
Linux 的中断处理体系在 kernel/irq/
中实现,架构无关,ARM64 架构通过 irqchip
子系统进行集成。
4.1 GICv3 中断控制器支持
ARM64 SoC 多使用 GICv2/GICv3/ITS 作为中断控制器,主要组件:
- Distributor (GICD):中断控制与分发中心,配置目标 CPU、触发方式
- Redistributor (GICR):为每个 CPU 核提供中断上下文
- ITS:GICv3 扩展,支持 MSI(PCI 中断)
驱动代码位于 drivers/irqchip/irq-gic-v3.c
:
gic_of_init()
:通过设备树初始化中断控制器gic_cpu_init()
:为每个 CPU 配置 GICR 和启用中断__gic_v3_do_irq()
:在中断发生后调用的汇编路径,跳入 C 处理
4.2 中断号与触发配置
- 内核抽象的中断号是逻辑编号(Linux IRQ Number),通过设备树
interrupts
属性绑定; - 支持
level
(电平触发)和edge
(边缘触发); request_irq()
用于驱动层注册中断回调。
五、中断处理路径详解
5.1 IRQ 异常入口
// arch/arm64/kernel/entry.S
el1_irq:
kernel_entry 1
bl asm_do_IRQ
kernel_exit 1
5.2 中断分发流程
// arch/arm64/kernel/irq.c
asmlinkage void asm_do_IRQ(struct pt_regs *regs)
{
irq_enter(); // 开启抢占计数
generic_handle_irq(read_irq_number()); // 调用中断控制器分发中断
irq_exit(); // 清理抢占标记
}
- 中断控制器根据 IRQ Number 定位具体 handler,并调用
__handle_irq_event_percpu()
→ 驱动层irq_handler_t
。 - 若启用软中断(tasklet/workqueue),部分中断在
softirq
阶段延迟执行。
六、系统调用机制(Syscall)
系统调用是用户态访问内核功能的桥梁。ARM64 使用 SVC
(Supervisor Call)指令从 EL0 切换到 EL1。
6.1 系统调用入口
// arch/arm64/kernel/entry.S
el0_sync:
kernel_entry 0
mrs x8, esr_el1 // 获取异常原因
cmp x8, #ESR_ELx_EC_SVC64
b.eq el0_svc // 若为 SVC,进入系统调用处理
ESR_EL1[31:26]
是 Exception Class (EC
);0x15
(0b010101
)为 64 位SVC
系统调用。
系统调用号通常放在
x8
寄存器,参数x0 ~ x5
。
6.2 syscall 调度表
// arch/arm64/kernel/sys.c
DEFINE_SYSCALL_TABLE(sys_call_table)
long syscall_trace_enter(struct pt_regs *regs)
{
syscall_fn_t fn = sys_call_table[regs->syscallno];
...
ret = fn(regs->regs[0], regs->regs[1], ..., regs->regs[5]);
}
系统调用表定义在 arch/arm64/kernel/sys.c
,每个系统调用号映射到对应函数,最终统一调用。
6.3 示例:用户调用流程
用户程序:
write(1, "hello\n", 6);
Glibc 封装后调用:
mov x8, #__NR_write
svc #0
EL0 → EL1 跳转后,内核通过 syscall 表找到 sys_write()
并执行。
七、异常上下文保存与 pt_regs
ARM64 在异常入口处,使用 kernel_entry 0/1
宏保存现场:
.macro kernel_entry el, regsize = 64
stp x29, x30, [sp, #-16]! // 保存调用者帧地址
...
mov x29, sp // 设置新的帧地址
mrs x0, sp_el0
...
.endm
内核构建一个 struct pt_regs
(定义在 arch/arm64/include/asm/ptrace.h
):
struct pt_regs {
u64 regs[31]; // x0 ~ x30
u64 sp; // 堆栈指针
u64 pc; // 程序计数器
u64 pstate; // 状态寄存器
};
这些信息供后续内核分析系统调用来源、异常上下文、调试等使用。
八、信号处理与异常返回
当用户进程收到信号时,内核会构建信号帧并设置 pc
跳转到用户注册的 handler 地址。常见流程:
- 捕获信号:软中断/中断上下文检查
do_signal()
; - 构建信号栈帧,保存原上下文到用户栈;
- 修改
pt_regs
中的 PC 为 handler 地址; - handler 执行后使用
sigreturn
恢复上下文。
总结
模块 | 关键点 |
---|---|
异常级别(EL) | 用户 EL0,内核 EL1,支持从 EL0 向上触发异常 |
异常向量表 | VBAR_EL1 指向的向量,入口在 entry.S 中 |
中断控制器 | GICv3 架构,支持 IRQ 分发,支持 CPU 热插拔 |
中断处理流程 | 汇编入口 → IRQ 分发器 → 驱动注册的 request_irq 回调 |
系统调用路径 | EL0 发起 SVC ,EL1 解析 syscall number 调用 syscall 表 |
pt_regs 上下文结构 | 用于保存/恢复寄存器、状态寄存器、异常返回 |
本章介绍的从硬件中断到系统调用的所有通道,是内核调度、IO、调试、异常恢复等机制的基础。下一章我们将进入调度系统与 CPU 运行时行为,探索 Linux 的多核调度策略与任务切换实现。
常用源码路径参考:
arch/arm64/kernel/entry.S
:异常入口arch/arm64/kernel/irq.c
:IRQ 异常处理drivers/irqchip/irq-gic-v3.c
:GIC 中断控制器驱动arch/arm64/include/asm/ptrace.h
:pt_regs 定义kernel/irq/handle.c
:中断分发器arch/arm64/kernel/sys.c
:系统调用表与 handler