本系列参考: 学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春 整理而来,主要作为xv6操作系统学习的一个前置基础。
RVOS是本课程基于RISC-V搭建的简易操作系统名称。
课程代码和环境搭建教程参考github仓库: https://github.com/plctlab/riscv-operating-system-mooc/blob/main/howto-run-with-ubuntu1804_zh.md
前置知识:
所谓任务,简单来说就是一个函数的调用过程,也就是一个指令的执行流,也就是运行起来的程序,也就是我们常说的一个进程或者线程的概念。
多任务就是存在多个核并行执行多个指令流的过程:
本节中使用单核配合分时复用,完成多个任务的切换运行。
由于一个核通常关联一套通用寄存器,每个进程运行时,都会使用寄存器来保持当前执行流中相关变量的值,所以当进程A需要切换到进程B执行时,就需要将通用寄存器的值保存到内存中进程A对应的Context对象中,而将进程B关联的Context对象中保存的寄存器z值恢复到当前通用寄存器上:
/* task management */
struct context {
/* ignore x0 */
reg_t ra;
reg_t sp;
reg_t gp;
reg_t tp;
reg_t t0;
reg_t t1;
reg_t t2;
reg_t s0;
reg_t s1;
reg_t a0;
reg_t a1;
reg_t a2;
reg_t a3;
reg_t a4;
reg_t a5;
reg_t a6;
reg_t a7;
reg_t s2;
reg_t s3;
reg_t s4;
reg_t s5;
reg_t s6;
reg_t s7;
reg_t s8;
reg_t s9;
reg_t s10;
reg_t s11;
reg_t t3;
reg_t t4;
reg_t t5;
reg_t t6;
};
上面代码存在于03节的os.h文件中
本节我们来讲解一下协作式多任务的实现流程,首先我们先来复习一下call指令和ret指令:
call label
label
是目标子程序的标签。call
指令会将当前指令的下一条指令的地址保存到链接寄存器(link register)中,并跳转到目标子程序的地址。ra
(x1),它通常用于保存函数的返回地址。因此,在执行 call
指令之前,程序需要将函数的参数准备好,并将它们存储在适当的寄存器中。ret
指令返回到调用位置。ret
指令会从链接寄存器中获取保存的返回地址,并跳转到该地址继续执行。call
和 ret
指令没有显式地处理参数传递和局部变量的保存。这些任务通常通过约定和编程规范来实现。例如,参数可以通过寄存器传递,而局部变量可以在堆栈上分配和访问。编程者需要根据具体的编程规范和约定来管理参数和局部变量的传递。下面我们先来看看任务切换的实现流程:
我们可以在程序中调用switch_to函数,手动完成任务的切换,由于任务切换十分频繁,所以这里使用汇编来实现switch_to函数:
# void switch_to(struct context *next);
# a0: pointer to the context of the next task
# 汇编编写的switch_to函数可以看做c语言中的: void switch_to(struct context *next);
# 其中函数有一个context指针作为参数,而实际上由a0作为参数寄存器,存放该context指针的值
.globl switch_to
.align 4
switch_to:
# 交换mscratch和t6寄存器的值--t6指向进程的Context上下文地址
csrrw t6, mscratch, t6 # swap t6 and mscratch
# 判断switch_to函数是否是首次调用(t6==0),如果是则跳到标签1处执行
beqz t6, 1f # Note: the first time switch_to() is
# called, mscratch is initialized as zero
# (in sched_init()), which makes t6 zero,
# and that's the special case we have to
# handle with t6
# 调用宏保存通用寄存器
reg_save t6 # save context of prev task
# Save the actual t6 register, which we swapped into
# mscratch
# t5承担t6的职责,指向当前进程的Context上下文地址
mv t5, t6 # t5 points to the context of current task
# 恢复t6寄存器原本的值
csrr t6, mscratch # read t6 back from mscratch
# 将t6的值保存到Context上下文中
sw t6, 120(t5) # save t6 with t5 as base
1:
# switch mscratch to point to the context of the next task
# 将mscratch指向要切换的上下文地址
csrw mscratch, a0
# Restore all GP registers
# Use t6 to point to the context of the new task
# t6寄存器指向要切换的上下文地址
mv t6, a0
# 将t6指向的上下文中的寄存器值进行恢复操作
reg_restore t6
# Do actual context switching.
ret
隐藏操作:
问题解答:
# save all General-Purpose(GP) registers to context
# struct context *base = &ctx_task;
# base->ra = ra;
# ......
.macro reg_save base
sw ra, 0(\base)
sw sp, 4(\base)
sw gp, 8(\base)
sw tp, 12(\base)
sw t0, 16(\base)
sw t1, 20(\base)
sw t2, 24(\base)
sw s0, 28(\base)
sw s1, 32(\base)
sw a0, 36(\base)
sw a1, 40(\base)
sw a2, 44(\base)
sw a3, 48(\base)
sw a4, 52(\base)
sw a5, 56(\base)
sw a6, 60(\base)
sw a7, 64(\base)
sw s2, 68(\base)
sw s3, 72(\base)
sw s4, 76(\base)
sw s5, 80(\base)
sw s6, 84(\base)
sw s7, 88(\base)
sw s8, 92(\base)
sw s9, 96(\base)
sw s10, 100(\base)
sw s11, 104(\base)
sw t3, 108(\base)
sw t4, 112(\base)
sw t5, 116(\base)
# we don't save t6 here, due to we have used
# it as base, we have to save t6 in an extra step
# outside of reg_save
.endm
# restore all General-Purpose(GP) registers from the context
# struct context *base = &ctx_task;
# ra = base->ra;
# ......
.macro reg_restore base
lw ra, 0(\base)
lw sp, 4(\base)
lw gp, 8(\base)
lw tp, 12(\base)
lw t0, 16(\base)
lw t1, 20(\base)
lw t2, 24(\base)
lw s0, 28(\base)
lw s1, 32(\base)
lw a0, 36(\base)
lw a1, 40(\base)
lw a2, 44(\base)
lw a3, 48(\base)
lw a4, 52(\base)
lw a5, 56(\base)
lw a6, 60(\base)
lw a7, 64(\base)
lw s2, 68(\base)
lw s3, 72(\base)
lw s4, 76(\base)
lw s5, 80(\base)
lw s6, 84(\base)
lw s7, 88(\base)
lw s8, 92(\base)
lw s9, 96(\base)
lw s10, 100(\base)
lw s11, 104(\base)
lw t3, 108(\base)
lw t4, 112(\base)
lw t5, 116(\base)
lw t6, 120(\base)
.endm
mscratch 是 RISC-V 架构中的一个控制和状态寄存器(Control and Status Register),用于保存机器模式下的临时数据或上下文相关的信息。它的作用是提供一个通用的、临时的存储位置,供软件使用。
具体而言,mscratch 寄存器通常用于以下情况:
需要注意的是,mscratch 寄存器的使用是由软件决定的,它没有特定的预定义用途。软件可以根据需要将 mscratch 寄存器用于临时存储和处理数据。然而,由于 mscratch 寄存器的值可能会被上下文切换或其他操作修改,因此软件在使用 mscratch 寄存器时应注意保存和恢复其中的数据。
总结:mscratch 寄存器是 RISC-V 架构中的一个控制和状态寄存器,用于保存机器模式下的临时数据或上下文相关的信息。它可以用于上下文切换、异步事件处理、调试和跟踪等情况,提供一个通用的临时存储位置供软件使用。
在汇编语言中,标签通常以 .
或一个数字开头,并可以在其后加上后缀来表示不同的类型。
后缀 f
表示前向引用(forward reference)。在这种情况下,数字后面的 f
表示标签是前向引用,即在当前位置之后定义的标签。这种用法允许在代码中跳转到稍后定义的标签。
在给定的示例中,1f
表示跳转到标签 1
所在的位置,而 1
是在当前位置之后定义的标签。这样的标签定义可以简化代码中的跳转逻辑。
#define STACK_SIZE 1024
//当前任务的函数栈,用于存放任务进行函数调用时的栈帧
uint8_t task_stack[STACK_SIZE];
//当前任务上下文
struct context ctx_task;
static void w_mscratch(reg_t x){
asm volatile("csrw mscratch, %0" : : "r" (x));
}
void user_task0(void);
//调度任务初始化
void sched_init(){
//初始化mscratch为0
w_mscratch(0);
//初始化第一个任务的栈地址
ctx_task.sp = (reg_t) &task_stack[STACK_SIZE - 1];
//保存任务1的地址到当前任务上下文的ra寄存器中(内存中)
ctx_task.ra = (reg_t) user_task0;
}
/*
* a very rough implementaion, just to consume the cpu
*/
void task_delay(volatile int count)
{
count *= 50000;
while (count--);
}
void user_task0(void)
{
//通过串口输出任务创建信息
uart_puts("Task 0: Created!\n");
//每隔1s输出一条信息
while (1) {
uart_puts("Task 0: Running...\n");
task_delay(1000);
}
}
首先,start.s汇编文件中,会对每个hart的栈空间进行隔离设置:
#include "platform.h"
# 每个硬件线程的栈大小为1024字节
.equ STACK_SIZE, 1024
# 声明符号 _start 为全局符号。它是程序的入口点。
.global _start
# 指定以下代码属于 .text 段,其中包含可执行指令。它标志着 _start 代码的开始。
.text
_start:
# park harts with id != 0
csrr t0, mhartid # 读取当前硬件线程的ID
mv tp, t0 # 将CPU的硬件线程ID保存在tp寄存器中以备后用
bnez t0, park # 如果不是硬件线程0,则进入休眠状态
# Set all bytes in the BSS section to zero.
# 将_bss_start标签的地址加载到寄存器a0中
la a0, _bss_start
# 将_bss_end标签的地址加载到寄存器a1中
la a1, _bss_end
# 无符号比较a0和a1的值,如果a0大于或等于a1,则跳转到标签2f处。这个条件判断的目的是确保_bss_start的地址小于_bss_end的地址,以避免处理一个空的BSS段。
bgeu a0, a1, 2f
# 循环,用于将BSS(Block Started by Symbol)段中的字节清零。它使用了一个带有标签的循环,逐个访问BSS段中的字节,并将其置为零。
1:
# 将零值(使用寄存器zero)存储到地址a0指向的内存位置,即将BSS段中的一个字节置为零
sw zero, (a0)
# 将寄存器a0的值增加4个字节的偏移量,即将a0更新为指向下一个字节。
addi a0, a0, 4
# 无符号比较寄存器a0和a1的值,如果a0小于a1,则跳转到标签1b处继续执行循环。这个条件判断的目的是检查是否遍历完了BSS段中的所有字节。
bltu a0, a1, 1b
2:
slli t0, t0, 10 # 将硬件线程ID左移10位(相当于乘以1024)
la sp, stacks + STACK_SIZE # 将初始栈指针设置为第一个栈空间的末尾
add sp, sp, t0 # 将当前硬件线程的栈指针移动到栈空间中的相应位置
j start_kernel # hart 0 jump to c
park:
wfi
j park
stacks:
.skip STACK_SIZE * MAXNUM_CPU # 为所有硬件线程分配栈空间
.end # End of file
BSS段用于存放未初始化的全局变量和静态遍历的区域,清零是为了确保未初始化这个条件。
内核程序的栈空间,从物理内存最高地址处向下延伸。
这上面的内容在课程第一节中就进行了分析,那么目前还差什么呢?
void schedule()
{
//获取要切换执行的任务上下文地址
struct context *next = &ctx_task;
//调用switch_to函数进行任务切换
switch_to(next);
}
内核启动:
void start_kernel(void){
uart_init();
uart_puts("Hello, RVOS!\n");
page_init();
//1号调度任务初始化
sched_init();
//切换到1号任务执行
schedule();
//如果下面这段文字输出了,说明任务切换实现的有bug
uart_puts("Would not go here!\n");
while (1) {}; // stop here!
}
测试:
到目前为止,我们只实现了操作系统启动后切换到1号任务执行的效果,还无法实现多任务切换,本节我们在上一节的基础上进行改进,实现多任务切换效果:
//这里我们最多支持创建10个任务
#define MAX_TASKS 10
#define STACK_SIZE 1024
uint8_t task_stack[MAX_TASKS][STACK_SIZE];
struct context ctx_tasks[MAX_TASKS];
/*
* _top is used to mark the max available position of ctx_tasks
* _current is used to point to the context of current task
*/
static int _top = 0;
static int _current = -1;
/*
* implment a simple cycle FIFO schedular
*/
void schedule()
{
if (_top <= 0) {
panic("Num of task should be greater than zero!");
return;
}
_current = (_current + 1) % _top;
struct context *next = &(ctx_tasks[_current]);
switch_to(next);
}
/*
* DESCRIPTION
* task_yield() causes the calling task to relinquish the CPU and a new
* task gets to run.
*/
void task_yield()
{
schedule();
}
void sched_init()
{
w_mscratch(0);
}
/*
* DESCRIPTION
* Create a task.
* - start_routin: task routine entry
* RETURN VALUE
* 0: success
* -1: if error occured
*/
//传入任务启动函数
int task_create(void (*start_routin)(void))
{
//在任务没有超过最大限制的情况下
if (_top < MAX_TASKS) {
//初始化新任务的上下文
ctx_tasks[_top].sp = (reg_t) &task_stack[_top][STACK_SIZE - 1];
ctx_tasks[_top].ra = (reg_t) start_routin;
_top++;
return 0;
} else {
return -1;
}
}
#include "os.h"
#define DELAY 1000
void user_task0(void)
{
uart_puts("Task 0: Created!\n");
while (1) {
uart_puts("Task 0: Running...\n");
task_delay(DELAY);
task_yield();
}
}
void user_task1(void)
{
uart_puts("Task 1: Created!\n");
while (1) {
uart_puts("Task 1: Running...\n");
task_delay(DELAY);
task_yield();
}
}
/* NOTICE: DON'T LOOP INFINITELY IN main() */
void os_main(void)
{
task_create(user_task0);
task_create(user_task1);
}
void start_kernel(void)
{
uart_init();
uart_puts("Hello, RVOS!\n");
page_init();
sched_init();
os_main();
schedule();
uart_puts("Would not go here!\n");
while (1) {}; // stop here!
}
测试: