基于 Linux 内核源码(master 分支,commit 8a30aeb0d 附近)实际阅读生成
- RISC-V ISA 基础:RV32/RV64 与标准扩展
- 特权级别:U/S/M 模式与 CSR 寄存器
- 异常与中断处理
- 内存管理单元:Sv39/Sv48/Sv57 页表
- 上下文切换:__switch_to 与 thread_struct
- SBI(Supervisor Binary Interface)
- 设备发现:Device Tree
- 平台启动流程:OpenSBI → Linux start_kernel
- 向量扩展(RVV)支持
- Zicbom 缓存块管理扩展
- 中断控制器:PLIC/CLINT/AIA
- 内核虚拟内存布局
RISC-V 是一套开放的精简指令集架构(ISA),由加州大学伯克利分校设计。其最显著的特征是模块化设计:一个极简基础整数指令集(I)加上可选的标准扩展组合。
基础整数宽度决定了寄存器宽度和地址空间大小:
RV32I : 32 位寄存器,32 位地址空间
RV64I : 64 位寄存器,64 位地址空间(Linux 主流部署)
RV128I : 128 位(实验性,目前无内核支持)
RISC-V 的扩展以字母标记,常见组合如下:
| 扩展字母 | 名称 | 说明 |
|---|---|---|
| I | Integer | 基础整数指令集(必须) |
| M | Multiply/Divide | 整数乘除法 |
| A | Atomic | 原子操作(AMO 系列) |
| F | Single-Precision Float | 单精度浮点 |
| D | Double-Precision Float | 双精度浮点(需要 F) |
| C | Compressed | 16 位压缩指令(代码密度优化) |
| V | Vector | SIMD 向量扩展(RVV) |
| H | Hypervisor | 虚拟化扩展 |
| Zicsr | CSR Instructions | CSR 读写指令 |
| Zicbom | Cache Block Management | 缓存块操作(clean/flush/invalidate) |
| Svnapot | NAPOT PTE | 自然对齐页表项 |
| Svpbmt | Page-Based Memory Type | 页表内存类型 |
| Svvptc | Virtual TLB Management | 虚拟 TLB 控制 |
| Zicfiss | Shadow Stack | 控制流完整性(影子栈) |
Linux 内核在 arch/riscv/include/asm/csr.h 中通过 RISCV_ISA_EXT_* 枚举标记各扩展的可用性,并在运行时通过 riscv_has_extension_likely() / riscv_has_extension_unlikely() 使用静态分支进行高效检查。
RISC-V 定义了 32 个通用整数寄存器(x0–x31),ABI 名称如下:
x0 / zero : 硬连接到 0,写入被丢弃
x1 / ra : 返回地址(Return Address)
x2 / sp : 栈指针(Stack Pointer)
x3 / gp : 全局指针(Global Pointer)
x4 / tp : 线程指针(Thread Pointer)—— 内核中指向 task_struct
x5 / t0 : 临时寄存器
x6 / t1 : 临时寄存器
x7 / t2 : 临时寄存器
x8 / s0/fp : 保留寄存器 / 帧指针
x9 / s1 : 保留寄存器
x10 / a0 : 函数参数 / 返回值
x11 / a1 : 函数参数 / 返回值
x12 / a2 : 函数参数
x13 / a3 : 函数参数
x14 / a4 : 函数参数
x15 / a5 : 函数参数
x16 / a6 : 函数参数
x17 / a7 : 函数参数(也用于 SBI 扩展 ID)
x18-x27 / s2-s11 : 保留寄存器(被调用者保存)
x28-x31 / t3-t6 : 临时寄存器(调用者保存)
在内核异常入口 handle_exception(arch/riscv/kernel/entry.S,第 125 行)中,寄存器 tp(x4)专门用于保存指向当前任务 thread_info 的指针。
RISC-V 定义了三种特权模式,构成完整的软件栈:
+-----------------------------------------------+
| Machine Mode (M) 最高特权 |
| OpenSBI / 固件运行于此层 |
+-----------------------------------------------+
| Supervisor Mode (S) 内核特权 |
| Linux 内核运行于此层 |
+-----------------------------------------------+
| User Mode (U) 最低特权 |
| 用户进程运行于此层 |
+-----------------------------------------------+
每个模式有自己对应的 CSR 寄存器集合:mXXX(M 模式),sXXX(S 模式),hXXX(Hypervisor 扩展)。
Linux 内核通常运行在 S 模式,通过 SBI 接口与 M 模式固件(OpenSBI)交互。
源码文件:arch/riscv/include/asm/csr.h
sstatus(Supervisor Status Register,CSR 地址 0x100)
// arch/riscv/include/asm/csr.h,第 13–66 行
#define SR_SIE _AC(0x00000002, UL) /* Supervisor Interrupt Enable */
#define SR_SPIE _AC(0x00000020, UL) /* Previous Supervisor IE */
#define SR_SPP _AC(0x00000100, UL) /* Previously Supervisor (mode) */
#define SR_SUM _AC(0x00040000, UL) /* Supervisor User Memory Access */
#define SR_FS _AC(0x00006000, UL) /* Floating-point Status */
#define SR_FS_OFF _AC(0x00000000, UL)
#define SR_FS_INITIAL _AC(0x00002000, UL)
#define SR_FS_CLEAN _AC(0x00004000, UL)
#define SR_FS_DIRTY _AC(0x00006000, UL)
#define SR_VS _AC(0x00000600, UL) /* Vector Status */
#define SR_VS_OFF _AC(0x00000000, UL)
#define SR_VS_INITIAL _AC(0x00000200, UL)
#define SR_VS_CLEAN _AC(0x00000400, UL)
#define SR_VS_DIRTY _AC(0x00000600, UL)
#define SR_SD _AC(0x8000000000000000, UL) /* FS/VS/XS dirty(64位)*/SR_FS 和 SR_VS 字段各占 2 位,用于惰性保存/恢复 FPU 和向量单元状态。OFF=00、Initial=01、Clean=10、Dirty=11。
satp(Supervisor Address Translation and Protection,CSR 0x180)
// arch/riscv/include/asm/csr.h,第 76–85 行(64 位配置)
#define SATP_PPN _AC(0x00000FFFFFFFFFFF, UL) /* 物理页号,位[43:0] */
#define SATP_MODE_39 _AC(0x8000000000000000, UL) /* Sv39 */
#define SATP_MODE_48 _AC(0x9000000000000000, UL) /* Sv48 */
#define SATP_MODE_57 _AC(0xa000000000000000, UL) /* Sv57 */
#define SATP_MODE_SHIFT 60
#define SATP_ASID_BITS 16
#define SATP_ASID_SHIFT 44
#define SATP_ASID_MASK _AC(0xFFFF, UL)satp 寄存器的结构(RV64):
63 60 59 44 43 0
+----------+----------+------------------+
| MODE(4) | ASID(16) | PPN(44) |
+----------+----------+------------------+
Sv39=8 0-65535 根页表物理页号
Sv48=9
Sv57=10
stvec(Supervisor Trap Vector,CSR 0x105)
异常/中断向量寄存器。在 arch/riscv/kernel/head.S 第 205–206 行中写入:
la a0, handle_exception
csrw CSR_TVEC, a0 // CSR_TVEC 在 S 模式下等同于 CSR_STVECsepc(Supervisor Exception Program Counter,CSR 0x141)
保存触发异常时的指令地址(PC)。在 entry.S 第 185 行:
csrr s2, CSR_EPCscause(Supervisor Cause,CSR 0x142)
触发异常或中断的原因,最高位为 1 表示中断:
// arch/riscv/include/asm/csr.h,第 88 行
#define CAUSE_IRQ_FLAG (_AC(1, UL) << (__riscv_xlen - 1))sscratch(Supervisor Scratch,CSR 0x140)
内核用于内核/用户模式识别的关键寄存器:
- 运行于内核态时值为 0
- 运行于用户态时保存当前 hart 的
task_struct指针
在 entry.S 第 131 行:
csrrw tp, CSR_SCRATCH, tp // 原子交换:tp <-> sscratch
bnez tp, .Lsave_context // tp 非零说明来自用户态| CSR 名称 | 地址 | 说明 |
|---|---|---|
| sstatus | 0x100 | S 模式状态寄存器 |
| sie | 0x104 | S 模式中断使能 |
| stvec | 0x105 | S 模式异常向量 |
| sscratch | 0x140 | S 模式临时暂存 |
| sepc | 0x141 | S 模式异常 PC |
| scause | 0x142 | S 模式异常原因 |
| stval | 0x143 | S 模式陷阱值(故障地址等) |
| sip | 0x144 | S 模式中断挂起 |
| satp | 0x180 | S 模式地址转换保护 |
| mstatus | 0x300 | M 模式状态寄存器 |
| mtvec | 0x305 | M 模式异常向量 |
| mepc | 0x341 | M 模式异常 PC |
| mcause | 0x342 | M 模式异常原因 |
| mhartid | 0xf14 | 当前 hart 的硬件 ID |
| vstart | 0x008 | 向量起始元素索引 |
| vtype | 0xc21 | 向量类型(元素宽度/分组/尾部) |
| vl | 0xc20 | 向量长度 |
| vlenb | 0xc22 | 向量寄存器字节长度 |
源码文件:arch/riscv/kernel/entry.S,第 125 行起
整个异常处理流程从汇编标号 handle_exception 开始,这是写入 stvec 的入口点:
用户态/内核态触发异常或中断
|
v
handle_exception (entry.S:125)
|
csrrw tp, CSR_SCRATCH, tp -- 判断来源
|
tp非0 (来自用户态) tp为0 (已在内核)
| |
.Lsave_context .Lrestore_kernel_tpsp
| |
+--------+------------+
|
保存完整寄存器上下文到内核栈
(x1/ra, x3/gp, x5-x31)
(sstatus, sepc, stval, scause)
|
清除 SR_SUM | SR_FS_VS(安全隔离)
|
csrw CSR_SCRATCH, x0 -- 标记进入内核
|
判断 scause 最高位
|
bit=1(中断) bit=0(异常)
| |
call do_irq 查 excp_vect_table
|
根据 cause 码分发
|
ret_from_exception
上下文保存细节(entry.S 第 160–194 行):
.Lsave_context:
REG_S sp, TASK_TI_USER_SP(tp) // 保存用户栈指针
REG_L sp, TASK_TI_KERNEL_SP(tp) // 切换到内核栈
addi sp, sp, -(PT_SIZE_ON_STACK) // 在内核栈上分配 pt_regs
REG_S x1, PT_RA(sp)
REG_S x3, PT_GP(sp)
REG_S x5, PT_T0(sp)
save_from_x6_to_x31 // 宏:保存 x6-x31
// ... 禁用 SUM(阻止内核随意访问用户内存)
csrrc s1, CSR_STATUS, t0 // 读状态并清除 FS/VS/SUM
csrr s2, CSR_EPC // 保存异常 PC
csrr s3, CSR_TVAL // 保存陷阱值(如故障地址)
csrr s4, CSR_CAUSE // 保存异常原因
REG_S s1, PT_STATUS(sp)
REG_S s2, PT_EPC(sp)
REG_S s4, PT_CAUSE(sp)
csrw CSR_SCRATCH, x0 // 标记:现在在内核中在 entry.S 第 480–501 行定义了异常向量表 excp_vect_table:
SYM_DATA_START_LOCAL(excp_vect_table)
RISCV_PTR do_trap_insn_misaligned // cause=0: 指令地址未对齐
ALT_INSN_FAULT(RISCV_PTR do_trap_insn_fault) // cause=1: 指令访问故障
RISCV_PTR do_trap_insn_illegal // cause=2: 非法指令
RISCV_PTR do_trap_break // cause=3: 断点(ebreak)
RISCV_PTR do_trap_load_misaligned // cause=4: 加载未对齐
RISCV_PTR do_trap_load_fault // cause=5: 加载访问故障
RISCV_PTR do_trap_store_misaligned // cause=6: 存储未对齐
RISCV_PTR do_trap_store_fault // cause=7: 存储访问故障
RISCV_PTR do_trap_ecall_u // cause=8: 用户态 ecall(系统调用)
RISCV_PTR do_trap_ecall_s // cause=9: S 模式 ecall
RISCV_PTR do_trap_unknown // cause=10: 保留
RISCV_PTR do_trap_ecall_m // cause=11: M 模式 ecall
ALT_PAGE_FAULT(RISCV_PTR do_page_fault) // cause=12: 指令页错误
RISCV_PTR do_page_fault // cause=13: 加载页错误
RISCV_PTR do_trap_unknown // cause=14: 保留
RISCV_PTR do_page_fault // cause=15: 存储页错误
RISCV_PTR do_trap_unknown // cause=16-17: 保留
RISCV_PTR do_trap_software_check // cause=18: 软件检查异常
SYM_DATA_END_LABEL(excp_vect_table, SYM_L_LOCAL, excp_vect_table_end)查表逻辑(entry.S 第 225–233 行):
slli t0, s4, RISCV_LGPTR // cause << 3(乘以指针大小)
la t1, excp_vect_table
la t2, excp_vect_table_end
add t0, t1, t0
bgeu t0, t2, 3f // 越界则跳到 do_trap_unknown
REG_L t1, 0(t0) // 加载处理函数指针
jalr t1 // 间接调用// arch/riscv/include/asm/csr.h,第 105–123 行
#define EXC_INST_MISALIGNED 0 // 指令地址未对齐
#define EXC_INST_ACCESS 1 // 指令访问故障
#define EXC_INST_ILLEGAL 2 // 非法指令
#define EXC_BREAKPOINT 3 // 断点
#define EXC_LOAD_MISALIGNED 4 // 加载地址未对齐
#define EXC_LOAD_ACCESS 5 // 加载访问故障
#define EXC_STORE_MISALIGNED 6 // 存储地址未对齐
#define EXC_STORE_ACCESS 7 // 存储访问故障
#define EXC_SYSCALL 8 // 用户态 ecall(系统调用)
#define EXC_INST_PAGE_FAULT 12 // 指令页错误
#define EXC_LOAD_PAGE_FAULT 13 // 加载页错误
#define EXC_STORE_PAGE_FAULT 15 // 存储页错误中断原因(cause 最高位=1,取低位):
// arch/riscv/include/asm/csr.h,第 91–102 行
#define IRQ_S_SOFT 1 // S 模式软件中断
#define IRQ_M_SOFT 3 // M 模式软件中断
#define IRQ_S_TIMER 5 // S 模式计时器中断
#define IRQ_M_TIMER 7 // M 模式计时器中断
#define IRQ_S_EXT 9 // S 模式外部中断(来自 PLIC)
#define IRQ_M_EXT 11 // M 模式外部中断
#define IRQ_PMU_OVF 13 // PMU 溢出中断(AIA 扩展)ret_from_exception(entry.S 第 247–318 行)负责恢复状态并返回:
- 检查
PT_STATUS中的SR_SPP位,判断是否返回用户态 - 若返回用户态,写回
CSR_SCRATCH = tp(保存内核结构指针) - 恢复所有通用寄存器(x1–x31)
- 清除 LR/SC 保留(原子操作安全):
REG_SC x0, a2, PT_EPC(sp) - 执行
sret返回(M 模式执行mret)
csrw CSR_STATUS, a0 // 恢复 sstatus
csrw CSR_EPC, a2 // 恢复 sepc(返回地址)
REG_L x1, PT_RA(sp)
REG_L x3, PT_GP(sp)
...
REG_L x2, PT_SP(sp)
sret // 返回用户/内核态,硬件自动切换特权级RISC-V 支持三种分页模式,通过 satp 寄存器的 MODE 字段选择:
Sv39(3 级页表,39 位虚拟地址空间)
虚拟地址 [38:0](39位有效):
63 39 38 30 29 21 20 12 11 0
+--------+---------+---------+---------+----------+
| 符号扩展| VPN[2] | VPN[1] | VPN[0] | offset |
| (补全) | (9位) | (9位) | (9位) | (12位) |
+--------+---------+---------+---------+----------+
物理地址:56 位
页表级别:3 级(PGD -> PMD -> PTE)
Sv48(4 级页表,48 位虚拟地址空间)
63 48 47 39 38 30 29 21 20 12 11 0
+--------+---------+---------+---------+---------+-------+
|符号扩展 | VPN[3] | VPN[2] | VPN[1] | VPN[0] | off |
| | (9位) | (9位) | (9位) | (9位) |(12位)|
+--------+---------+---------+---------+---------+-------+
物理地址:56 位
页表级别:4 级(P4D -> PUD -> PMD -> PTE)
Sv57(5 级页表,57 位虚拟地址空间)
63 57 56 48 47 39 38 30 29 21 20 12
+--------+---------+---------+---------+---------+-------+
|符号扩展 | VPN[4] | VPN[3] | VPN[2] | VPN[1] |VPN[0]|
| | (9位) | (9位) | (9位) | (9位) | (9位)|
+--------+---------+---------+---------+---------+-------+
+------+
| off |
|(12位)|
+------+
页表级别:5 级(PGD -> P4D -> PUD -> PMD -> PTE)
Linux 内核在编译和运行时动态决定使用的页表级数:
// arch/riscv/mm/init.c,第 49–60 行
#ifdef CONFIG_64BIT
u64 satp_mode __ro_after_init =
!IS_ENABLED(CONFIG_XIP_KERNEL) ? SATP_MODE_57 : SATP_MODE_39;
bool pgtable_l4_enabled __ro_after_init = !IS_ENABLED(CONFIG_XIP_KERNEL);
bool pgtable_l5_enabled __ro_after_init = !IS_ENABLED(CONFIG_XIP_KERNEL);
#else
u64 satp_mode __ro_after_init = SATP_MODE_32;
#endif在 arch/riscv/include/asm/pgtable-64.h 中,PGDIR_SHIFT 根据运行时选择动态计算:
// arch/riscv/include/asm/pgtable-64.h,第 16–20 行
#define PGDIR_SHIFT_L3 30 // Sv39 模式:PGD 覆盖 1GB
#define PGDIR_SHIFT_L4 39 // Sv48 模式:PGD 覆盖 512GB
#define PGDIR_SHIFT_L5 48 // Sv57 模式:PGD 覆盖 256TB
#define PGDIR_SHIFT (pgtable_l5_enabled ? PGDIR_SHIFT_L5 : \
(pgtable_l4_enabled ? PGDIR_SHIFT_L4 : PGDIR_SHIFT_L3))
#define PMD_SHIFT 21 // PMD 覆盖 2MB(固定)
#define PUD_SHIFT 30 // PUD 覆盖 1GB(固定)RV64 的页表项(PTE)格式(定义于 arch/riscv/include/asm/pgtable-64.h 第 76–78 行):
| 63 | 62-61 | 60-54 | 53 10 | 9 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| N | MT | RSV | PFN(44) | RSW | D | A | G | U | X | W | R | V |
各标志位含义(定义于 arch/riscv/include/asm/pgtable-bits.h,第 11–19 行):
#define _PAGE_PRESENT (1 << 0) // V: 有效(Valid)
#define _PAGE_READ (1 << 1) // R: 可读
#define _PAGE_WRITE (1 << 2) // W: 可写
#define _PAGE_EXEC (1 << 3) // X: 可执行
#define _PAGE_USER (1 << 4) // U: 用户态可访问
#define _PAGE_GLOBAL (1 << 5) // G: 全局映射(不需要 ASID 匹配)
#define _PAGE_ACCESSED (1 << 6) // A: 已访问(硬件自动置位)
#define _PAGE_DIRTY (1 << 7) // D: 已修改(硬件自动置位)
#define _PAGE_SOFT (3 << 8) // 软件保留位(RSW)关键规则(pgtable-bits.h 第 72–76 行):
// 当 R/W/X 全为 0 时,该 PTE 是指向下一级页表的指针
// 当 R/W/X 中任意一位为 1 时,该 PTE 是叶子项(映射实际物理页)
#define _PAGE_LEAF (_PAGE_READ | _PAGE_WRITE | _PAGE_EXEC)高位扩展(Svpbmt,内存类型):
// arch/riscv/include/asm/pgtable-64.h,第 123–125 行
// [62:61] 内存类型:
// 00 = PMA 正常可缓存内存
// 01 = NC Non-cacheable,弱排序主内存
// 10 = IO Non-cacheable,强排序 I/O 内存
// 11 = Rsvd 保留
#define _PAGE_NOCACHE_SVPBMT (1UL << 61)
#define _PAGE_IO_SVPBMT (1UL << 62)在 arch/riscv/mm/init.c 中定义了核心页表:
// arch/riscv/mm/init.c,第 357–361 行
pgd_t swapper_pg_dir[PTRS_PER_PGD] __page_aligned_bss; // 内核主页表
pgd_t trampoline_pg_dir[PTRS_PER_PGD] __page_aligned_bss; // 启动跳板页表
static pte_t fixmap_pte[PTRS_PER_PTE] __page_aligned_bss; // fixmap 页表
pgd_t early_pg_dir[PTRS_PER_PGD] __initdata __aligned(PAGE_SIZE); // 早期页表struct pt_alloc_ops(pgtable.h 第 152–163 行)提供了多阶段页表分配的抽象:
struct pt_alloc_ops {
pte_t *(*get_pte_virt)(phys_addr_t pa);
phys_addr_t (*alloc_pte)(uintptr_t va);
pmd_t *(*get_pmd_virt)(phys_addr_t pa);
phys_addr_t (*alloc_pmd)(uintptr_t va);
pud_t *(*get_pud_virt)(phys_addr_t pa);
phys_addr_t (*alloc_pud)(uintptr_t va);
p4d_t *(*get_p4d_virt)(phys_addr_t pa);
phys_addr_t (*alloc_p4d)(uintptr_t va);
};根据 MMU 初始化阶段(early/fixmap/late),函数指针指向不同的实现,实现了优雅的多阶段初始化。
在 RV64 下,satp 中 ASID 字段为 16 位(最多 65536 个地址空间)。Linux 利用 ASID 避免上下文切换时的全量 TLB 刷新。sfence.vma 指令可携带 ASID 和虚拟地址参数做精细的 TLB 失效:
sfence.vma zero, zero // 刷新所有 ASID 的所有映射
sfence.vma va, asid // 精确刷新某 ASID 某地址的映射源码文件:arch/riscv/include/asm/processor.h,第 106–126 行
thread_struct 保存每个任务的 CPU 相关私有状态,在进程切换时保存/恢复:
struct thread_struct {
/* 被调用者保存的寄存器(callee-saved) */
unsigned long ra; // 返回地址
unsigned long sp; // 内核态栈指针
unsigned long s[12]; // s0-s11(s[0] 即帧指针 fp)
/* 浮点状态 */
struct __riscv_d_ext_state fstate;
unsigned long bad_cause; // 上次异常原因(调试)
unsigned long envcfg; // senvcfg CSR 值
unsigned long sum; // SR_SUM 标志备份
/* 向量扩展状态 */
u32 riscv_v_flags; // 向量上下文标志(见 processor.h)
u32 vstate_ctrl; // 向量状态控制
struct __riscv_v_ext_state vstate; // 用户向量状态
struct __riscv_v_ext_state kernel_vstate; // 内核向量状态
unsigned long align_ctl; // 非对齐访问控制
#ifdef CONFIG_SMP
bool force_icache_flush; // 迁移时需要刷新 icache
unsigned int prev_cpu; // 上次运行的 CPU
#endif
};向量标志的详细位域定义(processor.h 第 96–103 行):
#define RISCV_V_CTX_DEPTH_MASK 0x00ff0000 // 可抢占向量上下文深度
#define RISCV_V_CTX_UNIT_DEPTH 0x00010000
#define RISCV_KERNEL_MODE_V 0x00000001 // 内核正在使用向量
#define RISCV_PREEMPT_V 0x00000100 // 启用可抢占内核向量
#define RISCV_PREEMPT_V_DIRTY 0x80000000 // 可抢占向量上下文是脏的
#define RISCV_PREEMPT_V_NEED_RESTORE 0x40000000 // 需要恢复可抢占向量上下文源码文件:arch/riscv/kernel/entry.S,第 421–471 行
函数签名(注释):__switch_to(prev: a0, next: a1)
执行 __switch_to 时的完整流程:
a0 = prev task_struct
a1 = next task_struct
|
计算偏移:
a3 = a0 + TASK_THREAD_RA (prev->thread.ra 的地址)
a4 = a1 + TASK_THREAD_RA (next->thread.ra 的地址)
|
保存 prev 的被调用者寄存器:
REG_S ra, TASK_THREAD_RA_RA(a3)
REG_S sp, TASK_THREAD_SP_RA(a3)
REG_S s0, TASK_THREAD_S0_RA(a3)
... (s0-s11)
|
保存 SR_SUM(用户内存访问权限):
csrr s0, CSR_STATUS
REG_S s0, TASK_THREAD_SUM_RA(a3)
|
scs_save_current -- 保存影子调用栈指针
|
恢复 next 的 SR_SUM:
REG_L s0, TASK_THREAD_SUM_RA(a4)
li s1, SR_SUM
and s0, s0, s1
csrs CSR_STATUS, s0 -- 更新 sstatus.SUM 位
|
恢复 next 的被调用者寄存器:
REG_L ra, TASK_THREAD_RA_RA(a4)
REG_L sp, TASK_THREAD_SP_RA(a4)
... (s0-s11)
|
move tp, a1 -- tp 指向 next 的 task_struct
scs_load_current -- 加载 next 的影子调用栈
|
ret -- 返回到 next->thread.ra 指向的位置
只有被调用者保存的寄存器(ra, sp, s0–s11)需要显式保存/恢复。调用者保存的寄存器(a0–a7, t0–t6)在进入 __switch_to 前已由 C 调用约定处理。
RISC-V 采用惰性(lazy)策略管理浮点状态。sstatus.FS 字段跟踪 FPU 状态:
FS=OFF -> 任何 FP 指令触发非法指令异常 -> 内核在异常中分配/恢复状态
FS=INITIAL -> FP 寄存器已重置为初始值
FS=CLEAN -> FP 状态未变化,无需保存
FS=DIRTY -> FP 状态已变化,切换时必须保存
在 arch/riscv/kernel/process.c 的 start_thread() 中(第 144–179 行):
void start_thread(struct pt_regs *regs, unsigned long pc, unsigned long sp)
{
regs->status = SR_PIE; // 用户态开启中断
if (has_fpu()) {
regs->status |= SR_FS_INITIAL; // FPU 置为 Initial 状态
fstate_restore(current, regs); // 恢复初始 FP 状态
}
regs->epc = pc; // 设置入口 PC
regs->sp = sp; // 设置用户栈SBI(Supervisor Binary Interface)是 RISC-V 特权软件层之间的标准二进制接口。Linux 内核(S 模式)通过 ecall 指令陷入 M 模式固件(OpenSBI)来访问硬件能力。
+------------------+
| Linux Kernel (S) | <-- sbi_ecall(ext_id, fid, a0..a5)
+------------------+
| ecall(触发 EXC_SUPERVISOR_SYSCALL)
v
+------------------+
| OpenSBI (M 模式) | <-- 分发到对应扩展处理函数
+------------------+
|
v
硬件操作(计时器、IPI、reset 等)
调用约定(RISC-V SBI 规范 v2.0+):
a7 = SBI Extension ID(扩展标识符)
a6 = SBI Function ID(扩展内的函数号)
a0-a5 = 函数参数
返回值:
a0 = error code(0 = 成功,负值 = 错误)
a1 = return value(附加返回值)
源码文件:arch/riscv/include/asm/sbi.h,第 15–49 行
enum sbi_ext_id {
// Legacy 扩展(SBI v0.1,向后兼容)
SBI_EXT_0_1_SET_TIMER = 0x0,
SBI_EXT_0_1_CONSOLE_PUTCHAR = 0x1,
SBI_EXT_0_1_CONSOLE_GETCHAR = 0x2,
SBI_EXT_0_1_SEND_IPI = 0x4,
SBI_EXT_0_1_SHUTDOWN = 0x8,
// Base Extension(必须支持)
SBI_EXT_BASE = 0x10,
// 标准扩展(SBI v0.2+)
SBI_EXT_TIME = 0x54494D45, // "TIME"
SBI_EXT_IPI = 0x735049, // "sPI"
SBI_EXT_RFENCE = 0x52464E43, // "RFNC"
SBI_EXT_HSM = 0x48534D, // "HSM"(Hart State Management)
SBI_EXT_SRST = 0x53525354, // "SRST"(System Reset)
SBI_EXT_PMU = 0x504D55, // "PMU"
SBI_EXT_DBCN = 0x4442434E, // "DBCN"(Debug Console)
SBI_EXT_FWFT = 0x46574654, // "FWFT"(Firmware Features)
SBI_EXT_MPXY = 0x4D505859, // "MPXY"(Message Proxy)
SBI_EXT_DBTR = 0x44425452, // "DBTR"(Debug Triggers)
};源码文件:arch/riscv/kernel/sbi.c,第 649–709 行
void __init sbi_init(void)
{
int ret = sbi_get_spec_version(); // 探测 SBI 版本
if (ret > 0) sbi_spec_version = ret;
if (!sbi_spec_is_0_1()) { // SBI >= 0.2
// 探测并选择各扩展实现
if (sbi_probe_extension(SBI_EXT_TIME)) {
__sbi_set_timer = __sbi_set_timer_v02; // 新接口
} else {
__sbi_set_timer = __sbi_set_timer_v01; // 兼容旧接口
}
if (sbi_probe_extension(SBI_EXT_IPI)) {
__sbi_send_ipi = __sbi_send_ipi_v02;
}
if (sbi_probe_extension(SBI_EXT_RFENCE)) {
__sbi_rfence = __sbi_rfence_v02; // 支持 VMID/ASID
}
if (sbi_spec_version >= sbi_mk_version(0, 3) &&
sbi_probe_extension(SBI_EXT_SRST)) {
// 注册系统重置/关机处理
register_restart_handler(&sbi_srst_reboot_nb);
}
if (sbi_spec_version >= sbi_mk_version(2, 0) &&
sbi_probe_extension(SBI_EXT_DBCN)) {
sbi_debug_console_available = true; // 调试控制台
}
if (sbi_spec_version >= sbi_mk_version(3, 0) &&
sbi_probe_extension(SBI_EXT_FWFT)) {
sbi_fwft_supported = true; // 固件特性
}
}
}计时器设置(sbi.c 第 96–103 行):
static void __sbi_set_timer_v02(uint64_t stime_value)
{
#if __riscv_xlen == 32
// RV32 分两次传 64 位值
sbi_ecall(SBI_EXT_TIME, SBI_EXT_TIME_SET_TIMER,
stime_value, stime_value >> 32, 0, 0, 0, 0);
#else
sbi_ecall(SBI_EXT_TIME, SBI_EXT_TIME_SET_TIMER,
stime_value, 0, 0, 0, 0, 0);
#endif
}远程 TLB 刷新(sbi.c 第 406–428 行):
Linux 通过 SBI RFENCE 扩展触发多 hart 的 sfence.vma 指令:
int sbi_remote_sfence_vma_asid(const struct cpumask *cpu_mask,
unsigned long start,
unsigned long size,
unsigned long asid)
{
if (asid == FLUSH_TLB_NO_ASID)
return __sbi_rfence(SBI_EXT_RFENCE_REMOTE_SFENCE_VMA,
cpu_mask, start, size, 0, 0);
else
return __sbi_rfence(SBI_EXT_RFENCE_REMOTE_SFENCE_VMA_ASID,
cpu_mask, start, size, asid, 0);
}扩展探测(sbi.c 第 543–554 行):
long sbi_probe_extension(int extid)
{
struct sbiret ret;
ret = sbi_ecall(SBI_EXT_BASE, SBI_EXT_BASE_PROBE_EXT, extid,
0, 0, 0, 0, 0);
if (!ret.error)
return ret.value; // 非零表示支持
return 0;
}RISC-V 平台主要依赖 Device Tree Blob(DTB)进行设备发现,这与 x86(ACPI)和较旧 ARM 平台的区别明显。虽然内核代码中已开始引入 ACPI 支持(arch/riscv/kernel/setup.c 第 11 行 #include <linux/acpi.h>),但当前主流部署依然使用 DTB。
DTB 由 OpenSBI 固件在启动时传递给内核,通过寄存器 a1 传入(物理地址):
OpenSBI 调用 Linux 入口时:
a0 = boot hart ID(启动 hart 的硬件 ID)
a1 = DTB 物理地址
在 arch/riscv/kernel/head.S 第 326–331 行:
#ifdef CONFIG_BUILTIN_DTB
la a0, __dtb_start // 使用内建 DTB
XIP_FIXUP_OFFSET a0
#else
mv a0, a1 // 使用 OpenSBI 传入的 DTB 地址
#endif
call setup_vm // 带 DTB 地址调用 setup_vm在 arch/riscv/mm/init.c 中,启动内存相关:
void *_dtb_early_va __initdata; // DTB 虚拟地址(启动早期)
uintptr_t _dtb_early_pa __initdata; // DTB 物理地址/dts-v1/;
/ {
compatible = "sifive,hifive-unmatched-a00", "sifive,fu740-c000";
#address-cells = <2>;
#size-cells = <2>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "sifive,u74-mc", "riscv";
device_type = "cpu";
reg = <0>;
riscv,isa = "rv64imafdc"; // ISA 字符串
riscv,cbom-block-size = <64>; // Zicbom 块大小
riscv,cboz-block-size = <64>;
mmu-type = "riscv,sv39"; // 页表模式
// PLIC 中断控制器连接
interrupt-controller {
compatible = "riscv,cpu-intc";
#interrupt-cells = <1>;
};
};
};
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x2 0x0>; // 8GB RAM
};
soc {
plic: interrupt-controller@c000000 {
compatible = "sifive,plic-1.0.0";
reg = <0x0 0xc000000 0x0 0x4000000>;
riscv,ndev = <186>;
};
clint@2000000 {
compatible = "sifive,clint0";
reg = <0x0 0x2000000 0x0 0x10000>;
interrupts-extended = <&cpu0_intc 3 &cpu0_intc 7>;
};
};
};
+------------------+
| ROM / Bootloader | (U-Boot SPL, etc.)
+------------------+
|
v
+------------------+
| OpenSBI (M 模式) | 加载于 DRAM,建立 M 模式运行环境
| fw_dynamic 等 | 初始化 PMP、CLINT、控制台
+------------------+
| 跳转到 Linux 内核入口(S 模式)
| a0=hartid, a1=DTB 物理地址
v
+------------------+
| Linux _start | arch/riscv/kernel/head.S:22
| (S 模式) |
+------------------+
| _start_kernel (head.S:217)
| 1. 屏蔽所有中断(csrw CSR_IE/CSR_IP, zero)
| 2. 关闭 FPU/Vector(防止内核误用)
| 3. 多核竞争启动(hart_lottery amoadd)
| 4. 初始化 BSS 段
| 5. 调用 setup_vm(建立早期页表)
| 6. relocate_enable_mmu(开启 MMU,切换到虚拟地址)
| 7. 设置异常向量(csrw CSR_TVEC, handle_exception)
| 8. 跳转到 start_kernel()
v
+------------------+
| start_kernel() | init/main.c
+------------------+
|
| setup_arch() -- 架构初始化
| |- parse_dtb() -- 解析 DTB
| |- setup_bootmem() -- 初始化内存块(memblock)
| |- paging_init() -- 建立完整页表
| |- sbi_init() -- 探测 SBI 功能
|
| trap_init() -- 注册异常处理
| mm_init() -- 内存管理初始化
| sched_init() -- 调度器初始化
| ...
v
+------------------+
| init 进程 / Shell |
+------------------+
在 arch/riscv/kernel/head.S 第 75–126 行,relocate_enable_mmu 的关键代码:
relocate_enable_mmu:
la a1, kernel_map
REG_L a1, KERNEL_MAP_VIRT_ADDR(a1) // 虚拟地址基址
la a2, _start
sub a1, a1, a2 // 计算 VA-PA 偏移
// 更新 stvec 指向虚拟地址(防止切换时陷阱找不到处理器)
la a2, 1f
add a2, a2, a1
csrw CSR_TVEC, a2
// 计算 satp:PPN | MODE
srl a2, a0, PAGE_SHIFT // a0=页表物理地址 >> PAGE_SHIFT = PPN
la a1, satp_mode
REG_L a1, 0(a1) // 读取 MODE(Sv39/Sv48/Sv57)
or a2, a2, a1 // 合并 PPN 和 MODE
// 先加载跳板页表(trampoline_pg_dir)
la a0, trampoline_pg_dir
srl a0, a0, PAGE_SHIFT
or a0, a0, a1
sfence.vma // 确保之前的页表写入可见
csrw CSR_SATP, a0 // 写入 satp,MMU 开始工作
1: csrw CSR_SATP, a2 // 切换到最终内核页表
sfence.vma // 使新映射生效
ret主核(boot hart)通过 SBI HSM 扩展(sbi.h 第 79–84 行)唤醒从核:
SBI_EXT_HSM_HART_START = 0 // 启动指定 hart
SBI_EXT_HSM_HART_STOP = 1 // 停止当前 hart
SBI_EXT_HSM_HART_STATUS = 2 // 查询 hart 状态
SBI_EXT_HSM_HART_SUSPEND = 3 // 挂起当前 hart(WFI)从核在 secondary_start_sbi(head.S 第 129–188 行)中完成:
- 屏蔽中断
- 加载全局指针
- 读取栈和任务结构指针
- 调用
relocate_enable_mmu开启 MMU - 设置异常向量
- 调用
smp_callin()注册到调度器
CLINT 是 RISC-V 最基础的中断控制器,提供:
- msip:软件中断(Machine Software Interrupt,用于 IPI)
- mtimecmp:计时器比较寄存器
- mtime:系统计时器
内存映射地址(典型值):
0x02000000: CLINT 基址
+0x0000: msip[0] -- Hart 0 软件中断
+0x0004: msip[1] -- Hart 1 软件中断
...
+0x4000: mtimecmp[0] -- Hart 0 计时器比较值(64 位)
+0x4008: mtimecmp[1] -- Hart 1 计时器比较值
...
+0xBFF8: mtime -- 全局计时器(64 位,所有 hart 共享)
在 S 模式下,Linux 通过 SBI SBI_EXT_TIME 扩展间接操作计时器:
// arch/riscv/kernel/sbi.c,第 182–187 行
static void __sbi_set_timer_v02(uint64_t stime_value)
{
sbi_ecall(SBI_EXT_TIME, SBI_EXT_TIME_SET_TIMER,
stime_value, 0, 0, 0, 0, 0);
}PLIC 是 RISC-V 平台的全局外部中断控制器,支持多 source、多 target(hart context)的中断路由:
外设中断源(最多 1023 个)
IRQ 1..N
|
v
+------------------+
| PLIC |
| 优先级、掩码管理 |
| 每 hart 的阈值 |
+------------------+
| IRQ_S_EXT(cause=9)
v
Linux S 模式中断处理器(do_irq)
|
v
读取 PLIC Claim 寄存器(获取中断号)
|
v
调用对应驱动 IRQ 处理函数
|
v
写入 PLIC Complete 寄存器(结束中断)
AIA 是 RISC-V 新一代中断架构,解决 PLIC 在 SMP 场景下的局限性:
主要组件:
- IMSIC(Incoming MSI Controller):每 hart 一个,支持 MSI 中断
- APLIC(Advanced PLIC):替代 PLIC,支持转发到 IMSIC
相关 CSR(定义于 csr.h 第 347–358 行):
// AIA Supervisor-Level CSR
#define CSR_SISELECT 0x150 // 间接寄存器选择
#define CSR_SIREG 0x151 // 间接寄存器访问
#define CSR_STOPEI 0x15c // 顶级外部中断操作
#define CSR_STOPI 0xdb0 // 顶级挂起中断信息
// AIA 中断原因
#define IRQ_PMU_OVF 13 // PMU 计数器溢出(AIA 扩展)RISC-V Vector Extension(RVV)是 RISC-V 的 SIMD 扩展,其设计哲学是"可变长度向量"而非固定宽度(如 x86 的 SSE/AVX):
硬件实现的 VLEN(向量寄存器位宽)可为 128-65536 位
软件通过 vsetvl 指令获取实际可用向量长度 VL
32 个向量寄存器(v0-v31),每个 VLEN 位
关键向量 CSR(定义于 csr.h 第 455–469 行):
#define CSR_VSTART 0x008 // 向量起始元素索引
#define CSR_VCSR 0x00f // 向量控制状态(vxsat+vxrm)
#define CSR_VL 0xc20 // 当前向量长度
#define CSR_VTYPE 0xc21 // 向量类型寄存器
#define CSR_VLENB 0xc22 // 向量寄存器字节数(VLEN/8)
// vtype 字段定义
#define VTYPE_VLMUL _AC(7, UL) // 寄存器分组倍率
#define VTYPE_VSEW_SHIFT 3
#define VTYPE_VSEW (_AC(7, UL) << 3) // 每个元素宽度(SEW)
#define VTYPE_VTA_SHIFT 6
#define VTYPE_VTA (_AC(1, UL) << 6) // 尾部不活跃处理
#define VTYPE_VMA_SHIFT 7
#define VTYPE_VMA (_AC(1, UL) << 7) // 掩码不活跃处理
#define VTYPE_VILL_SHIFT (__riscv_xlen - 1)
#define VTYPE_VILL (_AC(1, UL) << VTYPE_VILL_SHIFT) // 非法配置源码文件:arch/riscv/include/asm/vector.h
sstatus.VS 字段(csr.h 第 36–46 行):
#define SR_VS _AC(0x00000600, UL)
#define SR_VS_OFF _AC(0x00000000, UL) // 向量不可用
#define SR_VS_INITIAL _AC(0x00000200, UL) // 初始状态
#define SR_VS_CLEAN _AC(0x00000400, UL) // 已同步,无变化
#define SR_VS_DIRTY _AC(0x00000600, UL) // 已变化,需要保存向量状态操作函数(vector.h 第 83–106 行):
// 将 sstatus.VS 设置为 CLEAN 状态(已保存,无变化)
static inline void __riscv_v_vstate_clean(struct pt_regs *regs) {
regs->status = __riscv_v_vstate_or(regs->status, CLEAN);
}
// 将 sstatus.VS 设置为 OFF(禁用向量,节省切换开销)
static inline void riscv_v_vstate_off(struct pt_regs *regs) {
regs->status = __riscv_v_vstate_or(regs->status, OFF);
}
// 在内核中开启向量访问
static __always_inline void riscv_v_enable(void) {
csr_set(CSR_SSTATUS, SR_VS);
}源码文件:arch/riscv/kernel/vector.c,第 32–62 行
int riscv_v_setup_vsize(void)
{
unsigned long this_vsize;
// 如果 DTB 中有 thead,vlenb 属性,使用 DTB 提供的值
if (thead_vlenb_of) {
riscv_v_vsize = thead_vlenb_of * 32;
return 0;
}
// 否则通过 CSR 探测:开启向量,读 VLENB,再关闭
riscv_v_enable();
this_vsize = csr_read(CSR_VLENB) * 32; // VLENB * 32 个寄存器
riscv_v_disable();
if (!riscv_v_vsize) {
riscv_v_vsize = this_vsize;
return 0;
}
// SMP 下要求所有核的 VLEN 相同
if (riscv_v_vsize != this_vsize) {
WARN(1, "RISCV_ISA_V only supports one vlenb on SMP systems");
return -EOPNOTSUPP;
}
return 0;
}vector.c 第 64–145 行实现了向量上下文的 slab 缓存管理:
void __init riscv_v_setup_ctx_cache(void)
{
if (!(has_vector() || has_xtheadvector())) return;
// 为用户向量上下文创建专用 slab(支持 usercopy)
riscv_v_user_cachep = kmem_cache_create_usercopy(
"riscv_vector_ctx",
riscv_v_vsize, // 对象大小 = VLENB * 32
16, // 对齐 16 字节
SLAB_PANIC,
0, riscv_v_vsize, // usercopy 范围
NULL);
// 为内核可抢占向量上下文创建 slab(CONFIG_RISCV_ISA_V_PREEMPTIVE)
riscv_v_kernel_cachep = kmem_cache_create(
"riscv_vector_kctx",
riscv_v_vsize, 16, SLAB_PANIC, NULL);
}__riscv_v_ext_state 结构(struct thread_struct 的成员 vstate):
// 每个任务有两份向量状态:
thread.vstate -- 用户向量状态(heap 分配,datap 指针)
thread.kernel_vstate -- 内核可抢占向量状态(CONFIG_RISCV_ISA_V_PREEMPTIVE)在 vector.h 第 129–136 行,__vstate_csr_save 负责保存向量 CSR:
static __always_inline void __vstate_csr_save(struct __riscv_v_ext_state *dest)
{
asm volatile (
"csrr %0, " __stringify(CSR_VSTART) "\n\t"
"csrr %1, " __stringify(CSR_VTYPE) "\n\t"
"csrr %2, " __stringify(CSR_VL) "\n\t"
: "=r" (dest->vstart), "=r" (dest->vtype), "=r" (dest->vl),
"=r" (dest->vcsr) : :);
// ... T-Head 扩展特殊处理(xtheadvector)
}向量寄存器(v0–v31)的实际数据通过 vstore 类指令保存到 dest->datap 指向的内存区域。
当用户程序首次使用向量指令时(VS=OFF),触发非法指令异常,内核在 riscv_v_first_use_handler() 中:
- 检测异常是否由向量指令引发(通过
insn_is_vector(),vector.c 第 81–112 行) - 为该任务分配向量状态内存(
kmem_cache_alloc) - 将
sstatus.VS设置为INITIAL - 从异常返回,重新执行该向量指令
Zicbom(Cache Block Management Operations)扩展提供三条缓存块操作指令:
| 指令 | 操作 | 说明 |
|---|---|---|
cbo.clean rs1 |
Clean | 将脏缓存行写回内存,但保留在 cache 中 |
cbo.flush rs1 |
Flush | 写回并从 cache 中驱逐 |
cbo.inval rs1 |
Invalidate | 使缓存行无效(不写回,适合 DMA 后的失效) |
源码文件:arch/riscv/mm/cacheflush.c,第 111–140 行
unsigned int riscv_cbom_block_size; // Clean/Flush 块大小
unsigned int riscv_cboz_block_size; // Zero 块大小(Zicboz 扩展)
unsigned int riscv_cbop_block_size; // Prefetch 块大小(Zicbop 扩展)
void __init riscv_init_cbo_blocksizes(void)
{
// 从每个 CPU 节点的 DTB 属性读取:
// riscv,cbom-block-size
// riscv,cboz-block-size
// riscv,cbop-block-size
for_each_of_cpu_node(node) {
cbo_get_block_size(node, "riscv,cbom-block-size",
&cbom_block_size, &cbom_hartid);
cbo_get_block_size(node, "riscv,cboz-block-size",
&cboz_block_size, &cboz_hartid);
cbo_get_block_size(node, "riscv,cbop-block-size",
&cbop_block_size, &cbop_hartid);
}
// 同一系统中所有 hart 的块大小必须一致,否则发出警告
}Zicbom 操作的用户态访问权限通过 senvcfg(Supervisor Environment Configuration)CSR 控制:
// arch/riscv/include/asm/csr.h,第 222–231 行
#define ENVCFG_CBZE (_AC(1, UL) << 7) // 允许用户使用 cbo.zero
#define ENVCFG_CBCFE (_AC(1, UL) << 6) // 允许用户使用 cbo.clean/flush
#define ENVCFG_CBIE_SHIFT 4
#define ENVCFG_CBIE (_AC(0x3, UL) << 4) // cbo.inval 行为控制
#define ENVCFG_CBIE_ILL _AC(0x0, UL) // 用户 inval => 非法指令
#define ENVCFG_CBIE_FLUSH _AC(0x1, UL) // 用户 inval => flush
#define ENVCFG_CBIE_INV _AC(0x3, UL) // 用户 inval => 真正 invalidate在非一致性(non-coherent)DMA 平台上,Zicbom 指令用于维护 DMA 操作前后的缓存一致性:
DMA 写入前(CPU → 外设):
cbo.clean 地址 // 确保内存包含最新数据
DMA 读取后(外设 → CPU):
cbo.inval 地址 // 使旧 cache 行无效,强制从内存读取新数据
基于 arch/riscv/include/asm/pgtable.h 和 arch/riscv/mm/init.c 中的 print_vm_layout() 函数:
地址空间(Sv39,512GB 用户 + 512GB 内核)
0xFFFFFFFFFFFFFFFF
|
| [内核空间,最高 2GB]
| Kernel image, BPF JIT
|
0xFFFFFFFF80000000 <- KERNEL_LINK_ADDR (ADDRESS_SPACE_END - SZ_2G + 1)
|
| [模块区域]
| MODULES_VADDR .. MODULES_END
| (kernel_start - 2GB .. kernel_start)
|
| [KASAN Shadow(如配置)]
|
| [直接映射(lowmem)]
| PAGE_OFFSET .. PAGE_OFFSET + RAM_SIZE
| 物理内存线性映射
|
| [vmalloc 区域]
| VMALLOC_START .. VMALLOC_END
| (内核虚拟地址空间的 1/4)
|
| [vmemmap]
| struct page 数组的虚拟映射
|
| [PCI I/O]
| PCI_IO_START .. PCI_IO_END (16MB)
|
| [fixmap]
| FIXADDR_START .. FIXADDR_TOP
| 固定虚拟地址(DTB、fixmap PTE 等)
|
0x0000003FFFFFFFFF <- 用户空间顶端(Sv39)
|
| [用户空间]
| 0 .. TASK_SIZE
|
0x0000000000000000
pgtable.h 中各区域的计算(第 41–111 行):
// 直接映射区大小:PGD 条目数的一半 * 每 PGD 覆盖大小 / 2
#define KERN_VIRT_SIZE ((PTRS_PER_PGD / 2 * PGDIR_SIZE) / 2)
// vmalloc 区占用内核虚拟空间的一半
#define VMALLOC_SIZE (KERN_VIRT_SIZE >> 1)
#define VMALLOC_END PAGE_OFFSET
#define VMALLOC_START (PAGE_OFFSET - VMALLOC_SIZE)
// vmemmap:页帧号到 struct page 的映射
#define VMEMMAP_SHIFT (VA_BITS - PAGE_SHIFT - 1 + STRUCT_PAGE_MAX_SHIFT)
#define VMEMMAP_SIZE BIT(VMEMMAP_SHIFT)
#define VMEMMAP_END VMALLOC_START
#define VMEMMAP_START (VMALLOC_START - VMEMMAP_SIZE)
// PCI I/O:16MB
#define PCI_IO_SIZE SZ_16M
#define PCI_IO_END VMEMMAP_START
#define PCI_IO_START (PCI_IO_END - PCI_IO_SIZE)
// fixmap(固定虚拟地址,DTB + PTE 映射)
#define FIXADDR_TOP PCI_IO_START
#define FIXADDR_SIZE (PMD_SIZE + FIX_FDT_SIZE) // 64 位
#define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE)VA_BITS 由运行时页表模式决定(pgtable.h 第 78–79 行):
#define VA_BITS (pgtable_l5_enabled ? VA_BITS_SV57 : \
(pgtable_l4_enabled ? VA_BITS_SV48 : VA_BITS_SV39))内核在 arch/riscv/mm/init.c 第 40 行声明:
u64 new_vmalloc[NR_CPUS / sizeof(u64) + 1];该位图配合 entry.S 第 23–93 行的 new_vmalloc_check 宏,实现了无需 sfence.vma 的惰性 vmalloc 映射传播(在支持 Svvptc 扩展的硬件上通过 ALTERNATIVE 优化为 nop)。
+=====================================================================+
| RISC-V Linux 内核架构全局视图 |
+=====================================================================+
用户进程 内核 固件(M 模式)
+---------+ +----------+ +------------+
| App | ecall | do_irq | ecall | OpenSBI |
| U 模式 | ------> | 异常分发 | -----------> | SBI 服务 |
+---------+ sret<-- | handle_ | sret<--- +------------+
| exception|
+----------+
|
+------+------+
| |
+----------+ +-----------+
| 进程调度 | | 内存管理 |
| __switch | | Sv39/48/ |
| _to() | | Sv57 MMU |
| 保存: | | satp 切换 |
| ra,sp,s0- | | ASID 管理 |
| s11,VS | +-----------+
+----------+
|
+----------+ +-----------+ +----------+
| FPU/向量 | | 中断控制 | | 缓存管理 |
| 惰性保存 | | PLIC/CLINT| | Zicbom |
| FS/VS 位 | | /AIA | | cbom/cboz|
+----------+ +-----------+ +----------+
关键设计决策:
1. CSR_SCRATCH 双用途:内核态=0,用户态=task_struct 指针
2. 只保存被调用者寄存器(ra/sp/s0-s11),减少切换开销
3. FPU/Vector 惰性保存:FS/VS 字段追踪脏状态
4. SBI 屏蔽 M 模式硬件差异,提供统一接口(计时器/IPI/reset)
5. DTB 为唯一设备发现机制(无 ACPI,RISC-V 原生)
6. 页表模式运行时选择(Sv39/Sv48/Sv57),非编译时固化
7. satp.ASID 减少上下文切换时的 TLB 刷新范围
| 功能 | 文件路径 |
|---|---|
| 异常入口汇编 | arch/riscv/kernel/entry.S |
| 进程切换 | arch/riscv/kernel/process.c |
__switch_to |
arch/riscv/kernel/entry.S(第 421 行) |
| CSR 宏定义 | arch/riscv/include/asm/csr.h |
| 页表定义 | arch/riscv/include/asm/pgtable.h |
| 页表 64 位扩展 | arch/riscv/include/asm/pgtable-64.h |
| 页表标志位 | arch/riscv/include/asm/pgtable-bits.h |
| MMU 初始化 | arch/riscv/mm/init.c |
| 缓存/Zicbom | arch/riscv/mm/cacheflush.c |
| SBI 接口 | arch/riscv/kernel/sbi.c |
| SBI 定义头文件 | arch/riscv/include/asm/sbi.h |
| 线程结构体 | arch/riscv/include/asm/processor.h(第 106 行) |
| 启动汇编 | arch/riscv/kernel/head.S |
| 架构初始化 | arch/riscv/kernel/setup.c |
| 向量扩展头文件 | arch/riscv/include/asm/vector.h |
| 向量扩展实现 | arch/riscv/kernel/vector.c |
arch/riscv/kernel/head.S 是 RISC-V Linux 内核第一个被执行的代码文件,负责从裸机状态建立 C 运行环境。
内核镜像开头包含一个紧凑的元数据头,引导程序(OpenSBI、U-Boot)通过它识别并加载 Linux:
arch/riscv/kernel/head.S:40
_start:
/* jump to start_kernel() */
j _start_kernel /* 跳过头部,直达启动代码 */
/* EFI PE/COFF 头:为 UEFI 引导预留 */
...
.word RISCV_HEADER_VERSION /* 头版本号 */
.word 0 /* 保留 */
.dword _kernel_offset /* 加载偏移(相对于 RAM 起始) */
.dword _end - _start /* 镜像有效尺寸 */
.dword _kernel_flags /* flags: LE=0, PE=1, 保留位 */
.dword 0, 0, 0 /* 保留 */
.ascii RISCV_IMAGE_MAGIC /* "RISCV\0\0\0" */
.ascii RISCV_IMAGE_MAGIC2 /* "RSC\x05" */
.word 0 /* 保留 */
这套格式兼容 ARM64 的引导协议(Image 头),使得同一套引导逻辑可以处理多种架构。
arch/riscv/kernel/head.S:~90
以下是 _start_kernel 中各步骤的详细说明:
_start_kernel:
┌─────────────────────────────────────────────────────────┐
│ 步骤 1:关闭 S 模式中断 (csrw CSR_SIE, zero) │
│ 防止启动过程中发生意外中断 │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────┐
│ 步骤 2:初始化 gp(全局指针)寄存器 │
│ .option push / .option norelax │
│ la gp, __global_pointer$ │
│ .option pop │
│ gp 用于 GP 相对寻址,编译器优化小数据访问效率 │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────┐
│ 步骤 3:Hart 彩票竞选 —— 只有一个 hart 执行初始化 │
│ la a3, hart_lottery │
│ li a2, 1 │
│ amoadd.w a3, a2, (a3) /* 原子加 1,读旧值 */ │
│ bnez a3, .Lsecondary_start /* 非 0 则为从核 */ │
│ 利用 AMO(Atomic Memory Operation)确保只有一个 hart │
│ 通过,其余 hart 转入从核等待流程 │
└───────────────────────┬─────────────────────────────────┘
│(主 hart 继续)
┌───────────────────────▼─────────────────────────────────┐
│ 步骤 4:清零 BSS 段 │
│ la a3, __bss_start │
│ la a4, __bss_stop │
│ 1: REG_S zero, 0(a3) /* 写零 */ │
│ add a3, a3, RISCV_SZPTR │
│ blt a3, a4, 1b /* 循环至 BSS 结束 */ │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────┐
│ 步骤 5:建立早期页表并开启 MMU │
│ call setup_vm /* arch/riscv/mm/init.c */ │
│ call relocate_enable_mmu │
│ 此后 PC 进入虚拟地址空间运行 │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────┐
│ 步骤 6:设置内核栈和 tp 寄存器 │
│ la tp, init_task /* 指向 init 进程 */ │
│ la sp, init_thread_union + THREAD_SIZE │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────┐
│ 步骤 7:跳转到 C 代码 │
│ call soc_early_init /* 平台早期初始化(可选)*/ │
│ tail start_kernel /* 进入 init/main.c */ │
└─────────────────────────────────────────────────────────┘
arch/riscv/kernel/head.S
这是启动过程中最关键的一步,涉及从物理地址到虚拟地址的切换:
relocate_enable_mmu:
/* 1. 计算虚拟地址与物理地址的偏移量 */
la a2, kernel_map
XIP_FIXUP_OFFSET a2
REG_L a0, KERN_VIRT_ADDR(a2) /* 加载虚拟起始地址 */
/* 2. 构造 satp 值(模式 | ASID=0 | PPN) */
la a1, swapper_pg_dir
srl a1, a1, PAGE_SHIFT /* 转换为页帧号 */
or a1, a1, a0 /* 加上模式位 */
/* 3. 将返回地址调整为虚拟地址(关键!) */
la a2, .Lrelocated
sub a2, a2, a0 /* 计算偏移 */
add ra, ra, a0 /* 修正 ra 为虚拟地址 */
/* 4. 开启 MMU:写 satp 并 sfence.vma */
csrw CSR_SATP, a1
sfence.vma
/* 此指令执行后,PC 仍在物理地址,但 ra 已是虚拟地址 */
/* 下一条 ret 后跳转到虚拟地址空间 */
ret这里的精妙之处在于:csrw CSR_SATP 和紧随的 ret 之间存在一个"混合态"——当前指令流在物理地址运行,但 ret 后将跳到虚拟地址。这依赖于早期页表(swapper_pg_dir)中同时包含物理地址映射(等值映射)和虚拟地址映射,确保跳转时 TLB 可以翻译新地址。
arch/riscv/kernel/head.S: .Lsecondary_start
非主 hart 在 amoadd 后跳转到此处,根据配置走两条路:
从核启动路径:
┌───────────────────────────────────────┐
│ CONFIG_SMP │
│ │
│ 方式 A:SBI HSM(推荐) │
│ secondary_start_sbi: │
│ - 接收 hartid(a0) │
│ - 开启 MMU(复用主核页表) │
│ - 设置独立内核栈 │
│ - 进入 smp_callin() │
│ │
│ 方式 B:SPINWAIT(自旋等待) │
│ secondary_start_common: │
│ - 轮询 __cpu_up_stack_pointer[] │
│ - 主核写入后解除等待 │
│ - 直接进入 smp_callin() │
└───────────────────────────────────────┘
HSM 方式(Hart State Management)通过 SBI ecall 启动从核,是更现代的方式。SPINWAIT 方式用于不支持 SBI HSM 的老平台。
当启用 CONFIG_XIP_KERNEL 时,内核代码直接从 ROM/Flash 执行,数据段复制到 RAM:
XIP_FIXUP_OFFSET 宏:在 XIP 模式下修正地址偏移
.macro XIP_FIXUP_OFFSET reg
#ifdef CONFIG_XIP_KERNEL
REG_L \reg, _xip_fixup
add \reg, \reg, \reg
#endif
.endm
SBI 是 RISC-V 平台中 S 模式(Linux)与 M 模式固件(OpenSBI)之间的标准化接口,类似于 x86 的 BIOS/UEFI 中断服务。
arch/riscv/kernel/sbi.c:29struct sbiret sbi_ecall(int ext, int fid, unsigned long arg0,
unsigned long arg1, unsigned long arg2,
unsigned long arg3, unsigned long arg4,
unsigned long arg5)
{
struct sbiret ret;
register uintptr_t a0 asm("a0") = (uintptr_t)arg0;
register uintptr_t a1 asm("a1") = (uintptr_t)arg1;
register uintptr_t a2 asm("a2") = (uintptr_t)arg2;
register uintptr_t a3 asm("a3") = (uintptr_t)arg3;
register uintptr_t a4 asm("a4") = (uintptr_t)arg4;
register uintptr_t a5 asm("a5") = (uintptr_t)arg5;
register uintptr_t a6 asm("a6") = (uintptr_t)fid;
register uintptr_t a7 asm("a7") = (uintptr_t)ext;
asm volatile("ecall"
: "+r"(a0), "+r"(a1)
: "r"(a2), "r"(a3), "r"(a4), "r"(a5), "r"(a6), "r"(a7)
: "memory");
ret.error = a0;
ret.value = a1;
return ret;
}调用约定:
a7:扩展 ID(Extension ID,EID)a6:功能 ID(Function ID,FID)a0-a5:参数- 返回:
a0= error code,a1= value
arch/riscv/kernel/sbi.c:~600 sbi_init()
Linux 内核维护对多个 SBI 规范版本的兼容:
SBI 版本演进:
┌────────────────────────────────────────────────────────────┐
│ SBI v0.1(旧版) │
│ - 无版本探测机制(SBI_EXT_BASE 不存在) │
│ - 扩展:IPI、TIMER、RFENCE、SRST(通过 legacy EID) │
│ - EID 空间:0x00-0x08(单功能 ID) │
│ - 已废弃,但 Linux 仍支持检测和回退 │
├────────────────────────────────────────────────────────────┤
│ SBI v0.2 │
│ - 引入 BASE 扩展(EID=0x10):版本探测、能力查询 │
│ - 新扩展格式:EID + FID 二维编址 │
│ - HSM 扩展(EID=0x48534D):hart 状态管理 │
│ - RFENCE 扩展(EID=0x52464E43):远程 fence │
├────────────────────────────────────────────────────────────┤
│ SBI v1.0 │
│ - 正式规范,SRST 扩展(EID=0x53525354):系统重启 │
│ - PMU 扩展(EID=0x504D55):性能监控 │
├────────────────────────────────────────────────────────────┤
│ SBI v2.0/v3.0(最新) │
│ - NACL 扩展(嵌套加速) │
│ - STA 扩展(窃取时间统计) │
│ - DBCN 扩展(调试控制台) │
└────────────────────────────────────────────────────────────┘
arch/riscv/kernel/sbi.c:~640
void __init sbi_init(void)
{
int ret;
sbi_set_power_off(); /* 注册 machine_power_off 回调 */
ret = sbi_get_spec_version();
if (ret == SBI_ERR_NOT_SUPPORTED) {
/* v0.1 固件:无 BASE 扩展 */
__sbi_set_timer = __sbi_set_timer_v01;
__sbi_send_ipi = __sbi_send_ipi_v01;
__sbi_rfence = __sbi_rfence_v01;
sbi_spec_version = SBI_SPEC_VERSION_DEFAULT; /* 0.1 */
goto out;
}
/* v0.2+ 固件 */
sbi_spec_version = ret;
if (sbi_probe_extension(SBI_EXT_SRST) > 0) {
/* 注册 SRST 关机/重启处理 */
}
if (sbi_probe_extension(SBI_EXT_HSM) > 0) {
/* 注册 HSM 从核启动函数 */
sbi_cpuhp_setup();
}
...
out:
pr_info("SBI specification v%lu.%lu detected\n",
sbi_major_version(), sbi_minor_version());
}RISC-V 的 sfence.vma 只刷新本地 hart 的 TLB,跨核 TLB 刷新必须通过 SBI RFENCE 扩展(或旧版 IPI 机制):
/* 远程 sfence.vma(刷新指定 hart 集合的 TLB) */
sbi_remote_sfence_vma(cpumask, start, size)
-> sbi_ecall(SBI_EXT_RFENCE, SBI_EXT_RFENCE_REMOTE_SFENCE_VMA,
hmask, hbase, start, size, 0, 0)
/* ASID 相关 TLB 刷新 */
sbi_remote_sfence_vma_asid(cpumask, start, size, asid)
-> SBI_EXT_RFENCE_REMOTE_SFENCE_VMA_ASID
/* I-Cache 同步(修改代码后) */
sbi_remote_fence_i(cpumask)
-> SBI_EXT_RFENCE_REMOTE_FENCE_ISBI HSM(Hart State Management)扩展管理每个 hart 的生命周期:
HSM Hart 状态机:
STOPPED ─────────────────► STARTED
│ sbi_hart_start() │
│ │
│◄───────────────────────────┘
│ sbi_hart_stop()
│
│ sbi_hart_suspend()
▼
SUSPENDED ─────────────► STARTED
resume event
(timer/IPI)
状态枚举:
SBI_HSM_STATE_STARTED = 0
SBI_HSM_STATE_STOPPED = 1
SBI_HSM_STATE_START_PEND = 2
SBI_HSM_STATE_STOP_PEND = 3
SBI_HSM_STATE_SUSPENDED = 4
SBI_HSM_STATE_SUSP_PEND = 5
SBI_HSM_STATE_RESU_PEND = 6
CPU 热插拔(cpuhp)框架在下线时调用 sbi_hart_stop(),新核上线时通过 sbi_hart_start() 传递启动地址和上下文参数。
RISC-V 特权模式切换通过以下机制实现:
模式切换触发条件:
┌──────────────────────────────────────────────────────┐
│ 升级(低权限 → 高权限) │
│ ecall : 同步 —— 主动请求(系统调用/SBI 调用) │
│ 异常 : 同步 —— 非法指令/访问违规/断点等 │
│ 中断 : 异步 —— 外部中断/计时器中断/软件中断 │
│ │
│ 降级(高权限 → 低权限) │
│ sret : 从 S 模式返回(必须设置 sstatus.SPP) │
│ mret : 从 M 模式返回 │
└──────────────────────────────────────────────────────┘
M 模式通过委托寄存器决定哪些中断/异常交由 S 模式处理:
M 模式委托给 S 模式(OpenSBI 在启动时配置):
mideleg(中断委托):
Bit 1 (SSI) : S 模式软件中断 ✓ 委托
Bit 5 (STI) : S 模式计时器中断 ✓ 委托
Bit 9 (SEI) : S 模式外部中断 ✓ 委托
medeleg(异常委托):
Bit 0 : 指令地址未对齐 ✓
Bit 1 : 指令访问错误 ✓
Bit 2 : 非法指令 ✓
Bit 3 : 断点(ebreak) ✓
Bit 4 : 加载地址未对齐 ✓
Bit 5 : 加载访问错误 ✓
Bit 6 : 存储/AMO 地址未对齐 ✓
Bit 7 : 存储/AMO 访问错误 ✓
Bit 8 : U 模式 ecall ✓(系统调用)
Bit 12 : 指令页错误 ✓
Bit 13 : 加载页错误 ✓
Bit 15 : 存储/AMO 页错误 ✓
未委托(M 模式保留):
Bit 9 : S 模式 ecall —— OpenSBI 自己处理 SBI 调用
Bit 11 : M 模式 ecall
sstatus 位域(64 位):
63 31 19 18 17 16 15-14 13-12 8 7 5 1 0
+-------+---+---+---+---+---+------+------+----+----+----+----+
| SD | |MXR|SUM| | | XS | FS |SPP |SPIE| |SIE |
+-------+---+---+---+---+---+------+------+----+----+----+----+
字段说明:
SIE (bit 1) : S 模式全局中断使能
SPIE (bit 5) : 陷阱发生前的 SIE 值(用于 sret 恢复)
SPP (bit 8) : 陷阱前的特权级别(0=U, 1=S)
FS (bit 13-14): 浮点状态(0=OFF, 1=INITIAL, 2=CLEAN, 3=DIRTY)
XS (bit 15-16): 自定义扩展状态
SUM (bit 18) : Supervisor User Memory access(内核访问用户页)
MXR (bit 19) : Make eXecutable Readable(可执行页可读)
SD (bit 63) : 状态脏位(FS 或 XS 非零时为 1)
SUM 位的使用:内核通过 uaccess_enable()/uaccess_disable() 开关 SUM 位,控制内核是否可以直接访问用户空间内存页。默认关闭以防止内核误读用户数据。
CSR 地址 [11:10] 编码访问权限:
00 : 可读可写(RW)
01 : 可读可写(RW)
10 : 可读可写(RW)
11 : 只读(RO)
CSR 地址 [9:8] 编码最低访问特权:
00 : U 模式(用户态可访问)
01 : S 模式
10 : HS 模式(虚拟化扩展)
11 : M 模式
示例:
0x300 (mstatus) : [11:10]=00 RW, [9:8]=11 M 模式 —— Linux 无法直接访问
0x100 (sstatus) : [11:10]=00 RW, [9:8]=01 S 模式 —— Linux 可以读写
0xC00 (cycle) : [11:10]=11 RO, [9:8]=00 U 模式 —— 用户态可读
当实现 Hypervisor 扩展时,增加 VS 模式(Virtual Supervisor)和 VU 模式(Virtual User),形成二级嵌套:
┌─────────────────────────────────┐
│ M 模式 (OpenSBI) │
└──────────────┬──────────────────┘
│ mret
┌──────────────▼──────────────────┐
│ HS 模式(Host S,Linux KVM) │
│ hstatus, hgatp 等 H 模式 CSR │
└──────────────┬──────────────────┘
┌────────┴──────────┐
┌─────▼──────┐ ┌───────▼──────┐
│ VS 模式 │ │ S 模式进程 │
│(Guest OS)│ │(普通内核线程)│
└─────┬──────┘ └──────────────┘
┌─────▼──────┐
│ VU 模式 │
│(Guest 用户)│
└────────────┘
RISC-V 硬件 PTW 在 TLB 缺失时自动遍历页表(全部在物理地址空间):
Sv39 三级页表遍历(39 位虚拟地址):
虚拟地址 [38:0]:
[38:30] = VPN[2](9 位,第一级索引)
[29:21] = VPN[1](9 位,第二级索引)
[20:12] = VPN[0](9 位,第三级索引)
[11:0] = 页内偏移(12 位)
遍历步骤:
1. a = satp.PPN << 12 /* 一级页表基地址 */
2. pte = *(a + VPN[2] * 8) /* 读一级 PTE */
3. if (pte.V==0) 页错误
if (pte.R==1 || pte.X==1) 是叶节点(1GB 超页)
4. a = pte.PPN << 12
pte = *(a + VPN[1] * 8) /* 读二级 PTE */
5. if 叶节点 → 2MB 超页
6. a = pte.PPN << 12
pte = *(a + VPN[0] * 8) /* 读三级 PTE */
7. 4KB 普通页
页大小层级:
Sv39: 4KB | 2MB | 1GB
Sv48: 4KB | 2MB | 1GB | 512GB
Sv57: 4KB | 2MB | 1GB | 512GB | 256TB
超页 PTE 判断条件(硬件规范):
- 非叶 PTE:V=1, R=0, W=0, X=0
- 叶 PTE:V=1 且 (R=1 或 X=1)
- 中间级 PTE 的 PPN 低位必须为 0(未对齐 = 错误)
Linux 内核使用 THP(Transparent Huge Page)支持运行时超页分配,
PMD 级别(2MB)是主要目标。
RISC-V PTE 的 A(Accessed)和 D(Dirty)位有两种更新模式:
模式 A:硬件自动更新(推荐)
- 硬件 PTW 在访问页时置 A,写入时置 D
- Linux 通过检测 D 位实现脏页跟踪
- 通过 menvcfg.ADUE 位使能
模式 B:软件管理(A/D=0 时触发页错误)
- 每次首次访问都陷入 S 模式处理
- Linux 在页错误处理程序中置 A/D 位
- 性能较差,但可以精确追踪访问模式
Linux 内核:
arch/riscv/include/asm/pgtable.h
pte_young() / pte_dirty() 检查 A/D 位
ptep_clear_young() / ptep_clear_flush() 清除 A 位
satp.ASID(Address Space Identifier)减少上下文切换时的 TLB 刷新:
satp(64 位):
[63:60] : MODE(8=Sv39, 9=Sv48, 10=Sv57)
[59:44] : ASID(16 位,硬件实际位数可能更少)
[43:0] : PPN(物理页帧号,即根页表地址)
ASID 分配策略(arch/riscv/mm/context.c):
- 全局 ASID 版本号 asid_generation
- 每个 mm_struct 保存 context.id(含版本号)
- 版本号耗尽时(ASID 回绕):全局 TLB 刷新
- 软件管理 ASID 分配,避免重用冲突
sfence.vma 变体:
sfence.vma : 刷新当前 hart 全部 TLB
sfence.vma x0, x0 : 同上(x0=0 表示全部地址空间/ASID)
sfence.vma ra, x0 : 刷新特定地址(全 ASID)
sfence.vma x0, t0 : 刷新特定 ASID(全地址)
sfence.vma ra, t0 : 刷新特定地址+特定 ASID(最精确)
阶段 1:head.S 早期固定映射(setup_vm)
- 使用静态数组 early_pg_dir / early_pmd
- 映射:内核代码+数据(物理等值映射 + 虚拟地址映射)
- MMU 开启后可访问内核符号
阶段 2:setup_vm_final()(在 start_kernel 中)
- 建立完整的直接映射(fixmap、vmalloc 区域)
- 建立 swapper_pg_dir(最终内核根页表)
- 迁移至 swapper_pg_dir
阶段 3:paging_init()
- 调用 memblock → page allocator 迁移
- 建立全部物理内存的 linear map
- 启用内核页表隔离(如果配置了)
arch/riscv/kernel/traps.c 包含从 entry.S 汇编入口分发后的 C 语言陷阱处理逻辑。
arch/riscv/kernel/traps.c:~40该宏批量定义常见异常的处理函数:
#define DO_ERROR_INFO(name, signo, code, str) \
asmlinkage __visible __trap_section void name( \
struct pt_regs *regs) \
{ \
do_trap_error(regs, signo, code, regs->epc, \
"Oops - " str); \
}
/* 展开后生成以下函数: */
DO_ERROR_INFO(do_trap_insn_misaligned, SIGBUS, BUS_ADRALN, "instruction address misaligned")
DO_ERROR_INFO(do_trap_insn_access, SIGBUS, BUS_ADRERR, "instruction access fault")
DO_ERROR_INFO(do_trap_load_misaligned, SIGBUS, BUS_ADRALN, "load address misaligned")
DO_ERROR_INFO(do_trap_load_access, SIGBUS, BUS_ADRERR, "load access fault")
DO_ERROR_INFO(do_trap_store_misaligned,SIGBUS, BUS_ADRALN, "store (or AMO) address misaligned")
DO_ERROR_INFO(do_trap_store_access, SIGBUS, BUS_ADRERR, "store (or AMO) access fault")
DO_ERROR_INFO(do_trap_ecall_s, SIGILL, ILL_ILLTRP, "environment call from S-mode")
DO_ERROR_INFO(do_trap_ecall_m, SIGILL, ILL_ILLTRP, "environment call from M-mode")arch/riscv/kernel/traps.c: do_trap_insn_illegal()非法指令异常(cause=2)是 RISC-V 中最复杂的陷阱之一,因为它既处理真正的非法指令,也处理"扩展首次使用":
asmlinkage __visible __trap_section void do_trap_insn_illegal(struct pt_regs *regs)
{
bool handled;
if (user_mode(regs)) {
/* 先尝试内核模拟 */
local_irq_enable();
handled = riscv_v_first_use_handler(regs); /* RVV 首次使用? */
if (!handled)
handled = riscv_fpu_first_use_handler(regs); /* FPU 首次使用? */
if (!handled)
do_trap_error(regs, SIGILL, ILL_ILLOPC, regs->epc,
"Oops - illegal instruction");
} else {
/* 内核态非法指令 = 内核 bug,直接 die() */
die(regs, "Oops - illegal instruction");
}
}"首次使用"机制:
- 进程启动时
sstatus.VS = OFF(向量禁用) - 第一次执行向量指令 → 触发非法指令异常
riscv_v_first_use_handler()分配向量寄存器上下文,设置VS = INITIAL- 返回后重新执行该向量指令,正常完成
arch/riscv/kernel/traps.c: do_trap_break()ebreak 指令(cause=3)触发断点异常:
asmlinkage __visible __trap_section void do_trap_break(struct pt_regs *regs)
{
#ifdef CONFIG_KPROBES
if (kprobe_single_step_handler(regs))
return;
if (kprobe_breakpoint_handler(regs))
return;
#endif
#ifdef CONFIG_UPROBES
if (uprobe_single_step_handler(regs))
return;
#endif
#ifdef CONFIG_GENERIC_BUG
if (!user_mode(regs)) {
enum bug_trap_type type = report_bug(regs->epc, regs);
if (type == BUG_TRAP_TYPE_WARN) {
regs->epc += get_break_insn_length(regs->epc);
return;
}
}
#endif
force_sig_fault(SIGTRAP, TRAP_BRKPT, (void __user *)regs->epc);
}ebreak 的指令大小可能是 2 字节(C.EBREAK 压缩指令)或 4 字节,get_break_insn_length() 通过检查指令编码判断。
arch/riscv/kernel/traps.c: do_trap_ecall_u()U 模式 ecall(cause=8)是系统调用的统一入口:
asmlinkage __visible __trap_section void do_trap_ecall_u(struct pt_regs *regs)
{
if (user_mode(regs)) {
long syscall = regs->a7; /* a7 = 系统调用号 */
ulong addr;
regs->epc += 4; /* 跳过 ecall 指令 */
regs->orig_a0 = regs->a0; /* 保存原始参数 */
riscv_v_vstate_discard(regs); /* 丢弃向量状态(安全边界) */
syscall = syscall_enter_from_user_mode(regs, syscall);
if (syscall >= 0 && syscall < NR_SYSCALLS)
syscall_handler(regs, syscall); /* 分发到系统调用表 */
else
syscall_set_return_value(current, regs, -ENOSYS, 0);
syscall_exit_to_user_mode(regs);
} else {
irq_enter_rcu();
do_trap_error(regs, SIGILL, ILL_ILLTRP, regs->epc,
"environment call from S-mode");
irq_exit_rcu();
}
}arch/riscv/kernel/traps.c: do_trap_software_check()当启用 CFI(Control Flow Integrity)时,非法的间接跳转会触发 cause=18(Software Check):
asmlinkage __visible __trap_section void do_trap_software_check(struct pt_regs *regs)
{
if (insn_is_cfi_trap(regs->epc)) {
handle_cfi_failure(regs); /* 报告 CFI 违规 */
return;
}
die(regs, "Oops - software check");
}arch/riscv/kernel/traps.c: die()void die(struct pt_regs *regs, const char *str)
{
static int die_counter;
int ret;
oops_enter();
spin_lock_irq(&die_lock);
console_verbose();
bust_spinlocks(1);
pr_emerg("%s [#%d]\n", str, ++die_counter);
print_modules();
show_regs(regs); /* 打印寄存器状态 */
if (regs && kexec_should_crash(current))
crash_kexec(regs); /* 触发 kdump */
bust_spinlocks(0);
ret = notify_die(DIE_OOPS, str, regs, 0,
trap_no(regs), SIGSEGV);
if (ret != NOTIFY_STOP) {
if (!regs || !user_mode(regs))
panic("Fatal exception"); /* 内核态 = 直接 panic */
}
oops_end(flags, regs, SIGKILL);
}arch/riscv/kernel/traps.c: handle_bad_stack()RISC-V 内核通过影子调用栈(Shadow Call Stack)和栈金丝雀(Stack Canary)检测栈溢出。当检测到时,切换到预分配的溢出栈执行处理:
asmlinkage void handle_bad_stack(struct pt_regs *regs)
{
unsigned long tsk_stk = (unsigned long)current->stack;
unsigned long ovf_stk = (unsigned long)this_cpu_ptr(overflow_stack);
console_verbose();
pr_emerg("Insufficient stack space to handle exception!\n");
pr_emerg("Task stack: [0x%016lx..0x%016lx]\n",
tsk_stk, tsk_stk + THREAD_SIZE);
pr_emerg("Overflow stack: [0x%016lx..0x%016lx]\n",
ovf_stk, ovf_stk + OVERFLOW_STACK_SIZE);
dump_backtrace(regs, current, KERN_EMERG);
panic("Kernel stack overflow");
}arch/riscv/include/asm/hwcap.h:1Linux 使用一个大型位图跟踪所有已知 RISC-V 扩展:
/* 单字母扩展:0-25('a'=0, 'b'=1, ..., 'z'=25)*/
#define RISCV_ISA_EXT_a 0 /* Atomic */
#define RISCV_ISA_EXT_b 1 /* Bit manipulation */
#define RISCV_ISA_EXT_c 2 /* Compressed */
#define RISCV_ISA_EXT_d 3 /* Double FP */
#define RISCV_ISA_EXT_f 5 /* Single FP */
#define RISCV_ISA_EXT_h 7 /* Hypervisor */
#define RISCV_ISA_EXT_i 8 /* Integer base */
#define RISCV_ISA_EXT_m 12 /* Multiply/Divide */
#define RISCV_ISA_EXT_s 18 /* Supervisor */
#define RISCV_ISA_EXT_u 20 /* User mode */
#define RISCV_ISA_EXT_v 21 /* Vector */
/* 多字母扩展:从 26 开始 */
#define RISCV_ISA_EXT_SMAIA 26
#define RISCV_ISA_EXT_SMSTATEEN 27
#define RISCV_ISA_EXT_SSAIA 28
/* ... */
#define RISCV_ISA_EXT_SSTC 46 /* Supervisor Timer */
#define RISCV_ISA_EXT_SVINVAL 52
#define RISCV_ISA_EXT_SVNAPOT 53
#define RISCV_ISA_EXT_MAX 128 /* 位图大小 */arch/riscv/kernel/cpufeature.c:~35/* 所有 hart 的 ISA 扩展交集(内核运行时使用) */
struct riscv_isa_ext_data riscv_isa[BITS_TO_LONGS(RISCV_ISA_EXT_MAX)];
/* 每个 hart 独立的 ISA 扩展位图 */
static struct riscv_hart_id_isa {
unsigned long isa[BITS_TO_LONGS(RISCV_ISA_EXT_MAX)];
} hart_isa[NR_CPUS];访问宏:
riscv_isa_extension_available(NULL, EXT) /* 检查全局位图 */
riscv_isa_extension_available(isa, EXT) /* 检查特定 hart 位图 */
/* 高效热路径(使用静态分支) */
riscv_has_extension_likely(ext) /* 使用 static_branch_likely */
riscv_has_extension_unlikely(ext) /* 使用 static_branch_unlikely */arch/riscv/kernel/cpufeature.c: riscv_fill_hwcap()
DTB 中 riscv,isa 属性包含 ISA 字符串,格式如 rv64imafdcvh_zicsr_zifencei_sstc:
解析步骤:
1. 读 DTB "riscv,isa-extensions" 属性(新格式)
或 "riscv,isa" 属性(旧格式字符串)
2. 字符串解析:
- 匹配 "rv32" / "rv64" 前缀
- 扫描单字母扩展:imafdqclbjtpvnhx...
- 扫描下划线分隔的多字母扩展:_zicsr_zifencei...
- 版本号处理:忽略 "2p0" 等版本后缀
3. 对每个识别到的扩展调用 validate 函数:
- 检查依赖关系(如 D 依赖 F)
- 检查内核编译支持(如 V 需要 CONFIG_RISCV_ISA_V)
- 平台特定黑名单(如 T-Head 的部分扩展)
4. 写入 hart_isa[cpu] 位图
5. 遍历所有 hart,取交集写入全局 riscv_isa[]
arch/riscv/kernel/cpufeature.c每个扩展可以注册自定义验证函数:
/* Sstc 扩展:需要固件支持 */
static int riscv_ext_sstc_validate(const struct riscv_isa_ext_data *data,
const unsigned long *isa_bitmap)
{
if (!riscv_isa_extension_available(isa_bitmap, RISCV_ISA_EXT_i))
return -EOPNOTSUPP;
/* Sstc 需要 S 模式计时器 CSR(CSR_STIMECMP)*/
if (!sbi_probe_extension(SBI_EXT_TIME))
/* 没有 SBI TIME 扩展则无法使用直接 Sstc */
;
return 0;
}
/* V 扩展:需要编译支持 */
static int riscv_ext_v_validate(const struct riscv_isa_ext_data *data,
const unsigned long *isa_bitmap)
{
if (!IS_ENABLED(CONFIG_RISCV_ISA_V))
return -EOPNOTSUPP;
return 0;
}T-Head(平头哥)的 C906/C910 芯片在早期固件中使用非标准 ISA 字符串,内核需要特殊兼容:
arch/riscv/kernel/cpufeature.c
/* T-Head 厂商扩展标记:xtheadc, xtheadvector 等 */
/* 某些 T-Head 扩展与标准扩展存在语义差异,
需要在使能时额外设置 CSR 配置位 */
/* thead,mvendorid + thead,marchid 组合用于识别 T-Head hart */
static const struct riscv_isa_vendor_ext_data_list * const
riscv_vendor_ext_lists[] = {
&riscv_isa_vendor_ext_list_thead,
...
};arch/riscv/kernel/cpufeature.c: riscv_fill_hwcap()最终结果通过 AT_HWCAP 导出给用户空间:
elf_hwcap = 0;
for (i = 0; i < RISCV_ISA_EXT_MAX; i++) {
if (__riscv_isa_extension_available(riscv_isa, i)) {
if (i < RISCV_ISA_EXT_MAX_HWCAP_BIT)
elf_hwcap |= BIT(i); /* 设置 HWCAP 对应位 */
}
}
/* 用户空间通过 getauxval(AT_HWCAP) 查询 */drivers/irqchip/irq-sifive-plic.c:~40PLIC 内存空间布局(基地址 = PLIC_BASE,通常 0xC000000):
+---------------------------+ 0x000000
| 中断优先级寄存器 | 每个中断源 4 字节
| IRQ[1].priority | 0x000004
| IRQ[2].priority | 0x000008
| ... |
| IRQ[1023].priority | 0x000FFC
+---------------------------+ 0x001000
| 中断待处理位图(只读) | 128 字节(1024 位)
| pending[0] | 0x001000
| ... |
+---------------------------+ 0x002000
| 上下文使能寄存器 | 每上下文 128 字节
| ctx[0].enable[0] | 0x002000 (Hart 0 M 模式)
| ctx[0].enable[1..31] |
| ctx[1].enable[0..31] | 0x002080 (Hart 0 S 模式)
| ctx[N].enable[0..31] | 0x002000 + N*0x80
+---------------------------+ 0x200000
| 上下文阈值和认领寄存器 | 每上下文 0x1000 字节
| ctx[0].threshold | 0x200000
| ctx[0].claim | 0x200004
| ctx[1].threshold | 0x201000
| ctx[1].claim | 0x201004
+---------------------------+
Linux 宏定义(irq-sifive-plic.c):
PRIORITY_BASE = 0x000000
CONTEXT_ENABLE_BASE = 0x002000
CONTEXT_ENABLE_SIZE = 0x000080
CONTEXT_BASE = 0x200000
CONTEXT_SIZE = 0x001000
CONTEXT_THRESHOLD = 0x000000(相对于 CONTEXT_BASE+ctx*CONTEXT_SIZE)
CONTEXT_CLAIM = 0x000004
完整中断处理路径:
外设产生中断
│
▼
PLIC 接收中断,优先级 > 阈值?
│ YES
▼
PLIC 向 Hart 的 S 模式发出外部中断信号
(拉高 Hart 的 SEIP 位)
│
▼
Hart 执行完当前指令后进入 entry.S::handle_exception
cause = CAUSE_IRQ_FLAG | IRQ_S_EXT (9)
│
▼
riscv_intc_irq() → generic_handle_domain_irq(intc_domain, 9)
│
▼
INTC 域处理 IRQ 9 → 调用 PLIC 链式处理程序
plic_handle_irq()
│
▼
读取 claim 寄存器(原子操作)
claim = readl(handler->hart_base + CONTEXT_CLAIM)
│
├── claim == 0?没有待处理中断,返回
│
▼
generic_handle_domain_irq(plic->irqdomain, claim)
│ 分发到具体外设驱动的 irq_handler
▼
外设驱动 ISR 执行
│
▼
写回 claim(EOI)
writel(claim, handler->hart_base + CONTEXT_CLAIM)
drivers/irqchip/irq-sifive-plic.c: __plic_toggle()static void __plic_toggle(void __iomem *enable_base, int hwirq, int enable)
{
u32 __iomem *reg = enable_base + (hwirq / 32) * sizeof(u32);
u32 hwirq_mask = 1 << (hwirq % 32);
if (enable)
writel(readl(reg) | hwirq_mask, reg);
else
writel(readl(reg) & ~hwirq_mask, reg);
}注意:readl/writel 是 32 位的,使能寄存器按每个上下文 128 字节(32 个 32 位寄存器)组织,覆盖 1024 个中断源。
drivers/irqchip/irq-sifive-plic.c
struct plic_priv {
struct cpumask lmask; /* 本 PLIC 关联的 CPU 掩码 */
struct irq_domain *irqdomain;
void __iomem *regs;
unsigned long plic_quirks;
unsigned int nr_irqs;
unsigned long *prio_save;
};
struct plic_handler {
bool present; /* 是否有效 */
void __iomem *hart_base; /* 本 Hart 的 ctx 基地址 */
raw_spinlock_t enable_lock; /* 保护使能位图 */
void __iomem *enable_base; /* 本 Hart 的使能寄存器基地址 */
u32 *enable_save; /* 节电模式保存 */
struct plic_priv *priv;
};
/* 每 CPU 变量,每个 Hart 有独立的处理器状态 */
static DEFINE_PER_CPU(struct plic_handler, plic_handlers);IRQ 域层次(两层结构):
riscv_intc_irq_domain(INTC)
hwirq 1 = SSI(S 模式软件中断)
hwirq 5 = STI(S 模式计时器中断)
hwirq 9 = SEI(S 模式外部中断)← PLIC 连接到此
plic_irqdomain(PLIC)
hwirq 1-1023 = 各外设中断源
parent = riscv_intc_irq_domain
parent hwirq = 9(SEI)
virq(Linux 虚拟 IRQ)
= irq_create_mapping(plic_irqdomain, hwirq)
外设驱动使用此 virq 调用 request_irq()
时钟相关寄存器层次:
M 模式(CLINT 硬件):
mtime : 全局单调计数器(64 位,所有 hart 共享)
mtimecmp : 每 hart 比较值(M 模式计时器,不直接给 Linux 用)
S 模式(Linux 使用):
time CSR : mtime 的只读别名(S 模式可读)
通过 SBI 设置 mtimecmp(触发 S 模式计时器中断)
Sstc 扩展(现代路径):
stimecmp : S 模式直接设置的比较值(不经 SBI)
stimecmph : RV32 上的高 32 位寄存器
drivers/clocksource/timer-riscv.cLinux 时钟框架需要两类抽象:
clocksource(计时源):
riscv_clocksource:
.read = riscv_clocksource_rdtime → get_cycles64()
.rating = 400 (高质量)
.flags = CLOCK_SOURCE_IS_CONTINUOUS
.vdso_clock_mode = VDSO_CLOCKMODE_ARCHTIMER (支持用户态 VDSO)
get_cycles64() 实现:
#ifdef CONFIG_64BIT
return csr_read(CSR_TIME); /* 64 位直接读 */
#else
/* RV32:先读高位,再读低位,再读高位确认无进位 */
do {
hi = csr_read(CSR_TIMEH);
lo = csr_read(CSR_TIME);
} while (hi != csr_read(CSR_TIMEH));
return ((u64)hi << 32) | lo;
#endif
clockevent(时钟事件):
riscv_clock_event(per_cpu):
.set_next_event = riscv_clock_next_event
.rating = 100(基础路径)
= 450(Sstc 路径)
.features = CLOCK_EVT_FEAT_ONESHOT
set_next_event 实现(两条路径):
Sstc 路径:csr_write(CSR_STIMECMP, next_tval) /* 直接写 CSR */
SBI 路径:sbi_set_timer(next_tval) /* ecall 到 OpenSBI */
性能比较(每次计时器设置的开销):
SBI 路径(旧):
ecall → M 模式陷阱 → OpenSBI 处理 → 写 mtimecmp → mret
约 100-500 ns(取决于实现)
Sstc 路径(新):
csr_write(CSR_STIMECMP, val)
约 1-5 ns(单条 CSR 写指令)
差异:约 100 倍性能提升(特别是高频率 tick 场景)
drivers/clocksource/timer-riscv.c: riscv_timer_init_common()初始化步骤:
1. 获取 INTC fwnode(riscv_get_intc_hwnode())
2. 在 INTC 域中创建计时器 IRQ 映射
riscv_clock_event_irq = irq_create_mapping(domain, RV_IRQ_TIMER)
RV_IRQ_TIMER = 5(S 模式计时器中断,hwirq=5)
3. 注册 clocksource:clocksource_register_hz(&riscv_clocksource, freq)
4. 注册调度时钟:sched_clock_register(riscv_sched_clock, 64, freq)
5. 注册 per-CPU IRQ:request_percpu_irq(riscv_clock_event_irq, ...)
6. 检测 Sstc:如果可用,启用 static_branch 并提升 rating
7. 注册 CPU 热插拔回调:
cpuhp_setup_state(CPUHP_AP_RISCV_TIMER_STARTING,
riscv_timer_starting_cpu,
riscv_timer_dying_cpu)
drivers/clocksource/timer-riscv.c: riscv_clock_next_event()
#if defined(CONFIG_32BIT)
/* 写高 32 位时先把低位置最大(防止中途触发) */
csr_write(CSR_STIMECMP, ULONG_MAX); /* 防止竞态 */
csr_write(CSR_STIMECMPH, next_tval >> 32);
csr_write(CSR_STIMECMP, next_tval & 0xFFFFFFFF);
#else
csr_write(CSR_STIMECMP, next_tval);
#endif这种三步写法防止了 64 位值写入过程中意外触发中断:先写低位为全 1 确保不提前触发,再写高位,最后更新低位。
RISC-V Vector Extension(RVV)是 RISC-V 的官方 SIMD 扩展,v1.0 规范已于 2021 年批准:
RVV 关键特性:
- 向量寄存器:v0-v31,共 32 个
- 向量长度:可配置(VLEN),最小 128 位,典型值 128/256/512/1024 位
- 元素宽度(SEW):8/16/32/64 位动态可选
- LMUL(寄存器分组):1/2/4/8,多个寄存器合并为逻辑向量
- 掩码寄存器:v0 专用于谓词操作
- vl(vector length)/ vtype CSR:控制当前向量操作配置
读取 VLEN(字节数):
vlenb = csr_read(CSR_VLENB) /* 硬件向量长度字节数 */
sstatus.VS 字段控制向量上下文状态(2 位):
OFF (00):向量禁用,访问 v0-v31 或 vcsr 触发非法指令异常
INITIAL (01):向量已使能,内容未定义(首次使用)
CLEAN (10):向量已保存且未修改
DIRTY (11):向量寄存器已被修改(需要在上下文切换时保存)
状态转换:
┌─────────────────────────────────────────────────────┐
│ │
▼ │
OFF ──[riscv_v_vstate_on()]──► INITIAL ──[写向量寄存器]──► DIRTY
▲ │
│ │ 上下文切换
│ [保存 v0-v31]
│ │
└────────── CLEAN ◄──────┘
[恢复 v0-v31]
arch/riscv/kernel/vector.c向量上下文通过内联汇编保存到 thread_struct.vstate:
/* 向量状态结构 */
struct __riscv_v_ext_state {
unsigned long vstart; /* 部分执行的起始元素 */
unsigned long vl; /* 当前向量长度 */
unsigned long vtype; /* 向量类型(SEW/LMUL) */
unsigned long vcsr; /* 向量控制状态 */
unsigned long vlenb; /* 向量字节长度(只读) */
void *datap; /* 指向 v0-v31 保存区(动态分配)*/
};
/* 保存向量寄存器 */
void __riscv_v_vstate_save(struct __riscv_v_ext_state *vstate,
void *datap)
{
/* 1. 保存 vcsr CSR */
vstate->vstart = csr_read(CSR_VSTART);
vstate->vl = csr_read(CSR_VL);
vstate->vtype = csr_read(CSR_VTYPE);
vstate->vcsr = csr_read(CSR_VCSR);
/* 2. 用 vs1r.v 指令保存 v0-v31(汇编循环)*/
__vstate_save(vstate, datap);
/* 3. 置 VS = CLEAN */
riscv_v_vstate_set_restore(task, regs);
}内核代码也可以使用向量指令(用于 memcpy 优化等),但必须明确标记:
/* 内核使用向量的正确方式 */
void some_kernel_function(void *dst, const void *src, size_t len)
{
kernel_vector_begin(); /* 保存当前进程向量状态,开启内核向量 */
/* ... 执行向量指令 ... */
kernel_vector_end(); /* 恢复用户向量状态 */
}
/* kernel_vector_begin() 实现要点:
1. 如果当前任务有向量上下文(VS != OFF),先保存
2. 设置 VS = INITIAL(使能向量)
3. 禁用抢占(向量状态不允许被打断保存到不同栈上)
*/由于 VLEN 是硬件决定的运行时值(不同板子可能不同),向量寄存器保存区在进程创建时动态分配:
arch/riscv/kernel/vector.c
int riscv_v_setup_vsize(void)
{
/* 读取硬件 VLEN */
riscv_v_vsize = csr_read(CSR_VLENB) * 32; /* 32 个向量寄存器 */
return 0;
}
/* fork 时延迟分配:首次使用向量时才分配 */
int riscv_v_first_use_handler(struct pt_regs *regs)
{
struct __riscv_v_ext_state *vstate = ¤t->thread.vstate;
if (!vstate->datap) {
vstate->datap = kzalloc(riscv_v_vsize, GFP_KERNEL);
if (!vstate->datap)
return 0;
}
/* 初始化向量状态,重新执行触发异常的指令 */
riscv_v_vstate_on(current, regs);
return 1; /* 已处理,重试指令 */
}使用 fallthrough 标记的长向量操作可以在内核抢占点被中断:
/* 向量操作中的可抢占检查点 */
void riscv_v_vstate_restore(struct task_struct *task, struct pt_regs *regs)
{
if (!(regs->status & SR_VS))
return;
/* 如果向量状态是 CLEAN,无需恢复(寄存器内容仍有效)*/
if (riscv_v_vstate_query(regs) == RISCV_V_CTX_STATE_CLEAN)
return;
__riscv_v_vstate_restore(&task->thread.vstate, task->thread.vstate.datap);
riscv_v_vstate_mark_saved(task); /* 标记为 CLEAN */
}PMP(Physical Memory Protection)是 RISC-V M 模式提供的内存访问控制机制,在没有虚拟内存时(如早期启动阶段)或 M 模式代码中限制低特权级别对物理内存的访问。
在 Linux 的使用场景中,PMP 主要由 OpenSBI 固件配置,Linux 本身不直接操作 PMP。
PMP 配置(最多 16 个条目,物理机可能更少):
pmpcfg0 ... pmpcfg3(RV64:每个寄存器包含 8 个条目的配置字节)
pmpaddr0 ... pmpaddr15(每个条目一个地址寄存器)
pmpcfg 每字节格式:
Bit 7 : L 锁定位(置 1 后仅 machine reset 可清除)
Bit 6-5 : 保留
Bit 4-3 : A 地址匹配模式
00 = DISABLED(关闭)
01 = TOR(Top Of Range,与上一条目形成范围)
10 = NA4(4 字节自然对齐)
11 = NAPOT(自然对齐幂次大小)
Bit 2 : X 可执行
Bit 1 : W 可写
Bit 0 : R 可读
NAPOT 地址编码(最常用):
pmpaddr 格式:aaaa...aaaa0...01...1
尾部连续 1 的个数 + 3 = log2(区域大小字节数)
示例:
pmpaddr = 0x...FFFF → 尾部 16 个 1 → 2^(16+3) = 512KB 区域
pmpaddr = 0x1FFFFFFF → 尾部 29 个 1 → 2^(32) = 4GB 区域
OpenSBI 在将控制权交给 Linux 之前设置 PMP:
典型 OpenSBI PMP 配置:
条目 0:保护 OpenSBI 固件自身
pmpcfg0[0] = L | NAPOT | R(没有W,X) 锁定只读
pmpaddr0 = opensbi_base >> 2 | napot_bits
条目 1:允许 Linux(S 模式)访问其余内存
pmpcfg0[1] = NAPOT | R | W | X 读/写/执行
pmpaddr1 = 0x3FFFFFFFFFFFFFF 全部物理地址空间
锁定位(L)的作用:
- 即使 M 模式也无法更改该条目(直到 reset)
- 阻止 S 模式代码(即 Linux)修改 PMP 绕过保护
- 可在 M 模式 ecall 处理中临时提权
Linux 内核通常不直接配置 PMP,但在以下场景中会感知 PMP 的存在:
/* PMP 违规会产生 load/store access fault 异常 */
/* 异常原因码 5 = load access fault */
/* 异常原因码 7 = store/AMO access fault */
/* Linux 的处理路径:
1. 访问受 PMP 保护的区域
2. 触发 access fault 异常(cause=5 或 7)
3. do_page_fault() 被调用
4. 无法通过页表修复 → 发送 SIGBUS 给进程
或内核态:直接 oops
*/Smepmp 扩展(Machine Smepmp)通过 mseccfg.MML 位增强 PMP 语义:
MML=1 时的 PMP 规则变化:
- PMP 条目同时指定 M 模式权限(bit[7]=L 变为不同含义)
- L=1, RWX: 该条目只适用 M 模式,S/U 模式无权限
- L=0, RWX: 该条目适用 S/U 模式,M 模式无该权限
- 可实现 M 模式的内存隔离(对抗固件漏洞攻击)
Linux 内核检测路径:
arch/riscv/include/asm/csr.h
CSR_MSECCFG = 0x747
MSECCFG_MML = BIT(0)
MSECCFG_MMWP = BIT(1) /* M 模式写保护 */
MSECCFG_RLB = BIT(2) /* 规则锁定旁路(测试用)*/
Alternative 机制允许内核在启动时根据当前 CPU 特性将通用代码替换为优化代码,无需在每次执行时做运行时判断:
arch/riscv/include/asm/alternative.h
arch/riscv/kernel/alternative.c/* 每个 alternative 补丁点的元数据结构 */
struct alt_entry {
void *old_ptr; /* 要替换的原始代码地址 */
void *alt_ptr; /* 替换用的新代码地址 */
unsigned long vendor_id;/* 厂商 ID(0 = 标准 RISC-V) */
unsigned long alt_len; /* 新代码长度 */
unsigned int spec_idx; /* 扩展索引(如 RISCV_ISA_EXT_SSTC)*/
unsigned int old_len; /* 原始代码长度 */
};所有补丁点由链接器收集到 .alternative section,内核启动时批量处理。
/* 在汇编代码中插入 alternative */
ALTERNATIVE("csrr %0, CSR_TIME", /* 原始代码:通过 SBI 读时间 */
"rdtime %0", /* 替代代码:直接读 time CSR(Zicntr) */
0, /* vendor_id=0(标准 ISA) */
RISCV_ISA_EXT_ZICNTR, /* 需要 Zicntr 扩展 */
CONFIG_RISCV_ISA_ZICNTR)
/* C 代码中的 alternative */
asm(ALTERNATIVE("nop\nnop", "sfence.vma", 0, RISCV_ISA_EXT_SVNAPOT, 1)
::: "memory");Alternative 处理阶段:
阶段 1:early(start_kernel 之前)
apply_early_alternatives()
处理启动关键路径上的 alternative(如 MMU 操作)
阶段 2:SMP 启动后
apply_alternatives_all()
遍历 .alternative section,对每个条目:
1. 检查 vendor_id 和扩展可用性
2. 如果扩展可用:将 alt_ptr 处的代码复制到 old_ptr
memcpy(old_ptr, alt_ptr, alt_len)
如果 alt_len < old_len:用 NOP 填充剩余字节
3. 执行 flush_icache_range() 同步 I-Cache
Alternative 机制也用于针对特定 CPU 实现的 Errata(勘误)修复:
/* 厂商 ID 用于识别需要修复的 CPU */
ALTERNATIVE_2(
"nop", /* 默认:无操作 */
".insn r 0x73, 0, 0, x0, x0, x0", /* SiFive errata 修复 */
SIFIVE_VENDOR_ID, RISCV_ISA_EXT_ID_ZICBOM, ...,
"fence rw, rw", /* T-Head errata 修复 */
THEAD_VENDOR_ID, RISCV_ISA_EXT_THEAD_CMO, ...
)RISC-V 使用 hart ID(硬件线程 ID)标识物理核,Linux 使用 CPU 编号(0-based)作为逻辑编号:
arch/riscv/include/asm/smp.h
/* hart ID → CPU 编号 */
int riscv_hartid_to_cpuid(unsigned long hartid);
/* CPU 编号 → hart ID */
unsigned long cpuid_to_hartid_map(int cpu);
/* 实现:简单的线性数组查找 */
unsigned long __cpuid_to_hartid_map[NR_CPUS] __ro_after_init = {
[0 ... NR_CPUS-1] = INVALID_HARTID
};Hart ID 不一定连续,也不一定从 0 开始(例如:hart 0 可能是 M 模式专用核,Linux 从 hart 1 开始使用)。
SMP 启动序列:
主核(CPU 0)
│
├── start_kernel() → smp_prepare_cpus() → smp_init()
│
├── 对每个从核调用 cpu_up(cpu)
│ │
│ ├── 方式 A:SBI HSM
│ │ sbi_hart_start(hartid, secondary_start_sbi, ctx)
│ │ ctx = {satp, sp, tp, ...}
│ │
│ └── 方式 B:SPINWAIT
│ 写入 __cpu_up_stack_pointer[cpu] = 从核栈地址
│ 写入 __cpu_up_task_pointer[cpu] = 从核 task 地址
│
└── 等待从核就绪(smp_callin_map 位被置 1)
从核(CPU N)
│
├── 方式 A:OpenSBI 唤醒,跳转到 secondary_start_sbi
│ 方式 B:从自旋中检测到栈/任务指针被写入
│
├── 开启 MMU(设置 satp)
├── 设置 sp、tp 寄存器
├── 设置陷阱向量(csrw stvec, trap_vector)
├── 开启中断委托
└── 跳入 smp_callin() → cpu_startup_entry()
RISC-V IPI 通过 SBI 或 CLINT 实现:
arch/riscv/kernel/smp.c
/* 发送 IPI 到指定 CPU 集合 */
void arch_send_call_function_ipi_mask(const struct cpumask *mask)
{
sbi_send_ipi(cpumask_bits(mask));
/* 或:通过 SBI IPI 扩展(EID=0x735049)*/
}
/* IPI 处理(在接收 hart 上执行) */
static irqreturn_t handle_IPI(int irq, void *dev)
{
unsigned long ops = atomic_long_xchg(&ipi_data[smp_processor_id()].bits, 0);
while (ops) {
unsigned long which = __ffs(ops);
ops &= ops - 1; /* 清除最低位 */
switch (which) {
case IPI_RESCHEDULE:
scheduler_ipi();
break;
case IPI_CALL_FUNC:
generic_smp_call_function_interrupt();
break;
case IPI_CPU_STOP:
/* 停止本 CPU */
ipi_cpu_stop(NULL);
break;
case IPI_CPU_CRASH_STOP:
ipi_cpu_crash_stop(smp_processor_id(), get_irq_regs());
break;
}
}
return IRQ_HANDLED;
}RISC-V 支持 Linux CPU 热插拔框架,允许运行时关闭/开启 CPU:
CPU 热插拔状态机(部分):
CPUHP_AP_RISCV_TIMER_STARTING
starting_cpu: riscv_timer_starting_cpu() → 启动计时器
dying_cpu : riscv_timer_dying_cpu() → 停止计时器
CPUHP_AP_IRQ_RISCV_STARTING
starting_cpu: 使能 CPU 的 PLIC 上下文
CPU 下线流程(cpu_down):
1. 迁移中断到其他 CPU
2. 调用 dying_cpu 回调链(计时器、PLIC 等)
3. 停止调度该 CPU
4. 调用 sbi_hart_stop()(SBI HSM 下线)
5. Hart 进入 STOPPED 状态,可被再次唤醒
RISC-V 支持 NUMA 拓扑(通过 DTB 中的 numa-node-id 属性):
arch/riscv/mm/numa.c
/* DTB NUMA 初始化 */
int __init arch_numa_init(void)
{
if (!numa_off)
return of_numa_init(); /* 从 DTB 读取 NUMA 拓扑 */
return dummy_numa_init(); /* 单节点回退 */
}
/* 每个 CPU 节点关联 */
void __init riscv_numa_setup(void)
{
for_each_possible_cpu(cpu) {
int nid = of_node_to_nid(cpu_device_node(cpu));
cpu_to_node_map[cpu] = nid;
}
}| 功能 | 文件路径 |
|---|---|
| 启动汇编(head.S) | arch/riscv/kernel/head.S |
| SBI 接口实现 | arch/riscv/kernel/sbi.c |
| 陷阱处理(C 层) | arch/riscv/kernel/traps.c |
| ISA 扩展检测 | arch/riscv/kernel/cpufeature.c |
| ISA 扩展定义 | arch/riscv/include/asm/hwcap.h |
| CSR 寄存器定义 | arch/riscv/include/asm/csr.h |
| IRQ 框架 | arch/riscv/kernel/irq.c |
| INTC 驱动 | drivers/irqchip/irq-riscv-intc.c |
| PLIC 驱动 | drivers/irqchip/irq-sifive-plic.c |
| RISC-V 计时器 | drivers/clocksource/timer-riscv.c |
| 向量扩展实现 | arch/riscv/kernel/vector.c |
| 向量扩展头文件 | arch/riscv/include/asm/vector.h |
| SMP 支持 | arch/riscv/kernel/smp.c |
| Alternative 机制 | arch/riscv/kernel/alternative.c |
| Alternative 头文件 | arch/riscv/include/asm/alternative.h |
| NUMA 支持 | arch/riscv/mm/numa.c |
| CPU 热插拔 | arch/riscv/kernel/cpu_ops_sbi.c |
| 上下文切换(ASID) | arch/riscv/mm/context.c |
由 Claude Code 分析生成