diff --git a/articles/20220501-riscv-context-switch.md b/articles/20220501-riscv-context-switch.md new file mode 100644 index 0000000000000000000000000000000000000000..de960ac11e199067080ae2705b124fefb3662454 --- /dev/null +++ b/articles/20220501-riscv-context-switch.md @@ -0,0 +1,307 @@ +> Author: Jack Y.
+> Date: 2022/05/01
+> Revisor:
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux) + +# RISC-V Linux 上下文切换分析 + +## 前言 + +为了在一个处理器上能实现多个任务的并发运行,操作系统要负责在各个任务之间完成调度,这是操作系统最基本的功能之一。 + +在 Linux 中,所有调度程序最终都归结到完成上下文切换(Context Switch),即前后两个任务之间运行环境的切换,本文主要基于 Linux 5.17 版本代码,讨论在 RISC-V 架构中上下文切换的诸多细节。 + +本文仅讨论上下文切换本身的流程,而何时进行上下文切换、切换到什么任务的问题,涉及到调度算法和策略,将在后续文章进行介绍。 + +## 纵览 Linux 上下文切换 + +Linux 上下文切换的入口在 `context_switch()` 函数,在其函数注释中已经明确概括了其主要内容: + +```c +// kernel/sched/core.c:4940 +/* + * context_switch - switch to the new MM and the new thread's register state. + */ +static __always_inline struct rq * +context_switch(struct rq *rq, struct task_struct *prev, + struct task_struct *next, struct rq_flags *rf) +{ + prepare_task_switch(rq, prev, next); + arch_start_context_switch(prev); + if (!next->mm) { // to kernel + enter_lazy_tlb(prev->active_mm, next); + next->active_mm = prev->active_mm; + if (prev->mm) // from user + mmgrab(prev->active_mm); + else + prev->active_mm = NULL; + } else { // to user + membarrier_switch_mm(rq, prev->active_mm, next->mm); + switch_mm_irqs_off(prev->active_mm, next->mm, next); + if (!prev->mm) { // from kernel + rq->prev_mm = prev->active_mm; + prev->active_mm = NULL; + } + } + rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP); + prepare_lock_switch(rq, next, rf); + switch_to(prev, next, prev); + barrier(); + return finish_task_switch(prev); +} +``` + +`context_switch()` 函数主要就负责两方面的切换,一是切换到新线程的 `mm_struct`,二是切换到新线程的寄存器状态。函数整体不长,去掉空行和注释后总共22行。 + +其函数入参共 $4$ 个: +* 第一个是 `rq`,即 Running Queue,每个 CPU 核有一个 Running Queue,大致可以理解为该 CPU 上的任务队列,每次从 Running Queue 中取出一个任务进行调度; +* 第二个是 `prev`,即切换之前正在执行的任务; +* 第三个是 `next`,即切换后要执行的任务; +* 第四个是 `rf`,在本函数中与 Running Queue 的锁有关。 + +`prepare_task_switch()` 函数主要是任务切换前的一些准备工作,里面主要涉及 `kcov`、`perf` 等与调测和性能监测相关的内容,与本文核心内容联系不大。 + +`arch_start_context_switch()` 函数给各个体系结构专有的开始上下文切换的工作提供了入口,但 RISC-V 架构对此函数无专门定义,各体系结构在通用的定义中,该函数为空。 + +接下来的 `if-else` 语句就到了本文第一个核心部分,即 `mm_struct` 的切换,将在下文中详述。 + +`rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);` 这一行与 Running Queue 的时钟更新有关,具体细节将在后续关于 Schedule 的文章中介绍。 + +下一行 `prepare_lock_switch()` 与死锁检测 `lockdep` 有关,本文将忽略这些离题较远的内容。 + +然后就到了 `switch_to()`,即 `context_switch()` 的第二个核心内容——寄存器的切换。 + +最后在加了一个内存屏障 `barrier()` 后,就到了结束环节 `finish_task_switch()`,这个函数和前面的 `prepare_lock_switch()` 相互对应。 + +下文将对 `mm_struct` 的切换和 `switch_to()` 寄存器内容的切换这两大部分进行详细分析。 + +## `mm_struct` 的切换 + +### 背景知识 + +我们先回顾《操作系统》课程中的两个基本概念: +* 进程是资源分配的最小单位 +* 线程是CPU调度的最小单位 + +上文中所提到的「任务」,其实就是线程。同一个进程的不同线程之间,共享内存资源,即他们拥有同一个 `mm_struct`。 + +对于 CPU 来说,不同线程之间在内存方面的主要区别是他们拥有不同的地址空间,即与地址翻译相关的页表(Page Table)及其缓存 TLB 不同。因此在上下文切换过程中,和内存相关的内容主要就涉及到页表指针的切换和 TLB 的刷新。 + +Linux 把整个虚拟地址空间分成两部分,一部分是用户空间,另一部分是内核空间。对于用户线程来说,它只能访问用户空间的内存,当用户线程陷入内核态后,它可以访问用户空间,也可以访问内核空间;对于内核线程来说,它只会访问内核空间的内存。每个用户进程之间是内存隔离的,因此他们拥有自己独有的用户空间的映射关系;而内核空间的是整个系统共有的,因此所有进程共享同样的内核空间的映射关系。 + +我们可以发现,在 `task_struct` 类中,有两个 `mm_struct*` 类型的成员,名字分别是 `mm` 和 `active_mm`(见`include/linux/sched.h:860`),在一封 [二十多年前的电子邮件中](https://www.kernel.org/doc/html/latest/vm/active_mm.html),Linus Tolvalds 向开发者解释了这两个指针的区别。我们把这封邮件的内容加以总结和提炼,得到 `mm` 和 `active_mm` 用法如下: + +1. `mm` 成员是用户进程(线程)所正常使用的 `mm_struct`; +2. 内核进程(线程)由于无专有的内存空间,而与所有其他进程共享同样的内核空间,为了方便与优化性能,内核线程借用了该 CPU 中上一个运行的用户线程所使用的 `mm_struct`; +3. 内核线程的 `mm` 成员是空指针 `NULL`; +4. 内核线程将其借用的 `mm_struct` 地址存储在 `active_mm` 中,用户线程的 `active_mm == mm`。 + +### 切换逻辑分析 + +接下来我们看这一段 `if-else` 语句,在语句前的注释中,已经对切换前和切换后分别是内核线程和用户线程这排列组合的四种情况进行了分类讨论: + +```c +// kernel/sched/core.c:4956 +/* +* kernel -> kernel lazy + transfer active +* user -> kernel lazy + mmgrab() active +* +* kernel -> user switch + mmdrop() active +* user -> user switch +*/ +``` + +在代码中首先处理的是「切换到 Kernel 线程」部分,即`if (!next->mm)`(如上文所说,内核线程的 `mm` 是空指针)。 + +按照上面注释,凡是切换到内核线程的首先要做一个「lazy」,即 `enter_lazy_tlb()` 函数的调用。 +这个函数在 x86 等架构中有实现,RISC-V 中未实现该函数,其默认实现为空。在 x86 中,「Lazy TLB」主要用于减少切换上下文时不必要的 TLB 清空,以免切换后因地址翻译变慢造成性能下降。 RISC-V 借鉴 ARM 设计了 ASID 来实现类似的优化,下文中会详细介绍。 + +下一行 `next->active_mm = prev->active_mm;` 把上一个线程的 `active_mm` 「借」到了下一个内核线程中。 + +之后对上一个线程的类别加以区分:如果上一个线程是用户线程(`prev->mm != NULL`),则需要增加这个被借用的 `mm_struct` 的引用计数,这个引用计数记录了当前有多少个内核线程正在借用该 `mm_struct`,如果该 `mm_struct` 对应的用户线程已经死亡,则 Linux 需要等到其引用计数为 $0$,即不再有内核线程借用它,才能将其销毁;如果上一个线程是内核线程,则把上一个线程的 `active_mm` 清空,结束其对于该 `mm_struct` 的借用(这只是把借用它的内核线程从一个转换到了另一个,引用计数无需增加)。 + +接下来我们看「切换到 User 线程」的情况,即 `else` 语句中的内容。 + +首先 `membarrier_switch_mm(rq, prev->active_mm, next->mm);` 使用了一个内存屏障,来保证上一个线程访问其内存空间与下一个线程访问其内存空间之间的先后顺序,避免在访存进行过程中发生 `mm_struct` 的切换导致的访存错误。(顺便补充一句,前面切换到内核线程因为没有切换 `mm_struct`,而不需要这样的内存屏障。) + +接着就到了重头戏 `switch_mm_irqs_off(prev->active_mm, next->mm, next);`,即真正切换 `mm_struct`。RISC-V 中没有定义 `switch_mm_irqs_off()` 函数,由通用宏定义转为 `switch_mm()` 函数。 + +最后如果切换之前的线程是内核线程,则需要设置 `rq->prev_mm` 用于后续清除其引用计数,并且解除上一个线程对它的借用。 + +### RISC-V 的 `switch_mm()` 实现 + +```c +// include/linux/mmu_context.h:8 +/* Architectures that care about IRQ state in switch_mm can override this. */ +#ifndef switch_mm_irqs_off +# define switch_mm_irqs_off switch_mm +#endif +``` + +RISC-V 的 `switch_mm()` 函数本身也很简单,只有短短 9 行: + +```c +// arch/riscv/mm/context.c:305 +void switch_mm(struct mm_struct *prev, struct mm_struct *next, + struct task_struct *task) +{ + unsigned int cpu; + if (unlikely(prev == next)) + return; + cpu = smp_processor_id(); + cpumask_clear_cpu(cpu, mm_cpumask(prev)); + cpumask_set_cpu(cpu, mm_cpumask(next)); + set_mm(next, cpu); + flush_icache_deferred(next, cpu); +} +``` + +首先是处理了 `prev == next` 的情况(同一个进程的不同线程之间切换,或者借用了某用户线程地址空间的内核线程切换到该用户线程),这种情况直接返回。 + +接着清除了该 CPU 的 `cpumask` 中之前的 `mm_struct` 的标志,并设置了新的 `mm_struct` 标志。 + +然后就到了 `set_mm()` 这个实际设置 `mm_struct` 的环节。 + +`set_mm()` 根据 `use_asid_allocator` 标志来区分,调用包含 `ASID` 的版本或者不包含 `ASID` 的版本: + +```c +// arch/riscv/mm/context.c:208 +static inline void set_mm(struct mm_struct *mm, unsigned int cpu) +{ + if (static_branch_unlikely(&use_asid_allocator)) + set_mm_asid(mm, cpu); + else + set_mm_noasid(mm); +} +``` + +`use_asid_allocator` 是系统初始化阶段时在 `asids_init()` 函数中设置的,下一小节再详细介绍 RISC-V 的 ASID 机制,这里先看一下不包含 ASID 情况下的 `set_mm()`: + +```c +// arch/riscv/mm/context.c:201 +static void set_mm_noasid(struct mm_struct *mm) +{ + /* Switch the page table and blindly nuke entire local TLB */ + csr_write(CSR_SATP, virt_to_pfn(mm->pgd) | satp_mode); + local_flush_tlb_all(); +} +``` + +`set_mm_noasid()` 还是很简单的,在 `satp` CSR 中设置一下页表指针(来自于 `mm->pgd`)和页表模式(32 位情况下为 `sv32`,64 位时根据 `CONFIG_XIP_KERNEL` 的配置选择 `sv39` 或 `sv48`),然后直接把所有的 TLB 项全部清空即可。 + +### RISC-V 的 ASID 机制 + +在不带 ASID 的 `set_mm_noasid()` 中,我们在设置页表后,简单地把所有的 TLB 项全部清空了,这就会导致切换到新的线程后,在一开始取指、访存时,会出现大量的 TLB Miss 的情况,需要反复到内存中查找页表,导致性能降低。ASID 机制是为了缓解这个问题而诞生的。 + +ASID 的全称是 Address Space Identifier,总而言之,每个用户进程(即每个 `mm_struct`)拥有一个唯一的 ASID,用于在 TLB 中与其他进程的表项区隔开来。这样在 TLB 中可以同时存在多个进程的表项,在地址翻译时增加表项的 ASID 与当前运行进程的 ASID 的匹配。经过这种优化后,如果两个用户进程之间频繁切换时,不用每次都清空 TLB,导致后续大量 TLB Miss,提高了取指和访存效率。 + +当前运行进程的 ASID 和页表指针一样,都写在 `satp` CSR 中,下图分别是 RV32 和 RV64 下该 CSR 的值设计: + +![](images/riscv_context-switch/satp-sv32.png) + +![](images/riscv_context-switch/satp-sv39.png) + +从上面两图中可以看出,RV32 和 RV64 下,ASID 最长分别可以到 9 位和 16 位。但这只是理论最大值,实际硬件实现中不一定有这么多位(因为 ASID 的位数越多,在 TLB 中其比较逻辑、存储空间占用就会越大,这里也是性能与面积/功耗的取舍)。 + +软件上判断 ASID 实现位数的方式是,先往 `satp` CSR 中理论上最多的 ASID 位上都写 $1$,然后再把这些位读出来,有多少位被写上了 $1$,就是硬件支持多少位。在 ASID 的初始化函数 `asids_init()` 中,Linux 也就是通过这种方式判断 ASID 的位数,从而决定是否开启 ASID。 + +```c +// arch/riscv/mm/context.c:220 + /* Figure-out number of ASID bits in HW */ + old = csr_read(CSR_SATP); + asid_bits = old | (SATP_ASID_MASK << SATP_ASID_SHIFT); + csr_write(CSR_SATP, asid_bits); + asid_bits = (csr_read(CSR_SATP) >> SATP_ASID_SHIFT) & SATP_ASID_MASK; + asid_bits = fls_long(asid_bits); + csr_write(CSR_SATP, old); +``` + +在这个初始化函数中,如果 ASID 的位数 `asid_bits` 大于当前 CPU 核数的两倍,就会开启 ASID,即设置 `use_asid_allocator` 为 `TRUE`,否则不会开启 ASID。 + +硬件上 ASID 的数量是有限的,为了在有限的硬件 ASID 上实现出「无限」的虚拟 ASID,RISC-V 借鉴 ARM 实现了如下的 Version 机制: +RISC-V 在 `mm_struct` 的体系结构特异字段 `context` 中增加了一个成员 `atomic_long_t id;`,其低位用于保存 ASID,而高位用于保存 ASID 的 version。 +这个 version 可以理解为 ASID 的有效版本,在当前版本的 ASID 空间耗尽时,版本号会自增 $1$,同时属于旧版本的所有 ASID 均将失效。 + +现在我们来开始分析开启 ASID 后,`set_mm_asid()` 函数中上下文切换时 ASID 的更新逻辑(见 `arch/riscv/mm/context.c:145`)。 + +首先读出下一个执行线程的 `mm_struct` 的 `context.id` 为 `cntx`,以及当前 CPU 正在执行线程的 `context.id` 为 `old_active_cntx`,如果 `old_active_cntx` 有效且 `cntx` 属于当前版本,那么直接 goto 到 `switch_mm_fast`,更新当前 CPU 的 `context.id` 为 `cntx` 以及设置 `satp` CSR 的 ASID 和页表指针即可: + +```c +// arch/riscv/mm/context.c:151 +cntx = atomic_long_read(&mm->context.id); +old_active_cntx = atomic_long_read(&per_cpu(active_context, cpu)); +if (old_active_cntx && + ((cntx & ~asid_mask) == atomic_long_read(¤t_version)) && + atomic_long_cmpxchg_relaxed(&per_cpu(active_context, cpu), + old_active_cntx, cntx)) + goto switch_mm_fast; +``` + +如果 `cntx` 不属于当前版本,则需要调用 `__new_context()` 生成一个新的 context。生成新的 context 的逻辑大致是,先尝试 ASID 不改,只更新 version,如果新的 context 有效,则无需清空 TLB;但如果新的 context 无效(该 ASID 已经被别的进程在新版本中使用),则需要在当前版本中从小到大找一个有效的 ASID 赋给该进程,如果当前版本 ASID 已经用光,则大版本 version 向前前进一次,同时将所有目前的 context 全部无效掉(具体内容读者可自行阅读这部分代码)。生成新的 context 后,存储在下一个进程的 `mm_struct` 当中: + +```c +// arch/riscv/mm/context.c:178 +/* Check that our ASID belongs to the current_version. */ +cntx = atomic_long_read(&mm->context.id); +if ((cntx & ~asid_mask) != atomic_long_read(¤t_version)) { + cntx = __new_context(mm); + atomic_long_set(&mm->context.id, cntx); +} +``` + +后面就是更新一下当前 CPU 核的 `active_context`,更新一下 `satp` CSR,按需刷新一下 TLB 即可。 + +```c +// arch/riscv/mm/context.c:185 +if (cpumask_test_and_clear_cpu(cpu, &context_tlb_flush_pending)) + need_flush_tlb = true; + +atomic_long_set(&per_cpu(active_context, cpu), cntx); + +raw_spin_unlock_irqrestore(&context_lock, flags); + +switch_mm_fast: +csr_write(CSR_SATP, virt_to_pfn(mm->pgd) | + ((cntx & asid_mask) << SATP_ASID_SHIFT) | + satp_mode); + +if (need_flush_tlb) + local_flush_tlb_all(); +``` + +到目前为止,页表指针和 TLB 项需要更新的都已经更新,`mm_struct` 的切换已经完成。 + +注:有关 RISC-V ASID 的设计读者也可以参考这封作者提交代码时的 [邮件](https://patchwork.kernel.org/project/linux-riscv/patch/20190327100201.32220-1-anup.patel@wdc.com/)。 + +另外,[这篇文章](https://zhuanlan.zhihu.com/p/118244515) 也生动地介绍了 ASID 管理,但其中对于 ASID 不足时处理方面的描述比较简略,未能反映出全部的情况,请读者阅读时注意鉴别。 + +## 切换寄存器内容 + +`context_switch()` 函数的下一个重要内容就是 `switch_to()`,即切换寄存器状态和栈(在 RISC-V 中栈指针也是若干通用寄存器中的一个,所以其实也属于切换寄存器内容)。 + +RISC-V 的 `switch_to()` 函数是一个带参宏,先是判断了这个核有没有 FPU,如果有还需要切换浮点寄存器,这部分我们就不深追究了;然后就是调用汇编写的 `__switch_to()` 函数: + +```c +// arch/riscv/include/asm/switch_to.h:74 +#define switch_to(prev, next, last) \ +do { \ + struct task_struct *__prev = (prev); \ + struct task_struct *__next = (next); \ + if (has_fpu()) \ + __switch_to_aux(__prev, __next); \ + ((last) = __switch_to(__prev, __next)); \ +} while (0) +``` + +`__switch_to()` 函数的位置在 `arch/riscv/kernel/entry.S:512`,基本内容就是把当前的各个寄存器保存在 `prev->thread` 中,然后从 `next->thread` 中恢复出各个寄存器的内容,受篇幅所限这里就不贴出代码了,感兴趣的读者可自行浏览。 + + +## 本文小结 + +本文介绍了 RISC-V 架构下 Linux 上下文切换的流程,并着重介绍了 `mm_struct` 的切换以及寄存器内容的切换 `switch_to()`,同时带大家了解了 RISC-V 中 ASID 的设计与实现。 + +## 参考文档 + +1. [进程切换分析(2):TLB处理](http://www.wowotech.net/process_management/context-switch-tlb.html) +2. [Commit Message](https://patchwork.kernel.org/project/linux-riscv/patch/20190327100201.32220-1-anup.patel@wdc.com/) +3. [Active MM](https://docs.kernel.org/vm/active_mm.html) \ No newline at end of file diff --git a/articles/20220501-riscv-linux-timer.md b/articles/20220501-riscv-linux-timer.md new file mode 100644 index 0000000000000000000000000000000000000000..c897603e9cfe7a735105723e85fcfa37fcf813cb --- /dev/null +++ b/articles/20220501-riscv-linux-timer.md @@ -0,0 +1,247 @@ +> Author: Yu Liao
+> Date: 2022/05/1
+> Revisor: lzufalcon
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux) + +# RISC-V timer 在 Linux 中的实现 + +## RISC-V timer 相关寄存器 + +### mtime & mtimecmp 寄存器 + +按照 RISC-V 定义,系统需要提供两个 64 位的 M 模式寄存器 `mtime` 和 `mtimecmp`,并通过 MMIO 方式映射到地址空间。 + +`mtime` 需要以固定的频率递增,并在发生溢出时回绕。当 `mtime` 大于或等于 `mtimecmp` 时,由核内中断控制器 (CLINT, Core-Local Interrupt Controller) 产生 timer 中断。中断的使能由 `mie` 寄存器中的 `MTIE` 和 `STIE` 位控制,`mip` 中的 `MPIE` 和 `SPIE` 则指示了 timer 中断是否处于 pending。在 RV32 中读取 `mtimecmp` 结果为低 32 位, `mtimecmp` 的高 32 位需要读取 `mtimecmph` 得到。 + +在 [RISC-V 特权 ISA 规范](https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMAFDQC/riscv-spec-20191213.pdf) 的 3.2.1 Machine Timer Registers (mtime and mtimecmp) 中详细介绍了这部分。 + +### time CSR + +RISC-V 还定义了一个 64 位非特权 CSR 寄存器 `time`,`time` 计数器是前面提到的 `mtime` 的只读映射。同样,在 RV32 中 `timeh` CSR 是 `mtime` 高 32 位的只读映射,对于 M 模式和 S 模式它们都是可读写的。 + +在 [RISC-V 特权 ISA 规范](https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMAFDQC/riscv-spec-20191213.pdf) 的 2.2 CSR Listing 和 3.1.11 Machine Counter-Enable Register (mcounteren) 可以找到这块的规范。 + +### htimedelta & htimedeltah 寄存器 + +在增加虚拟化扩展以后,特权模式会发生一定变化,如下图(来源于参考文档 5)所示: + +![RISC-V 虚拟化特权模式](images/riscv_specs/riscv-privilege-mode.png) + +相应地,timer 支持也进行了如下扩展: + +`htimedelta` 和 `htimedeltah` 是 Hypervisor 扩展里的 CSR,在 VS/VU 模式下读取 `time` 结果是真正的 host 中的 `time` 加上 `htimedelta`。同样的,对于 RV32 `htimedelta` 保存了低 32 位,高 32 位保存在 `htimedeltah`。 + +在 [RISC-V 特权 ISA 规范](https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMAFDQC/riscv-spec-20191213.pdf) 的 8.2.7 Hypervisor Time Delta Registers (htimedelta, htimedeltah) 中详细介绍了这部分。 + +### Sstc 扩展 + +由于 `mtimecmp` 只能在 M 模式下访问,对于 S/HS 模式下的内核和 VU/VS 模式下的虚拟机需要通过 SBI 才能访问,会造成较大的中断延迟和性能开销。为了解决这一问题,RISC-V 新增了 Sstc 拓展支持(已批准但尚未最终集成到规范中)。 + +Sstc 扩展为 HS 模式和 VS 模式分别新增了 `stimecmp` 和 `vstimecmp` 寄存器,当 $time >= stimecmp$ (HS)或 $time+htimedelta >= vstimecmp$ (VS)时会产生 timer 中断,不再需要通过 SBI 陷入其他模式。 + +详见 [RISC-V "stimecmp / vstimecmp" 扩展](https://github.com/riscv/riscv-time-compare/releases/download/v0.5.4/Sstc.pdf) 。 + +## Linux timer 实现 + +Linux 将底层时钟硬件抽象为两类设备:clockevent 和 clocksource,前者用来在未来指定的时间产生中断,通常用作定时器;后者则用于维护自系统启动以来所经过的时间。 + +当前 Linux 为 RISC-V 根据内核运行模式实现了两套驱动,代码路径为 drivers/clocksource/timer-riscv.c 和 drivers/clocksource/timer-clint.c。 + +本文代码基于最新的 Linux v5.18-rc4 和 OpenSBI v1.0,截止目前 Linux 对 Sstc 扩展的支持还没有合入主线内核,社区已有相关补丁:[Add Sstc extension support](https://lkml.org/lkml/2022/3/4/1175)。 + +`mtime` 频率由设备树 CPU 节点中的 timebase-frequency 定义,不同平台都各不相同,如 Kendryte K210 的频率是 7.8 MHz,平头哥 C910 的频率是 3 MHz,SiFive Unmatched A00 频率为 1 MHz。 + +### NoMMU timer-riscv.c + +timer-riscv.c 驱动适用于 NoMMU 系统,内核运行在 M 模式下,通过 CONFIG_CLINT_TIMER 使能该驱动。RV64 下 clocksource 是通过直接读取 `mtime` 寄存器实现的,RV32 系统需要分两次读取,并需要考虑产生进位的情况。 + +```c +#ifdef CONFIG_64BIT +static u64 notrace clint_get_cycles64(void) +{ + return clint_get_cycles(); +} +#else /* CONFIG_64BIT */ +static u64 notrace clint_get_cycles64(void) +{ + u32 hi, lo; + + do { + hi = clint_get_cycles_hi(); + lo = clint_get_cycles(); + } while (hi != clint_get_cycles_hi()); + + return ((u64)hi << 32) | lo; +} +#endif /* CONFIG_64BIT */ +``` + +`clint_get_cycles/clint_get_cycles_hi` 直接通过内存访问寄存器。 + +```c +#ifdef CONFIG_64BIT +#define clint_get_cycles() readq_relaxed(clint_timer_val) +#else +#define clint_get_cycles() readl_relaxed(clint_timer_val) +#define clint_get_cycles_hi() readl_relaxed(((u32 *)clint_timer_val) + 1) +#endif + +``` + +clockevent 是通过使能 `mie` 的 TIMER 中断,并向 `mtimecmp` 寄存器写入期望的计数值实现的。 + +```c +static int clint_clock_next_event(unsigned long delta, + struct clock_event_device *ce) +{ + void __iomem *r = clint_timer_cmp + + cpuid_to_hartid_map(smp_processor_id()); + + csr_set(CSR_IE, IE_TIE); + writeq_relaxed(clint_get_cycles64() + delta, r); + return 0; +} +``` + +### MMU timer-clint.c + +timer-clint.c 驱动适用于有 MMU 的场景,内核运行在 S/HS 模式下,通过 CONFIG_RISCV_TIMER 可以使能该驱动。和 timer-riscv.c 的驱动相比,本质上也是访问 `mtime` 和 `mtimecmp` 寄存器,不过由于 S 模式下无法直接访问它们,需要通过其他方式间接完成。 + +RV64 的 clocksource 是通过 csrr 直接读取 `time` 寄存器实现的;在 RV32 系统由于一条指令无法读完,需要分两次读取 `time` 和 `timeh`, 并考虑可能发生进位的情况。前面提到 `time` 和 `timeh` 这两个 CSR 是 `mtime` 寄存器的映射,因此频率与精度和 `mtime` 是一致的。 + +```c +#ifdef CONFIG_64BIT +static inline u64 get_cycles64(void) +{ + return get_cycles(); +} +#else /* CONFIG_64BIT */ +static inline u64 get_cycles64(void) +{ + u32 hi, lo; + + do { + hi = get_cycles_hi(); + lo = get_cycles(); + } while (hi != get_cycles_hi()); + + return ((u64)hi << 32) | lo; +} +#endif /* CONFIG_64BIT */ + +static inline cycles_t get_cycles(void) +{ + return csr_read(CSR_TIME); +} +static inline u32 get_cycles_hi(void) +{ + return csr_read(CSR_TIMEH); +} +``` + +clockevent 则是通过 SBI 间接访问 `mtimecmp` 实现的。 + +```c +static int riscv_clock_next_event(unsigned long delta, + struct clock_event_device *ce) +{ + csr_set(CSR_IE, IE_TIE); + sbi_set_timer(get_cycles64() + delta); + return 0; +} +``` + +这里以 OpenSBI 来分析,如果不支持 Sstc 扩展则调用在 SBI 中注册的 `timer_event_start` 函数写入 `mtimecmp`,这个需要具体平台自己去实现。 + +```c +void sbi_timer_event_start(u64 next_event) +{ + sbi_pmu_ctr_incr_fw(SBI_PMU_FW_SET_TIMER); + + /** + * Update the stimecmp directly if available. This allows + * the older software to leverage sstc extension on newer hardware. + */ + if (sbi_hart_has_feature(sbi_scratch_thishart_ptr(), SBI_HART_HAS_SSTC)) { +#if __riscv_xlen == 32 + csr_write(CSR_STIMECMP, next_event & 0xFFFFFFFF); + csr_write(CSR_STIMECMPH, next_event >> 32); +#else + csr_write(CSR_STIMECMP, next_event); +#endif + } else if (timer_dev && timer_dev->timer_event_start) { + timer_dev->timer_event_start(next_event); + csr_clear(CSR_MIP, MIP_STIP); + } + csr_set(CSR_MIE, MIP_MTIP); +} +``` + +在支持 Sstc 扩展后,可以直接访问 `stimecmp` 寄存器,避免通过 SBI 调用的方式产生的开销。社区已开展相关工作:[RISC-V: Prefer sstc extension if available](https://lore.kernel.org/all/20220426185245.281182-1-atishp@rivosinc.com/)。 + +### KVM vcpu_timer.c + +在 VS 模式下读取 `time` 时,KVM 会返回真正的 `time` 加上 `htimedelta`。 + +```c +static u64 kvm_riscv_current_cycles(struct kvm_guest_timer *gt) +{ + return get_cycles64() + gt->time_delta; +} +``` + +在 VS 模式下设置 `mtimecmp` 时,KVM 会开启一个已经创建好的高精度定时器,并把定时器的到期时间设置为写入 `mtimecmp` 值对应的 ns。 + +```c +int kvm_riscv_vcpu_timer_next_event(struct kvm_vcpu *vcpu, u64 ncycles) +{ + struct kvm_vcpu_timer *t = &vcpu->arch.timer; + struct kvm_guest_timer *gt = &vcpu->kvm->arch.timer; + u64 delta_ns; + + if (!t->init_done) + return -EINVAL; + + kvm_riscv_vcpu_unset_interrupt(vcpu, IRQ_VS_TIMER); + + delta_ns = kvm_riscv_delta_cycles2ns(ncycles, gt, t); + t->next_cycles = ncycles; + hrtimer_start(&t->hrt, ktime_set(0, delta_ns), HRTIMER_MODE_REL); + t->next_set = true; + + return 0; +} +``` + +在定时器到期后,KVM 会为 Guest 产生 TIMER 中断。 + +```c +static enum hrtimer_restart kvm_riscv_vcpu_hrtimer_expired(struct hrtimer *h) +{ + u64 delta_ns; + struct kvm_vcpu_timer *t = container_of(h, struct kvm_vcpu_timer, hrt); + struct kvm_vcpu *vcpu = container_of(t, struct kvm_vcpu, arch.timer); + struct kvm_guest_timer *gt = &vcpu->kvm->arch.timer; + + if (kvm_riscv_current_cycles(gt) < t->next_cycles) { + delta_ns = kvm_rizscv_delta_cycles2ns(t->next_cycles, gt, t); + hrtimer_forward_now(&t->hrt, ktime_set(0, delta_ns)); + return HRTIMER_RESTART; + } + + t->next_set = false; + kvm_riscv_vcpu_set_interrupt(vcpu, IRQ_VS_TIMER); + + return HRTIMER_NORESTART; +} +``` + +因此 VS 模式设置时钟事件需要通过 SBI 调用进入 HS 模式然后再进入 M 模式,会产生较大的开销。同样,在支持 Sstc 扩展后,可以直接访问 `vstimecmp` 并产生 timer 中断,社区目前已经开展了相关的工作:[RISC-V: KVM: Support sstc extension](https://lore.kernel.org/all/20220426185245.281182-4-atishp@rivosinc.com/)。 + +## 参考文档 + +1. [RISC-V Platform](https://github.com/riscv/riscv-platform-specs/blob/main/riscv-platform-spec.adoc/) +2. [RISC-V ISA Specification](https://riscv.org/technical/specifications/) +3. [RISC-V "stimecmp / vstimecmp" Extension](https://github.com/riscv/riscv-time-compare/releases/download/v0.5.4/Sstc.pdf) +4. 基于 FPGA 与 RISC-V 的嵌入式系统设计 +5. [RISC-V虚拟化扩展](https://static.sched.com/hosted_files/osseu19/4e/Xvisor_Embedded_Hypervisor_for_RISCV_v5.pdf) \ No newline at end of file diff --git a/articles/20220504-riscv-privileged.md b/articles/20220504-riscv-privileged.md new file mode 100644 index 0000000000000000000000000000000000000000..f0982c0d99e6886b1f30b2fca853cfc5972cd127 --- /dev/null +++ b/articles/20220504-riscv-privileged.md @@ -0,0 +1,115 @@ +> Author: Pingbo Wen
+> Date: 2022/05/04
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux) + +# RISC-V 特权指令 + +RISC-V ISA Spec 分为两部分,一个是非特权指令,另外一个就是特权指令。非特权指令主要是用于通用计算,站在操作系统的角度来看,可以理解为用户态(低权限模式)能够运行的指令。而特权指令,是为了能够运行像 Linux/Windows 现代操作系统而设定的。现代操作系统主要强调对资源的管控,这就需要硬件上提供额外的权限管理机制,从而能够限制普通应用代码的行为。 + +## 特权等级 + +在 ARM64 中,分为 el0-el3 特权等级。RISC-V 同样有类似的设定,具体定义如下: + +特权等级 | 编码 | 名称 | 缩写 +--------|------|-----|------ +0 | 00 | 用户模式 | U +1 | 01 | 监管者模式 | S +2 | 10 | Reserved +3 | 11 | 机器模式 | M + +M 模式是权限最高的特权等级,也是 RISC-V Spec 中明确规定必须要实现的特权等级,其他三个特权等级是可选的。芯片厂商可以根据实际应用场景来决定需要实现哪些特权等级。特权等级的实现组合有如下几种: + +- M, 简单嵌入式系统(单片机) +- M + U, 安全嵌入式系统(带保护) +- M + S + U, 现代操作系统(Windows/Linux) + +其中保留的特权等级 2 是留给虚拟化用的。在 H 扩展(Hypervisor Extension)中,把 S 模式扩展成 HS 模式(Hypervisor-Extended Supervisor mode),具体可以参考 Spec。 + +RISC-V 手册中有提到过一个 Debug Mode,可以理解为比 M 权限更高的特权等级,用于支持芯片调试。关于这个模式的资料可参考 [官方手册](https://github.com/riscv/riscv-debug-spec/blob/release/riscv-debug-release.pdf)。 + +在一个典型 Linux 系统中,用户态应用程序跑在 U 模式,内核跑在 S 模式,而 M 模式一般是 OpenSBI/U-Boot 等 Bootloader 在用。 + +## 异常处理 + +有了特权等级,相应的需要提供进入退出特权等级的方法,以及控制机制。和 ARM 类似,RISC-V 也是通过异常切换不同特权等级,这个地方你可以把异常理解成一种中断。严格来讲,中断也只是异常中的一种而已。 以 M 模式处理异常为例,当 U 或者 S 模式发生异常后,处理器会自动做如下处理: + +1. 处理器保存异常指令 PC 到 MEPC 中 +2. 根据发生的异常类型设置 MCAUSE,并更新 MTVAL 为出错的取指地址、存储/加载地址或者指令码 +3. 将 MSTATUS 的中断使能位域 MIE 保存到 MPIE 域中,将 MIE 域的值清零,禁止响应中断 +4. 将发生异常之前的权限模式保存到 MSTATUS 的 MPP 域中,切换到机器模式(没有做异常降级响应处理的话) +5. 根据 MTVEC 中的基址和模式,得到异常服务程序入口地址。处理器从异常服务程序的第一条指令处开始执行,进行异常的处理 + +如果是 S 模式处理异常,相应操作的寄存器就是 SEPC/SCAUSE/STVAL/SIE/SSTATUS 等。而读写这些寄存器主要是通过 CSR 指令,这跟 ARM 中的 MSR/MRS 指令类似。CSR 指令具体定义如下: + +CSR 指令 | 格式 | 说明 +---------|------|----- +CSRRC | csrrc rd, csr, rs1 | 控制寄存器清零,rd = csr,csr &= ~rs1 +CSRRCI | csrrci rd, csr, imm | 控制寄存器立即数清零,rd = csr, csr &= ~imm +CSRRS | csrrs rd, csr, rs1 | 控制寄存器置位,rd = csr, csr \|= rs1 +CSRRSI | csrrsi rd, csr, imm | 控制寄存器立即数置位,rd = csr, csr \|= imm +CSRRW | csrrw rd, csr, rs1 | 控制寄存器读写,rd = csr, csr = rs1 +CSRRWI | csrrwi rd, csr, imm | 控制寄存器立即数读写,rd = csr, csr = imm + +这些 CSR 指令配合 x0 寄存器,就组成了很多我们常见的伪指令: + +CSR 伪指令 | 格式 | 说明 +---------|------|----- +CSRC | csrc csr, rs | 对应基础指令 csrrc x0, csr, rs +CSRCI | csrci csr, imm | 对应基础指令 csrrci x0, csr, imm +CSRS | csrs csr, rs | 对应基础指令 csrrs x0, csr, rs +CSRSI | csrsi csr, imm | 对应基础指令 csrrsi x0, csr, imm +CSRR | csrr rd, csr | 对应基础指令 csrrs rd, csr, x0 +CSRW | csrw csr, rs | 对应基础指令 csrrw x0, csr, rs +CSRWI | csrwi csr, imm | 对应基础指令 csrrw x0, csr, imm + +除了硬件上的中断,以及非法指令等异常外,RISC-V 还提供 ECALL/EBREAK 两条指令,让软件可以自己主动产生异常,其中 ECALL 主要用于环境调用,Linux 系统调用就是通过这个指令执行内核系统调用。而 EBREAK 主要是在调试场景下用。 + +## Linux 系统下的系统调用实现 + +下面以 Linux 系统 `sys_open` 系统调用为例,我们看一下用户态程序(特权等级 0, U 模式)是怎么陷入到 Linux 内核(特权等级 1, S 模式)中执行系统调用的。 + +首先用户态通过 `ecall` 指令触发系统调用,使用 `a7` 寄存器传递系统调用编号,`a0-a5` 寄存器来传递参数: + +``` + 22482: eb8d bnez a5,224b4 <__libc_open+0x64> + 22484: 03800893 li a7,56 + 22488: f9c00513 li a0,-100 + 2248c: 8622 mv a2,s0 + 2248e: 00000073 ecall +``` + +陷入到内核态后,处理器从 STVEC 寄存器加载异常处理程序入口。在 Linux 内核初始化过程中(arch/riscv/kernel/head.S),就已经通过 CSR 指令设置好了 STVEC 寄存器,指向 `handle_exception` 函数: + +``` +setup_trap_vector: + /* Set trap vector to exception handler */ + la a0, handle_exception + csrw CSR_TVEC, a0 + + /* + * Set sup0 scratch register to 0, indicating to exception vector that + * we are presently executing in kernel. + */ + csrw CSR_SCRATCH, zero + ret +``` + +`handle_exception` 最终会跳转到 `handle_syscall`,然后从 a7 寄存器中拿到系统调用编号,从 `sys_call_table` 中索引到最终系统调用处理函数(arch/riscv/kernel/entry.S): + +``` + /* Check to make sure we don't jump to a bogus syscall number. */ + li t0, __NR_syscalls + la s0, sys_ni_syscall + /* + * Syscall number held in a7. + * If syscall number is above allowed value, redirect to ni_syscall. + */ + bgeu a7, t0, 1f + /* Call syscall */ + la s0, sys_call_table + slli t0, a7, RISCV_LGPTR + add s0, s0, t0 + REG_L s0, 0(s0) +1: + jalr s0 +``` diff --git a/articles/20220505-riscv-opensbi-quickstart.md b/articles/20220505-riscv-opensbi-quickstart.md new file mode 100644 index 0000000000000000000000000000000000000000..8624dbbc252b568fd33e1a3fa3a9455c7b02069c --- /dev/null +++ b/articles/20220505-riscv-opensbi-quickstart.md @@ -0,0 +1,531 @@ +> Author: Wu Zhangjin
+> Date: 2022/05/05
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux) + +# RISC-V OpenSBI 快速上手 + +## 简介 + +如果要支持 Linux 等现代操作系统,RISC-V 必须提供三种工作模式,即 Machine Mode, Supervisor Mode 和 User Mode。Supervisor 和 User 分别用于运行我们常见的 Linux 与用户态应用程序。而 Machine Mode 则用于运行 Bootloader 并装载和执行 OS。 + +在 RISC-V 上,Machine Mode 还为 Supervisor Mode 提供一些特定的服务,这些服务由 SBI (Supervisor Binary Interface) 规范定义,在运行完 Bootloader 以后,这些服务还驻留在内存,在 Supervisor Mode 可以通过 `ecall` 调用这些处于 Machine Mode 的服务。 + +OpenSBI 是 RISC-V SBI 规范的一种 C 语言参考实现,由西数开发,本文带领大家来上个手。 + +为了方便起见,本文统一使用 [Linux Lab Disk](https://tinylab.org/linux-lab-disk) 或 [Linux Lab](https://tinylab.org/linux-lab) 开发环境,含交叉编译器、模拟器等,可极速开展 RISC-V 开发。 + +另外,本文基于 OpenSBI v1.0 和 Linux v5.17。 + +## 基本用法 + +### 下载 + +OpenSBI 发布在 Github 上,其地址为: + +为了加快下载速度,我们在社区仓库做了镜像: + +接下来,下载到 Linux Lab 的 `src/examples` 目录下: + + $ cd src/examples + $ git clone https://gitee.com/tinylab/qemu-opensbi.git + $ cd qemu-opensbi/ + +### 编译 + +可以分别编译出 32 位和 64 位的版本: + + $ make all PLATFORM=generic LLVM=1 PLATFORM_RISCV_XLEN=32 + or + $ make all PLATFORM=generic LLVM=1 PLATFORM_RISCV_XLEN=64 + +下面简单说明一下编译设定: + +* `PLATFORM` 我们选择了支持 qemu 的 generic platform,关于 platform 的选择,大家可以参考 `docs/platform/platform.md`。 +* `LLVM` 则用于启用 llvm 编译器。 +* `PLATFORM_RISCV_XLEN` 用于指定 RV32 或 RV64。 + +编译结果保存在: + + $ tree build/platform/generic/firmware/ + build/platform/generic/firmware/ + ├── fw_dynamic.bin + ├── fw_dynamic.dep + ├── fw_dynamic.elf + ├── fw_dynamic.elf.ld + ├── fw_dynamic.o + ├── fw_jump.bin + ├── fw_jump.dep + ├── fw_jump.elf + ├── fw_jump.elf.ld + ├── fw_jump.o + ├── fw_payload.bin + ├── fw_payload.dep + ├── fw_payload.elf + ├── fw_payload.elf.ld + ├── fw_payload.o + └── payloads + ├── test.bin + ├── test.dep + ├── test.elf + ├── test.elf.ld + ├── test_head.dep + ├── test_head.o + ├── test_main.dep + ├── test_main.o + └── test.o + +编译后生成了多种不同类型的 firmware 文件,还有一个 payloads 目录。下面单独拿出一个小节来做介绍。 + +### Firmware 说明 + +为了兼容不同的运行需求,OpenSBI 支持三种类型的 Firmware,分别为: + +* dynamic:从上一级 Boot Stage 获取下一级 Boot Stage 的入口信息,以 `struct fw_dynamic_info` 结构体通过 `a2` 寄存器传递。 +* jump:假设下一级 Boot Stage Entry 为固定地址,直接跳转过去运行。 +* payload:在 jump 的基础上,直接打包进来下一级 Boot Stage 的 Binary。 + +下一级通常是 Bootloader 或 OS,比如 U-Boot,Linux。 + +Firmware 相关的源码在 OpenSBI 的 `firmware/` 目录下。 + +### 运行 + +OpenSBI 提供了直接运行的 make 入口: + + $ make run PLATFORM=generic + qemu-system-riscv64 -M virt -m 256M -nographic -bios /labs/linux-lab/src/examples/opensbi/build/platform/generic/firmware/fw_payload.elf + + OpenSBI v1.0-38-g794986f + ____ _____ ____ _____ + / __ \ / ____| _ \_ _| + | | | |_ __ ___ _ __ | (___ | |_) || | + | | | | '_ \ / _ \ '_ \ \___ \| _ < | | + | |__| | |_) | __/ | | |____) | |_) || |_ + \____/| .__/ \___|_| |_|_____/|____/_____| + | | + |_| + + Platform Name : riscv-virtio,qemu + Platform Features : medeleg + Platform HART Count : 1 + Platform IPI Device : aclint-mswi + Platform Timer Device : aclint-mtimer @ 10000000Hz + Platform Console Device : uart8250 + Platform HSM Device : --- + Platform Reboot Device : sifive_test + Platform Shutdown Device : sifive_test + Firmware Base : 0x80000000 + Firmware Size : 284 KB + Runtime SBI Version : 0.3 + + Domain0 Name : root + Domain0 Boot HART : 0 + Domain0 HARTs : 0* + Domain0 Region00 : 0x0000000002000000-0x000000000200ffff (I) + Domain0 Region01 : 0x0000000080000000-0x000000008007ffff () + Domain0 Region02 : 0x0000000000000000-0xffffffffffffffff (R,W,X) + Domain0 Next Address : 0x0000000080200000 + Domain0 Next Arg1 : 0x0000000082200000 + Domain0 Next Mode : S-mode + Domain0 SysReset : yes + + Boot HART ID : 0 + Boot HART Domain : root + Boot HART ISA : rv64imafdcsu + Boot HART Features : scounteren,mcounteren + Boot HART PMP Count : 16 + Boot HART PMP Granularity : 4 + Boot HART PMP Address Bits: 54 + Boot HART MHPM Count : 0 + Boot HART MIDELEG : 0x0000000000000222 + Boot HART MEDELEG : 0x000000000000b109 + + Test payload running + +`fw_payload.elf` 默认打包进去了 `build/platform/generic/firmware/payloads/test.bin`。如果需要打包 U-boot 或 Linux,可以参考 `docs/firmware/fw_payload.md` 传递 `FW_PAYLOAD_PATH` 参数指定需要打包 image 的路径,更详细用法可分别参考: + +* docs/firmware/payload_linux.md +* docs/firmware/payload_uboot.md + +`firmware/payloads/` 下的 test payload 是一个很好的 Next Boot Stage 的例子: + +* `test_head.S` - 做了一些执行 C 程序前的准备并跳转到 C 程序 +* `test.elf.ldS` - 链接脚本,涉及到 Payload 的内存装载位置, 由 `FW_TEXT_START + FW_PAYLOAD_OFFSET` 确定 +* `test_main.c` - C 程序,主要是通过 `ecall` 调用 OpenSBI 的字符串打印接口写了一行字符串 + +### 调试 + +如果要进一步深入分析 OpenSBI: + +* 一方面可以结合 cscope 等工具摸清各个函数之间的调用关系,进而理解 OpenSBI 的代码结构。 +* 另外一方面可以用 gdb + Qemu 研究 OpenSBI 的整个执行流程 + +`docs/platform/qemu_virt.md` 的最后有描述如何开展 OpenSBI 的调试,这里不做重述。 + +## 在 Linux Lab 下使用 OpenSBI + +### 更新 Linux Lab 中的 OpenSBI 固件 + +作为一套完善的内核实验框架,Linux Lab 为 RISC-V 提供了完整的 Linux 实验支持,提供了 2 套虚拟开发板:`riscv32/virt` 和 `riscv64/virt`。 + +其中,有用到 OpenSBI 作为 Bootloader。 + + $ cd /labs/linux-lab + $ grep BIOS -ur boards/riscv64/virt/Makefile + BIOS ?= $(BSP_BIOS)/opensbi/generic/fw_jump.elf + +其完整路径为: + + $ ls boards/riscv64/virt/bsp/bios/opensbi/generic/fw_jump.elf + boards/riscv64/virt/bsp/bios/opensbi/generic/fw_jump.elf + +在客制化 OpenSBI 以后,可以复制 OpenSBI build 目录中的 `fw_jump.elf` 到 Linux Lab 中的上述路径下进行使用。 + + $ cd /labs/linux-lab + $ cp src/examples/build/platform/generic/firmware/fw_jump.elf boards/riscv64/virt/bsp/bios/opensbi/generic/fw_jump.elf + +复制完以后就可以通过 `make boot` 引导 OpenSBI 和内核: + + $ make BOARD=riscv64/virt + $ make boot + ... + OpenSBI v1.0-38-g794986f + ____ _____ ____ _____ + / __ \ / ____| _ \_ _| + | | | |_ __ ___ _ __ | (___ | |_) || | + | | | | '_ \ / _ \ '_ \ \___ \| _ < | | + | |__| | |_) | __/ | | |____) | |_) || |_ + \____/| .__/ \___|_| |_|_____/|____/_____| + | | + |_| + + Platform Name : riscv-virtio,qemu + Platform Features : medeleg + Platform HART Count : 4 + Platform IPI Device : aclint-mswi + Platform Timer Device : aclint-mtimer @ 10000000Hz + Platform Console Device : uart8250 + Platform HSM Device : --- + Platform Reboot Device : sifive_test + Platform Shutdown Device : sifive_test + Firmware Base : 0x80000000 + Firmware Size : 308 KB + Runtime SBI Version : 0.3 + + Domain0 Name : root + Domain0 Boot HART : 0 + Domain0 HARTs : 0*,1*,2*,3* + Domain0 Region00 : 0x0000000002000000-0x000000000200ffff (I) + Domain0 Region01 : 0x0000000080000000-0x000000008007ffff () + Domain0 Region02 : 0x0000000000000000-0xffffffffffffffff (R,W,X) + Domain0 Next Address : 0x0000000080200000 + Domain0 Next Arg1 : 0x0000000082200000 + Domain0 Next Mode : S-mode + Domain0 SysReset : yes + + Boot HART ID : 0 + Boot HART Domain : root + Boot HART ISA : rv64imafdcsu + Boot HART Features : scounteren,mcounteren,time + Boot HART PMP Count : 16 + Boot HART PMP Granularity : 4 + Boot HART PMP Address Bits: 54 + Boot HART MHPM Count : 0 + Boot HART MIDELEG : 0x0000000000000222 + Boot HART MEDELEG : 0x000000000000b109 + Linux version 5.17.0-dirty (ubuntu@linux-lab) (riscv64-linux-gnu-gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #8 SMP Wed Mar 23 17:22:04 CST 2022 + OF: fdt: Ignoring memory range 0x80000000 - 0x80200000 + Machine model: riscv-virtio,qemu + efi: UEFI not found. + Zone ranges: + DMA32 [mem 0x0000000080200000-0x0000000087ffffff] + Normal empty + Movable zone start for each node + Early memory node ranges + node 0: [mem 0x0000000080200000-0x0000000087ffffff] + Initmem setup node 0 [mem 0x0000000080200000-0x0000000087ffffff] + SBI specification v0.3 detected + SBI implementation ID=0x1 Version=0x10000 + SBI TIME extension detected + SBI IPI extension detected + SBI RFENCE extension detected + SBI SRST extension detected + SBI HSM extension detected + riscv: ISA extensions acdfimsu + riscv: ELF capabilities acdfim + percpu: Embedded 16 pages/cpu s24792 r8192 d32552 u65536 + Built 1 zonelists, mobility grouping on. Total pages: 31815 + Kernel command line: route=172.20.169.140 iface=eth0 rw fsck.repair=yes rootwait root=/dev/vda console=ttyS0 + Unknown kernel command line parameters "route=172.20.169.140 iface=eth0", will be passed to user space. + ... + devtmpfs: mounted + Freeing unused kernel image (initmem) memory: 2140K + Hello, RISC-V Linux: https://gitee.com/tinylab/riscv-linux + Happy Hacking ................. + Run /sbin/init as init process + EXT4-fs (vda): re-mounted. Quota mode: disabled. + Starting syslogd: OK + Starting klogd: OK + Initializing random number generator... random: dd: uninitialized urandom read (512 bytes read) + done. + + Welcome to Linux Lab + linux-lab login: root + # + # uname -a + Linux linux-lab 5.17.0-dirty #8 SMP Wed Mar 23 17:22:04 CST 2022 riscv64 GNU/Linux + # poweroff + ... + The system is going down NOW! + Sent SIGTERM to all processes + Sent SIGKILL to all processes + Requesting system poweroff + reboot: Power down + +### 分析 Linux 内核中对 OpenSBI 的调用 + +类似上面的 test payload,在 Linux 内核中,也有通过 `ecall` 调用 OpenSBI 的服务。 + + $ cd src/linux-stable + $ find arch/riscv/ -name "*sbi*" + arch/riscv/include/asm/cpu_ops_sbi.h + arch/riscv/include/asm/kvm_vcpu_sbi.h + arch/riscv/include/asm/sbi.h + arch/riscv/kernel/cpu_ops_sbi.c + arch/riscv/kernel/sbi.c + arch/riscv/kvm/vcpu_sbi.c + arch/riscv/kvm/vcpu_sbi_base.c + arch/riscv/kvm/vcpu_sbi_hsm.c + arch/riscv/kvm/vcpu_sbi_replace.c + arch/riscv/kvm/vcpu_sbi_v01.c + +其中,核心的实现在 sbi.c: + + // arch/riscv/kernel/sbi.c: 25 + + 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; + } + EXPORT_SYMBOL(sbi_ecall); + +其中,`arg0-arg5` 作为参数,而 `ext` 和 `fid` 一起决定 OpenSBI 服务的 id,而返回的错误信息和返回值通过 `a0-a1` 取回。 + +关于服务的定义,可参考: + + // arch/riscv/include/asm/sbi.h: 13 + + #ifdef CONFIG_RISCV_SBI + enum sbi_ext_id { + #ifdef CONFIG_RISCV_SBI_V01 + 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_CLEAR_IPI = 0x3, + SBI_EXT_0_1_SEND_IPI = 0x4, + SBI_EXT_0_1_REMOTE_FENCE_I = 0x5, + SBI_EXT_0_1_REMOTE_SFENCE_VMA = 0x6, + SBI_EXT_0_1_REMOTE_SFENCE_VMA_ASID = 0x7, + SBI_EXT_0_1_SHUTDOWN = 0x8, + #endif + SBI_EXT_BASE = 0x10, + SBI_EXT_TIME = 0x54494D45, + SBI_EXT_IPI = 0x735049, + SBI_EXT_RFENCE = 0x52464E43, + SBI_EXT_HSM = 0x48534D, + SBI_EXT_SRST = 0x53525354, + + /* Experimentals extensions must lie within this range */ + SBI_EXT_EXPERIMENTAL_START = 0x08000000, + SBI_EXT_EXPERIMENTAL_END = 0x08FFFFFF, + + /* Vendor extensions must lie within this range */ + SBI_EXT_VENDOR_START = 0x09000000, + SBI_EXT_VENDOR_END = 0x09FFFFFF, + }; + + enum sbi_ext_base_fid { + SBI_EXT_BASE_GET_SPEC_VERSION = 0, + SBI_EXT_BASE_GET_IMP_ID, + SBI_EXT_BASE_GET_IMP_VERSION, + SBI_EXT_BASE_PROBE_EXT, + SBI_EXT_BASE_GET_MVENDORID, + SBI_EXT_BASE_GET_MARCHID, + SBI_EXT_BASE_GET_MIMPID, + }; + + enum sbi_ext_time_fid { + SBI_EXT_TIME_SET_TIMER = 0, + }; + + enum sbi_ext_ipi_fid { + SBI_EXT_IPI_SEND_IPI = 0, + }; + +其中,可以很容易看出 `ext` 和 `fid` 之间的关系,例如,在 base extension 下有 7 个 fid,而 time extension 下只有 1 个 fid,两者一起指定具体的服务函数,有点像 2 级映射。 + +具体地,timer 的 SBI 调用被封装成了: + + // arch/riscv/kernel/sbi.c: 240 + + static void __sbi_set_timer_v02(uint64_t stime_value) + { + #if __riscv_xlen == 32 + 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 + } + +需要注意地是,SBI 规范是不断演进的,可以看到内核代码中其实还有一个老的 `__sbi_set_timer_v01`,内核是通过 `sbi_probe_extension` 来探测 SBI 是否支持某个具体的 Extension: + + // arch/riscv/kernel/sbi.c: 580 + + /** + * sbi_probe_extension() - Check if an SBI extension ID is supported or not. + * @extid: The extension ID to be probed. + * + * Return: Extension specific nonzero value f yes, -ENOTSUPP otherwise. + */ + int 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) + if (ret.value) + return ret.value; + + return -ENOTSUPP; + } + EXPORT_SYMBOL(sbi_probe_extension); + +### OpenSBI 中如何提供服务 + +OpenSBI 通过 `lib/sbi` 注册了相关的 ecall 服务,全部加到了链表 `ecall_exts_list` 中: + + // lib/sbi/sbi_ecall.c: 144 + + int sbi_ecall_init(void) + { + int ret; + + /* The order of below registrations is performance optimized */ + ret = sbi_ecall_register_extension(&ecall_time); + if (ret) + return ret; + ret = sbi_ecall_register_extension(&ecall_rfence); + if (ret) + return ret; + ret = sbi_ecall_register_extension(&ecall_ipi); + if (ret) + return ret; + ret = sbi_ecall_register_extension(&ecall_base); + if (ret) + return ret; + ret = sbi_ecall_register_extension(&ecall_hsm); + if (ret) + return ret; + ret = sbi_ecall_register_extension(&ecall_srst); + if (ret) + return ret; + ret = sbi_ecall_register_extension(&ecall_pmu); + if (ret) + return ret; + ret = sbi_ecall_register_extension(&ecall_legacy); + if (ret) + return ret; + ret = sbi_ecall_register_extension(&ecall_vendor); + if (ret) + return ret; + + return 0; + } + +服务和 id 的映射由 `ecall_xxx` 对应的结构体 `struct sbi_ecall_extension` 中的 `extid_xxx` 和 `handle` 进行处理: + + // include/sbi/sbi_ecall.h: 23 + + struct sbi_ecall_extension { + struct sbi_dlist head; + unsigned long extid_start; + unsigned long extid_end; + int (* probe)(unsigned long extid, unsigned long *out_val); + int (* handle)(unsigned long extid, unsigned long funcid, + const struct sbi_trap_regs *regs, + unsigned long *out_val, + struct sbi_trap_info *out_trap); + }; + + // lib/sbi/sbi_ecall_replace.c: 22 + static int sbi_ecall_time_handler(unsigned long extid, unsigned long funcid, + const struct sbi_trap_regs *regs, + unsigned long *out_val, + struct sbi_trap_info *out_trap) + { + int ret = 0; + + if (funcid == SBI_EXT_TIME_SET_TIMER) { + #if __riscv_xlen == 32 + sbi_timer_event_start((((u64)regs->a1 << 32) | (u64)regs->a0)); + #else + sbi_timer_event_start((u64)regs->a0); + #endif + } else + ret = SBI_ENOTSUPP; + + return ret; + } + + struct sbi_ecall_extension ecall_time = { + .extid_start = SBI_EXT_TIME, + .extid_end = SBI_EXT_TIME, + .handle = sbi_ecall_time_handler, + }; + +而具体的服务定义在特定的文件中,比如 `lib/sbi/sbi_timer.c` 中定义 timer 相关的 SBI 服务。 + +### ecall 服务调用过程 + +当发起 Linux 的 `ecall` 调用后,这些 OpenSBI 中的服务具体是怎么触发的呢?这就涉及到如下过程: + +* 在 `firmware/fw_base.S` 中注册了 Machine Mode 的 trap handler,即 `sbi_trap_handler` +* 在 `lib/sbi/sbi_trap.c` 中定义了 `sbi_trap_handler`,处理各种 mcause,比如 Illegal Instructions,Misaligned Load & Store, Supervisor & Machine Ecall 等。 +* 在 `lib/sbi/sbi_ecall.c` 中定义了处理 ecall mcause 的 `sbi_ecall_handler`,它遍历上面 `ecall_exts_list` 中注册的各种 ecall 服务。 +* `sbi_ecall_handler` 根据 Linux 内核传递的 ext (extension id) 找到链表中对应的 ecall 服务,执行其中的 `handle` 函数,该函数根据 fid 执行具体的服务内容。 + +代码细节这里不做展开,大家打开相应源码,检索对应的关键字即可轻松找到。 + +## 小结 + +本文带领大家开展了基本的 OpenSBI 下载、编译、运行和调试实验,并进一步介绍了 Linux 内核如何调用 OpenSBI 的服务。 + +在这个基础上,大家就可以进一步探索更具体的服务实现甚至定制一些自己的服务啦。比如说,通过修改 `sbi_illegal_insn_handler` 在 OpenSBI 模拟处理器未实现的指令。 + +## 参考资料 + +* OpenSBI: docs/firmware/ +* OpenSBI: docs/platform/{generic.md,qemu_virt.md} diff --git a/articles/images/riscv_context-switch/satp-sv32.png b/articles/images/riscv_context-switch/satp-sv32.png new file mode 100644 index 0000000000000000000000000000000000000000..5d83de445fb6307f3b488db235b1ec782f0cf6bc Binary files /dev/null and b/articles/images/riscv_context-switch/satp-sv32.png differ diff --git a/articles/images/riscv_context-switch/satp-sv39.png b/articles/images/riscv_context-switch/satp-sv39.png new file mode 100644 index 0000000000000000000000000000000000000000..9d8b6fa34be223a7390c990cbf8b955323b1ff4e Binary files /dev/null and b/articles/images/riscv_context-switch/satp-sv39.png differ diff --git a/articles/images/riscv_specs/riscv-privilege-mode.png b/articles/images/riscv_specs/riscv-privilege-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..ff2f7ba2459d96e84da547196027f52e38e3ccfe Binary files /dev/null and b/articles/images/riscv_specs/riscv-privilege-mode.png differ diff --git a/meeting/README.md b/meeting/README.md index 44e63d31fb2a9c380e360acb8262dcb98f2e1385..aea026a953f43617d94d46ffa59cce6b0b3e35e4 100644 --- a/meeting/README.md +++ b/meeting/README.md @@ -15,8 +15,7 @@ 会议和直播均采用腾讯会议软件。 - 会议主题:RISC-V Linux 内核技术分享会 -- 会议时间:2022/03/26 20:30-21:30 中国标准时间 - 北京 -- 重复周期:2022/03/19-2022/06/04 20:30-21:30, 每周 (周六) +- 会议时间:2022/03/19-2022/06/04 20:30-21:30, 每周 (周六) - 参会方式 - 点击链接入会: - 腾讯会议入口:970-916-265 diff --git a/plan/README.md b/plan/README.md index 3f3020b60983ed36cdce651f609ef133b2d1004d..c9f5a13adedfa869a23fe198c22da3440ecc93fa 100644 --- a/plan/README.md +++ b/plan/README.md @@ -129,7 +129,7 @@ * Context switch @尹天宇 * Task implementation @tjytimi -* Multi-tasking +* Multi-tasking @Jack Y. ## User-space Support