Skip to content

Latest commit

 

History

History
3225 lines (2591 loc) · 111 KB

File metadata and controls

3225 lines (2591 loc) · 111 KB

Linux 内核 RISC-V 架构深度分析

基于 Linux 内核源码(master 分支,commit 8a30aeb0d 附近)实际阅读生成


目录

  1. RISC-V ISA 基础:RV32/RV64 与标准扩展
  2. 特权级别:U/S/M 模式与 CSR 寄存器
  3. 异常与中断处理
  4. 内存管理单元:Sv39/Sv48/Sv57 页表
  5. 上下文切换:__switch_to 与 thread_struct
  6. SBI(Supervisor Binary Interface)
  7. 设备发现:Device Tree
  8. 平台启动流程:OpenSBI → Linux start_kernel
  9. 向量扩展(RVV)支持
  10. Zicbom 缓存块管理扩展
  11. 中断控制器:PLIC/CLINT/AIA
  12. 内核虚拟内存布局

1. RISC-V ISA 基础:RV32/RV64 与标准扩展

1.1 ISA 概述

RISC-V 是一套开放的精简指令集架构(ISA),由加州大学伯克利分校设计。其最显著的特征是模块化设计:一个极简基础整数指令集(I)加上可选的标准扩展组合。

基础整数宽度决定了寄存器宽度和地址空间大小:

RV32I  : 32 位寄存器,32 位地址空间
RV64I  : 64 位寄存器,64 位地址空间(Linux 主流部署)
RV128I : 128 位(实验性,目前无内核支持)

1.2 标准扩展(IMAFDCV)

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() 使用静态分支进行高效检查。

1.3 整数寄存器

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_exceptionarch/riscv/kernel/entry.S,第 125 行)中,寄存器 tp(x4)专门用于保存指向当前任务 thread_info 的指针。


2. 特权级别:U/S/M 模式与 CSR 寄存器

2.1 三种特权级别

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)交互。

2.2 关键 CSR 寄存器定义

源码文件: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_FSSR_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_STVEC

sepc(Supervisor Exception Program Counter,CSR 0x141)

保存触发异常时的指令地址(PC)。在 entry.S 第 185 行:

csrr s2, CSR_EPC

scause(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 非零说明来自用户态

2.3 完整 CSR 地址表(关键部分)

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 向量寄存器字节长度

3. 异常与中断处理

3.1 异常向量入口:handle_exception

源码文件: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            // 标记:现在在内核中

3.2 异常分发表

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                    // 间接调用

3.3 异常原因码(来自 csr.h)

// 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 扩展)

3.4 从异常返回(ret_from_exception)

ret_from_exception(entry.S 第 247–318 行)负责恢复状态并返回:

  1. 检查 PT_STATUS 中的 SR_SPP 位,判断是否返回用户态
  2. 若返回用户态,写回 CSR_SCRATCH = tp(保存内核结构指针)
  3. 恢复所有通用寄存器(x1–x31)
  4. 清除 LR/SC 保留(原子操作安全):REG_SC x0, a2, PT_EPC(sp)
  5. 执行 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                   // 返回用户/内核态,硬件自动切换特权级

4. 内存管理单元:Sv39/Sv48/Sv57 页表

4.1 虚拟地址格式

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)

4.2 运行时页表级别选择

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(固定)

4.3 PTE 格式

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)

4.4 页表数据结构

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),函数指针指向不同的实现,实现了优雅的多阶段初始化。

4.5 ASID(地址空间标识符)

在 RV64 下,satp 中 ASID 字段为 16 位(最多 65536 个地址空间)。Linux 利用 ASID 避免上下文切换时的全量 TLB 刷新。sfence.vma 指令可携带 ASID 和虚拟地址参数做精细的 TLB 失效:

sfence.vma zero, zero   // 刷新所有 ASID 的所有映射
sfence.vma va, asid     // 精确刷新某 ASID 某地址的映射

5. 上下文切换:__switch_tostruct thread_struct

