571
社区成员
发帖
与我相关
我的任务
分享从4月份开课后,报名了操纵系统大赛,选题为基于rust语言来完成一系列基于RISCV指令集的操作系统,在课程实验中慢慢理解并从0到1开始实现了一些操作系统(从输出HelloWorld,到简单的批处理系统,再到带task概念的批处理系统,再到带task带虚拟内存空间的批处理系统),虽然比较遗憾没能完成预期目标(完成rCore系列学习并添加一些新的功能与优化),但还是有众多收获(操作系统开发经验,内核debug等),打算再考试结束后继续完成预期目标。
学习并重新实现了第0章到第4章的内容(从输出HelloWorld,到简单的批处理系统,再到带task概念的批处理系统,再到带task带虚拟内存空间的批处理系统),总结主要围绕目前最新进度的实现展开。
#[derive(Clone, Copy)]
pub struct Task
{
pub ctx: TaskContext,
pub status : TaskStatus,
pub satp : usize,
pub trapframe : PhyPage,
pub kernelstack_top : PhyPage,
}
ctx为当前任务的任务上下文,status代表了当前任务的状态,satp为当前任务对应的页表再内存中的地址,trapframe为当前任务的中断上下文,kernelstack_top为当前任务对应的内核栈在内存中的地址。
在riscv中,当中断或异常发生后,在用户模式下运行的控制流会跳转到stvec中指定的位置处开始运行,同时将权限提升为管理模式,在stvec处的代码负责保存中断上下文并为进入内核做好准备,最后进入内核。
中断上下文结构:
#[derive(Debug, Clone, Copy)]
pub struct Context
{
pub regs : [usize; 32],
pub sepc : usize,
pub sstatus : Sstatus,
pub pgt : usize,
pub kernel_stack : usize,
pub handler : usize,
pub trapframe : usize, //in user addr
}
regs为32个通用寄存器,sepc是进入中断之前用户程序的地址,sstatus为中断发生时程序状态字(类似于x86中的flag)。
pgt 为该任务对应的内核页表; kernel_stack为任务对应的内核栈; handler 为内核中中断异常处理代码的入口;trapframe为中断上下文在用户空间中的地址。下面是当中断异常发生时保存上下文并进入内核的riscv汇编:
.altmacro
.macro SAVE_REG n
sd x\n, \n*8(t6)
.endm
.macro LOAD_REG n
ld x\n, \n*8(a0)
.endm
.align 12
.section .text.trampoline
.globl trampoline
.globl usertrap
.globl trapret
.globl trampoline_end
trampoline:
usertrap:
csrrw t6, sscratch, t6
.set n, 0
.rept 31
SAVE_REG %n
.set n, n+1
.endr
csrr t0, sepc
csrr t1, sstatus
sd t0, 32*8(t6)
sd t1, 33*8(t6) //save sepc and sstatus
ld t0, 34*8(t6) // t0 points to kernel_pagetable
ld sp, 35*8(t6) // load the kernel_stack
ld t1, 36*8(t6) // load the traphander in t1
csrr t2, sscratch
sd t2, 31*8(t6) //save t6
csrw sscratch, t6 //sscratch points trapframe again
csrw satp, t0
sfence.vma zero, zero
jr t1
//trapret(user_trapframe, user_pagetable) a0 points to trapframe
usertrap_end:
大致思路为:在正式进入内核前,先保存31个通用寄存器,然后保存进入内核时的sepc和sstatus, 取出保存在trapframe中的kernel_stack写入sp, 取出内核中断异常处理程序地址放在t1中,取出内核页表地址并写入satp, 最后跳转到t1中的地址继续执行。
下面是任务上下文的定义:
#[derive(Copy, Clone, Debug)]
pub struct TaskContext
{
pub callee_save : [usize; 12],
pub sp : usize,
pub ra : usize,
}
callee_save是riscv中12个被调用者保存寄存器,因为其他寄存器如果有需要被保存的话,在调用切换函数前就已经保存在栈上了。sp是当前任务的内核栈,ra是调用切换函数前的返回地址。
在当前的设计中,内核空间与用户地址空间是分离的,内核存在一张页表中,而其他用户任务各自拥有一张自己地址空间的页表,内核页表被所有的用户任务共享,每一个用户任务在正式进入内核空间前会换上内核页表。
在离开内核空间的时候,可以获得当前被选中调度的进程的上下文来获得进程的页表,并在离开内核时写入satp寄存器中。
所以任务上下文除了在保存的一些寄存器之外,还有一个cur值,这个cur值指出当前运行在这个cpu上的任务在任务表中的下标,当需要获得当前运行在本cpu上的任务相关信息的时候,通过这个值可以找到该任务。这个值保存在一个全局结构中的一个域中。
struct InnerTaskManager
{
pub cur: usize,
pub tasks: [Task; APP_MAX_COUNT],
pub kernel_ctx: TaskContext,
}
在目前的实现中,对于任务的调度,使用的是时间片轮转调度算法。在内核一开始启动的时候,会选择一个任务开始运行,在过一段时间后发生时钟中断时,会选择另外一个任务开始运行,直到所有任务都退出后,内核才退出。
下面是内核开始任务调度的代码,get_next()方法返回下一个被调度上CPU的任务的下标,这里使用一个函数进行解耦合,方便后面有需要可以置换调度算法。
pub fn run_task(&self) -> !
{
loop
{
let next_task = self.get_next();
if let core::option::Option::Some(next_task_index) = next_task
{
println!("get task {}", next_task_index);
let mut tasks = self.tasks.access();
let saved = &mut tasks.kernel_ctx as *mut TaskContext;
let loaded = &(tasks.tasks[next_task_index].ctx) as *const TaskContext;
drop(tasks);
unsafe
{
switch(saved, loaded);
}
}
else
{
panic!("no more task\n");
}
}
}
下面是函数的具体实现:
fn get_next(&self) -> Option<usize>
{
let mut inner_tasks = self.tasks.access();
let mut cur = inner_tasks.cur;
for _ in 0..self.total
{
if let TaskStatus::Ready = inner_tasks.tasks[cur].status
{
inner_tasks.cur = cur;
return Some(cur);
}
cur = (cur + 1) % self.total;
}
None
}
它先获得当前正在运行的任务的下标值,然后开始遍历任务表,如果找得到一个任务的状态为ready的,则返回该下标,找不到返回None。
#[no_mangle]
pub fn trap_handler()
{
......
match scause.cause()
{
......
Trap::Interrupt(Interrupt::SupervisorTimer) =>
{
set_next_timer_intr_in_ms(10);
println!("timer interupt happen");
suspend();
}
......
}
before_trapret();
}
当定时器中断发生时,会调用trap_handler函数,在对定时器中断的处理中,先将下一次定时器的中断时间设置在10ms后,然后调用suspend()函数来暂停当前任务执行,并切换下一个任务。在suspend函数中会调用suspend_cur_task函数来完成这一操作。
pub fn suspend_cur_task(&self)
{
let mut tasks = self.tasks.access();
let cur = tasks.cur;
tasks.tasks[cur].status = TaskStatus::Ready;
tasks.cur = (cur + 1) % self.total;
let saved = &mut tasks.tasks[cur].ctx as *mut TaskContext;
let loaded = &tasks.kernel_ctx as *const TaskContext;
drop(tasks);
unsafe
{
switch(saved, loaded)
}
}
它将当前任务上下文地址作为被保存上下文地址, 把内核调度控制流的上下文地址作为被调度上cpu的上下文,并调用调度函数进行上下文切换。最后内核调度控制流进入get_next中选出下一次被调度上CPU的进程。
内核目前采用单一内核页表与多个用户内核页表的设计,用户程序进入内核时必须换上内核页表,离开时换上当前的用户内核页表。这样设计的一个缺点每次进入内核都需要有一次切换页表的开销,包括清空TLB和cache,开销相对较大。
用户程序的链接脚本如下:
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x00010000;
SECTIONS
{
. = BASE_ADDRESS;
.text : {
*(.text.entry)
*(.text .text.*)
}
. = ALIGN(4K);
.rodata : {
*(.rodata .rodata.*)
*(.srodata .srodata.*)
}
. = ALIGN(4K);
.data : {
*(.data .data.*)
*(.sdata .sdata.*)
}
. = ALIGN(4K);
.bss : {
sbss = .;
*(.bss .bss.*)
*(.sbss .sbss.*)
ebss = .;
}
. = ALIGN(4K);
/DISCARD/ : {
*(.eh_frame)
*(.debug*)
}
}
用户程序的地址从0X10000开始,依次放入代码段 只读数据段 全局数据段 和 bss段, 每个段以4KB,也就是一个物理页面大小进行对齐。
内核空间为直接映射,也就是在构造页表时,对于虚拟地址X,对应的物理地址也为X,不发生其他变化。
项目地址:ZYJ-33/my_os_proj: writing an os using rust (github.com)
参考资料:庖丁解牛Linux操作系统分析https://gitee.com/mengning997/linuxkernel
MIT 6.S081操作系统 6.S081 / Fall 2021 (mit.edu)
清华大学rCore系列 rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档 (rcore-os.github.io)
学号尾号:590