第一章 语言的切换
在RISC-V中,每个物理内核通常被称为hart(硬件线程),复位后所有hart都会从同一入口地址(例如0x80000000)开始执行。为了简化我们的工作,我们的目标是设计在一个hart中·运行的内核,那么就需要关停其他hart。
要关停其他物理内核,其实很简单,我们只需要从多个hart中选择一个使用就行。假设我们选择ID为0的hart为我们要用的hart,可以使用特权指令 csrr 将hart id读取到 t0 中,然后将t0的值赋值到 tp 中,接着使用 benz 指令判断 t0 内的值是不是0,不是就跳转到park块。park是一个中断死循环,内有 wfi 指令。之所以要把hartid存到 tp 中,是因为 tp 寄存器在当前上下文中被临时用作“当前 hart ID 的缓存。这样后续代码想判断自己是哪个 hart 时,直接读 tp 即可,无需再次执行 csrr, csrr作为特权指令相对来说时间花销比较大,存在 tp 中后续调用就非常快了。另外需要指出的是,现在用的寄存器不是所有hart共用的全局寄存器,而是私有的寄存器。
具体的汇编代码如下:
csrr t0, mhartid # 读取当前 hart 的 ID
mv tp, t0 # 将 hart id 存入 tp 寄存器
bnez t0, park # 如果 hart id != 0,则跳转到 park 循环
park块的设计主要是中断休眠和循环:
park:
wfi
j park
现在关停了其他的hart之后,还需要给每个hart初始化,使其栈指针 tp 能够指向对应的栈顶的位置。虽然我们现在不使用其hart,但是万一后面我们需要将操作系统从单核升级到多核,这一步骤是可以提前做的。tp 需要指向物理内存上的最低位。另外,预留的空间需要给所有的hart用。因此,每个hart都要有自己的栈空间。我们假设给每一个hart预留1024B的大小。如下图所示:

slli t0, t0, 10 # t0 = hart_id * 1024
la sp, stacks + STACK_SIZE # sp = stacks + STACK_SIZE(第一个 hart 的栈顶)
add sp, sp, t0 # sp = stacks + STACK_SIZE + hart_id * 1024
上一段提到为所有 hart 分配了一块连续的栈内存区域,代码如下:
.balign 16 # 使下一个符号(stacks)的地址是 16 的整数倍
stacks:
.skip STACK_SIZE * MAXNUM_CPU # 为所有 hart 预留连续的栈空间
.end # 文件结束
这些准备工作就绪了之后,我们就可以从繁琐的汇编语言编程切换到c语言的编程了,只需要使用 j 命令即可跳转到c语言的入口。
j start_kernel # 跳转到 C 内核入口
完整的代码如下:
// 包含平台相关的头文件,定义了 MAXNUM_CPU 等常量
#include "../../platform.h"
# size of each hart's stack is 1024 bytes
.equ STACK_SIZE, 1024 # 给 STACK_SIZE 这个名称赋值为 1024
.global _start
# .global 或 .globl 是汇编伪指令,用于将一个符号声明为全局可见,使链接器能够识别它。
.text
_start:
# park harts with id != 0
csrr t0, mhartid # read current hart id
mv tp, t0 # keep CPU's hartid in its tp for later usage.
# 将 hartid 保存在 tp 寄存器(线程指针)中,
# 后续 C 代码可通过 tp 获取当前核心编号
# 每个硬件线程(hart)都有自己独立的一套寄存器,包括 t0 和 tp。
bnez t0, park # if we're not on the hart 0
# we park the hart
# 如果 hartid 不为 0(即不是主核),则跳转到
# park 循环,让该核进入休眠
# Setup stacks, the stack grows from bottom to top, so we put the
# stack pointer to the very end of the stack range.
# 栈在内存中是从低地址向高地址增长的,但 RISC-V 的栈是向下增长(从高地址向低地址):
# 入栈时 sp 先减小,然后存放数据。所以初始 sp 应指向栈区域的最高地址。
slli t0, t0, 10 # shift left the hart id by 1024
# 将寄存器 t0 中的值左移 10 位,空出的低位补 0,然后将结果写回 t0。
la sp, stacks + STACK_SIZE # set the initial stack pointer
# to the end of the first stack space
# 实际效果:sp = &stacks + STACK_SIZE(即 stacks 符号的地址加上偏移量 STACK_SIZE)
add sp, sp, t0 # move the current hart stack pointer
# to its place in the stack space
j start_kernel # hart 0 jump to c
park:
wfi #(Wait For Interrupt):RISC-V 特权指令,使当前硬件线程(hart)进入低功耗待机状态,直到收
# 到一个中断(或调试请求)才被唤醒。
j park # jump 指令 用来跳转的,跳转到park等于死循环
# In the standard RISC-V calling convention, the stack pointer sp
# is always 16-byte aligned.
.balign 16 # 使下一个符号(stacks)的地址是 16 的整数倍。
stacks:
.skip STACK_SIZE * MAXNUM_CPU # allocate space for all the harts stacks
# .skip(也称 .space):在 BSS 段中保留指定字节数,不初始化。
# 栈区域起始地址为 stacks。
# 第 i 个 hart 的栈顶通常计算为:stacks + (i+1) * STACK_SIZE
.end # End of file
@ .text(代码段)只需要 可执行 + 可读,不应允许写入(防止代码被篡改或注入)。
@ .rodata(只读数据,如字符串常量、const 变量)只需 可读,写操作会触发异常。
@ .data(已初始化全局变量)需要 可读可写。
@ .bss(未初始化全局变量)同样需要 可读可写,但初始值为零。

浙公网安备 33010602011771号