5.1 thread_struct 结构

源码文件: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 // 需要恢复可抢占向量上下文

5.2 __switch_to 汇编实现

源码文件: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 调用约定处理。

5.3 FPU 惰性保存/恢复

RISC-V 采用惰性(lazy)策略管理浮点状态。sstatus.FS 字段跟踪 FPU 状态:

FS=OFF     -> 任何 FP 指令触发非法指令异常 -> 内核在异常中分配/恢复状态
FS=INITIAL -> FP 寄存器已重置为初始值
FS=CLEAN   -> FP 状态未变化,无需保存
FS=DIRTY   -> FP 状态已变化,切换时必须保存

arch/riscv/kernel/process.cstart_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;                       // 设置用户栈

6. SBI(Supervisor Binary Interface)

6.1 SBI 架构与 ecall 调用约定

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(附加返回值)

6.2 SBI 扩展 ID 枚举

源码文件: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)
};

6.3 sbi_init() 初始化流程

源码文件: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;              // 固件特性
        }
    }
}

6.4 关键 SBI 服务实现

计时器设置(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;
}

7. 设备发现:Device Tree

7.1 RISC-V 无 ACPI 的设备发现

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 物理地址

7.2 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 物理地址

7.3 典型 RISC-V 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>;
        };
    };
};

8. 平台启动流程:OpenSBI → Linux start_kernel

8.1 完整启动流程

+------------------+
| 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 |
+------------------+

8.2 MMU 开启关键步骤

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

8.3 多核(SMP)启动

主核(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 行)中完成:

  1. 屏蔽中断
  2. 加载全局指针
  3. 读取栈和任务结构指针
  4. 调用 relocate_enable_mmu 开启 MMU
  5. 设置异常向量
  6. 调用 smp_callin() 注册到调度器

11. 中断控制器:PLIC/CLINT/AIA

11.1 CLINT(Core Local Interrupt)

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);
}

11.2 PLIC(Platform-Level Interrupt Controller)

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 寄存器(结束中断)

11.3 AIA(Advanced Interrupt Architecture)

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 扩展)

9. 向量扩展(RVV)支持

9.1 RVV 架构概述

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) // 非法配置

9.2 向量上下文状态管理

源码文件: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);
}

9.3 向量寄存器大小初始化

源码文件: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;
}

9.4 向量上下文分配与释放

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

9.5 向量 CSR 保存流程

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 指向的内存区域。

9.6 第一次使用处理(Lazy Vector Allocation)

当用户程序首次使用向量指令时(VS=OFF),触发非法指令异常,内核在 riscv_v_first_use_handler() 中:

  1. 检测异常是否由向量指令引发(通过 insn_is_vector(),vector.c 第 81–112 行)
  2. 为该任务分配向量状态内存(kmem_cache_alloc
  3. sstatus.VS 设置为 INITIAL
  4. 从异常返回,重新执行该向量指令

10. Zicbom 缓存块管理扩展

10.1 Zicbom 概述

Zicbom(Cache Block Management Operations)扩展提供三条缓存块操作指令:

指令 操作 说明
cbo.clean rs1 Clean 将脏缓存行写回内存,但保留在 cache 中
cbo.flush rs1 Flush 写回并从 cache 中驱逐
cbo.inval rs1 Invalidate 使缓存行无效(不写回,适合 DMA 后的失效)

10.2 块大小发现

源码文件: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 的块大小必须一致,否则发出警告
}

10.3 ENVCFG 控制寄存器

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

10.4 DMA 缓存一致性

在非一致性(non-coherent)DMA 平台上,Zicbom 指令用于维护 DMA 操作前后的缓存一致性:

DMA 写入前(CPU → 外设):
    cbo.clean  地址       // 确保内存包含最新数据

DMA 读取后(外设 → CPU):
    cbo.inval  地址       // 使旧 cache 行无效,强制从内存读取新数据

12. 内核虚拟内存布局

12.1 RV64 (Sv39) 虚拟地址空间布局

基于 arch/riscv/include/asm/pgtable.harch/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))

12.2 vmalloc 新映射优化

内核在 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

13. head.S 启动流程深度分析

arch/riscv/kernel/head.S 是 RISC-V Linux 内核第一个被执行的代码文件,负责从裸机状态建立 C 运行环境。

13.1 内核镜像头格式

内核镜像开头包含一个紧凑的元数据头,引导程序(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 头),使得同一套引导逻辑可以处理多种架构。

13.2 _start_kernel 完整启动流程

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 */      │
  └─────────────────────────────────────────────────────────┘

13.3 relocate_enable_mmu:开启虚拟内存

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 可以翻译新地址。

13.4 从核启动流程

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 的老平台。

13.5 XIP(就地执行)支持

当启用 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

14. SBI(Supervisor Binary Interface)深度解析

SBI 是 RISC-V 平台中 S 模式(Linux)与 M 模式固件(OpenSBI)之间的标准化接口,类似于 x86 的 BIOS/UEFI 中断服务。

14.1 ecall 内联汇编实现

arch/riscv/kernel/sbi.c:29
struct 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

14.2 SBI 版本历史与兼容层

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 扩展(调试控制台)                                │
  └────────────────────────────────────────────────────────────┘

14.3 sbi_init() 初始化逻辑

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());
}

14.4 RFENCE 操作:跨 Hart TLB 刷新

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_I

14.5 HSM hart 状态机

SBI 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() 传递启动地址和上下文参数。


15. 特权模式深度分析

15.1 模式切换机制

RISC-V 特权模式切换通过以下机制实现:

模式切换触发条件:
  ┌──────────────────────────────────────────────────────┐
  │ 升级(低权限 → 高权限)                              │
  │   ecall    : 同步 —— 主动请求(系统调用/SBI 调用)   │
  │   异常     : 同步 —— 非法指令/访问违规/断点等        │
  │   中断     : 异步 —— 外部中断/计时器中断/软件中断    │
  │                                                      │
  │ 降级(高权限 → 低权限)                              │
  │   sret     : 从 S 模式返回(必须设置 sstatus.SPP)   │
  │   mret     : 从 M 模式返回                           │
  └──────────────────────────────────────────────────────┘

15.2 中断/异常委托寄存器

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

15.3 sstatus 寄存器关键字段

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 位,控制内核是否可以直接访问用户空间内存页。默认关闭以防止内核误读用户数据。

15.4 CSR 访问权限矩阵

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 模式 —— 用户态可读

15.5 H 扩展(虚拟化)

当实现 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 用户)│
      └────────────┘

16. 页表深度分析

16.1 页表遍历(PTW)硬件行为

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 普通页

16.2 超页(Huge Page)支持

页大小层级:
  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)是主要目标。

16.3 A/D 位管理策略

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 位

16.4 ASID 机制

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(最精确)

16.5 内核页表三阶段初始化

阶段 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
  - 启用内核页表隔离(如果配置了)

17. traps.c 陷阱处理深度分析

arch/riscv/kernel/traps.c 包含从 entry.S 汇编入口分发后的 C 语言陷阱处理逻辑。

17.1 DO_ERROR_INFO 宏

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")

17.2 非法指令处理(向量首次使用入口)

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");
    }
}

"首次使用"机制:

  1. 进程启动时 sstatus.VS = OFF(向量禁用)
  2. 第一次执行向量指令 → 触发非法指令异常
  3. riscv_v_first_use_handler() 分配向量寄存器上下文,设置 VS = INITIAL
  4. 返回后重新执行该向量指令,正常完成

17.3 断点处理与 kprobes

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() 通过检查指令编码判断。

17.4 系统调用入口

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();
    }
}

17.5 软件检查(CFI)处理

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");
}

17.6 die() 内核崩溃流程

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);
}

17.7 内核栈溢出处理

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");
}

18. cpufeature.c:ISA 扩展检测机制

18.1 扩展标识符体系

arch/riscv/include/asm/hwcap.h:1

Linux 使用一个大型位图跟踪所有已知 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  /* 位图大小 */

18.2 全局位图与 hart 位图

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 */

18.3 ISA 字符串解析流程

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[]

18.4 扩展验证函数示例

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;
}

18.5 T-Head 扩展特殊处理

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,
    ...
};

18.6 hwcap 导出到用户空间

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) 查询 */

19. PLIC 平台级中断控制器深度分析

19.1 PLIC 寄存器内存映射

drivers/irqchip/irq-sifive-plic.c:~40
PLIC 内存空间布局(基地址 = 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

19.2 PLIC 中断处理流程

完整中断处理路径:

  外设产生中断
      │
      ▼
  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)

19.3 PLIC 使能/禁用实现

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 个中断源。

19.4 多 Hart PLIC 上下文管理

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);

19.5 INTC ↔ PLIC 层次关系

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()

20. CLINT 与计时器深度分析

20.1 RISC-V 时钟架构

时钟相关寄存器层次:

  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 位寄存器

20.2 clocksource 与 clockevent

drivers/clocksource/timer-riscv.c

Linux 时钟框架需要两类抽象:

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 */

20.3 Sstc 扩展性能对比

性能比较(每次计时器设置的开销):

  SBI 路径(旧):
    ecall → M 模式陷阱 → OpenSBI 处理 → 写 mtimecmp → mret
    约 100-500 ns(取决于实现)

  Sstc 路径(新):
    csr_write(CSR_STIMECMP, val)
    约 1-5 ns(单条 CSR 写指令)

  差异:约 100 倍性能提升(特别是高频率 tick 场景)

20.4 计时器初始化流程

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)

20.5 RV32 双寄存器 64 位写

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 确保不提前触发,再写高位,最后更新低位。


21. 向量扩展(RVV)内核支持深度分析

21.1 RVV 指令集概述

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)   /* 硬件向量长度字节数 */

21.2 向量上下文状态机

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]

21.3 向量上下文保存/恢复

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);
}

21.4 内核态向量使用(kernel_vector_begin/end)

内核代码也可以使用向量指令(用于 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. 禁用抢占(向量状态不允许被打断保存到不同栈上)
*/

21.5 向量内存动态分配

由于 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 = &current->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;  /* 已处理,重试指令 */
}

21.6 可抢占向量操作

使用 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 */
}

22. PMP(物理内存保护)深度分析

22.1 PMP 概述与用途

PMP(Physical Memory Protection)是 RISC-V M 模式提供的内存访问控制机制,在没有虚拟内存时(如早期启动阶段)或 M 模式代码中限制低特权级别对物理内存的访问。

在 Linux 的使用场景中,PMP 主要由 OpenSBI 固件配置,Linux 本身不直接操作 PMP。

22.2 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 区域

22.3 OpenSBI 的 PMP 配置策略

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 处理中临时提权

22.4 Linux 与 PMP 的交互

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
*/

22.5 Smepmp 扩展(增强型 PMP)

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)   /* 规则锁定旁路(测试用)*/

23. Alternative 机制:运行时代码打补丁

23.1 机制概述

Alternative 机制允许内核在启动时根据当前 CPU 特性将通用代码替换为优化代码,无需在每次执行时做运行时判断:

arch/riscv/include/asm/alternative.h
arch/riscv/kernel/alternative.c

23.2 补丁点数据格式

/* 每个 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,内核启动时批量处理。

23.3 使用示例

/* 在汇编代码中插入 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");

23.4 应用阶段

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

23.5 Errata 修复

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, ...
)

24. SMP 多核支持深度分析

24.1 Hart ID 与 CPU 编号映射

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 开始使用)。

24.2 SMP 启动流程

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()

24.3 IPI(处理器间中断)实现

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;
}

24.4 CPU 热插拔(CPUHP)

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 状态,可被再次唤醒

24.5 NUMA 支持(可选)

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 分析生成