rCore第三章
加载
我们需要将程序根据编号加载到内存的对应位置,那么OS
就可以根据编号选择对应的任务进行调度。由于不同程序所在的地址不同,所以链接文件的基地址也必须不同,这里使用一个python
脚本进行实现。
import os
base_address = 0x80400000
step = 0x20000
linker = 'src/linker.ld'
app_id = 0
apps = os.listdir('src/bin')
apps.sort()
for app in apps:
app = app[:app.find('.')]
lines = []
lines_before = []
with open(linker, 'r') as f:
for line in f.readlines():
lines_before.append(line)
lines = line.replace(hex(base_address), hex(base_address+step*app_id))
lines.append(line)
with open(linker, 'w+') as f:
f.writelines(lines)
os.system('cargo build --bin %s --release' % app)
print('[build.py] application %s start with address %s' % (app, hex(base_address+step*app_id)))
with open(linker, 'w+') as f:
f.writelines(lines_before)
app_id = app_id + 1
这个python
脚本对于每一个app
源文件都做相同的操作,将链接文件中的base_address
替换为app_id
对应的地址使用的地址,之后使用cargo build
进行编译,编译完成后就可以恢复链接文件。
编译这些编译完成后的二进制文件,需要加载到内存中
pub fn load_apps() {
extern "C" { fn _num_app(); }
let num_app_ptr = _num_app as usize as *const usize;
let num_app = get_num_app();
let app_start = unsafe {
core::slice::from_raw_parts(num_app_ptr.add(1), num_app+1)
};
for i in 0..num_app {
let base_i = get_base_i(i);
(base_i..base_i+APP_SIZE_LIMIT).for_each(|addr| unsafe {
(addr as *mut u8).write_volatile(0)
})
let src = unsafe {
core::slice::from_raw_parts(
app_start[i] as *const u8,
app_start[i+1] - app_start[i]
)
};
let dst = unsafe {
core::slice::from_raw_parts_mut(base_i as *mut u8, src.len())
};
dst.copy_from_slice(src);
}
unsafe { asm!("fence.i"::::"volatile");}
}
由于布局信息的格式从地址到高地址为,app_num
、第一个程序起始地址、第二个程序的起始地址、...、最后一个程序的起始地址、最后一个程序的结束地址。首先生成首地址,这里使用unsafe
去生成一个引用。这里OS
的视角最后看到的所有程序,会像一个数组一样分布在内存空间,做这个假设的前提是每个程序占用的空间相同,由于我们现在的程序都不大,所以可以通过一个APP_SIZE_LIMIT
进行限制。这个数组的每个区间一开始都要清零,之后通过如下关键语句将代码拷贝到每个区间的首地址。
let src = unsafe {
core::slice::from_raw_parts(
app_start[i] as *const u8,
app_start[i+1] - app_start[i]
)
};
let dst = unsafe {
core::slice::from_raw_parts_mut(base_i as *mut u8, src.len())
};
dst.copy_from_slice(src);
于是我们执行的时候就可以通过跳转到对应的区间入口,进行程序调度。
任务切换
一个任务在运行中途可以主动或者被动地交出执行权,为了保障程序的正确执行,我们需要保存其上下文。这里的上下文切换和trap
上下文切换不同,它不涉及特权级的转化,一部分寄存器的保存由编译器进行完成。这里的任务切换,实际上是在进入了trap
控制流后调用__switch
函数,这里的switch
和普通函数的区别就是其会换栈。
如上图所示就是TaskManager
的结构,每一个TaskControlBlock
负责一个任务的状态管理,而current
指向当前正在执行的任务。我们要进行任务切换时会执行类似如下的语句:
TaskContext *current_task_cx_ptr = &tasks[current].task_cx;
TaskContext *next_task_cx_ptr = &tasks[next].task_cx;
我们执行时在trap
中会进行一个换栈,将执行流转移到下一个任务,使用下一个任务的trap
上下文返回到用户态。具体执行的代码如下:
fn find_next_task(&self) -> Option<usize> {
let inner = self.inner.exclusive_access();
let current = inner.current_task;
(current+1..current+self.num_app+1)
.map(|id| id%self.num_app)
.find(|id| inner.tasks[*id].task_status == TaskStatus::Ready)
}
fn run_next_task(&self) {
if let Some(next) = self.find_next_task() {
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[next].task_status = TaskStatus::Running;
inner.current_task = next;
let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
drop(inner);
unsafe {
__switch(current_task_cx_ptr, next_task_cx_ptr);
}
} else {
println!("All applications completed");
use crate::board::QEMUExit;
crate::board::QEMU_EXIT_HANDLE.exit_success();
}
}
其中find_next_task
这个函数用于找到下一个状态为Ready
的任务, 并返回这个任务的id
。接下来对run_next_task
进行解析,这里使用let Some()
语法,直接返回的结果赋值到next
中,之后使用current_task_cx_ptr
和next_task_cx_ptr
表示当前任务和下一个任务的上下文,最后在unsafe
中调用__switch
函数切换上下文。
.section .text
.global __switch
__switch:
sd sp, 8(a0)
sd ra, 0(a0)
.set n, 0
.rept 12
SAVE_SN %n
.set n, n+1
.endr
ld ra, 0(a1)
.set n, 0
.rept 12
LOAD_SN %n
.set n, n+1
.endr
ld sp, 8(a1)
ret
这里ra
中最后将是一个任务的地址,ret
后将执行下一个任务。下图就是switch
过程发生的变化,switch
在执行load
后就转化到了另外一个任务的上下文,trap
上下文也是另外一个任务的内核栈。
第一次执行
第一次执行,我们需要自己构造任务的上下文,然后使用上一章就提到的restore
方法进行启动。在TaskManager
结构体中实现了方法run_first_task
:
fn run_first_task(&self) -> !{
let mut inner = self.inner.exclusive_access();
let task0 = &mut inner.task[0];
task0.task_status = TaskStatus::Running;
let next_task_cx_ptr = &task0.task_cx as *const TaskContext;
drop(inner);
let mut _unused = TaskContext::zero_init();
unsafe {
__switch(&mut _unused as *mut TaskContext, next_task_cx_ptr);
}
panic!("unreachable in run_first_task!");
}
这里直接使用了task[0]
,原因是TaskManager
使用了lazy_static!
第一次使用时会被初始化。
pub static ref TASK_MANAGER: TaskManager = {
let num_app = get_num_app();
let mut tasks = [TaskControlBlock{
task_cx: TaskContext::zero_init(),
task_status: TaskStatus::UnInit,
}; MAX_APP_NUM];
for (i, task) in tasks.iter_mut().enumerate(){
task.task_cx = TaskContext::goto_restore(init_app_cx(i));
task.task_status = TaskStatus::Ready;
}
TaskManager {
num_app,
inner: unsafe {
UPSafeCell::new(TaskManagerInner{
tasks,
current_task: 0,
})
},
}
};
其中TaskContext::goto_restore(init_app_cx(i))
用于初始化TaskContext
:
pub fn init_app_cx(app_id:usize) -> usize {
KERNEL_STACK[app_id].push_context(TrapContext::app_init_context(
get_base_i(app_id),
USER_STACK[app_id].get_sp(),
))
}
impl TaskContext {
//...
pub fn goto_restore(kstack_ptr: usize) -> Self {
extern "C" {
fn __restore();
}
Self {
ra: __restore as usize,
sp: kstack_ptr,
s: [0; 12],
}
}
}
这里引用第二章中的内核栈的解析图,如上图所示,在init_app_cx
中,执行了一个push_context
,这个函数用于将这个生成的上下文内容压入内核栈。但是这里我们需要的是任务上下文,所以有所不同,所以需要进一步的封装。
这里将entry
即ra
赋值为restore
的地址,同时kernel stack
区域即sp
赋值为内核栈指针当前位置。之后根据代码可以看到将每一个任务的状态设置为ready
后。最后对于整个TaskManager
进行初始化,用UnSafeCell
对于整个结构进行包裹。
有了这个任务上下文之后,回看之前的启动代码,就可以进行启动。
unsafe {
__switch(&mut _unused as *mut TaskContext, next_task_cx_ptr);
}
这里将task0
的上下文作为next_task_cx_ptr
,调用switch
之后就可以切换到task0
的上下文。之后由于将ra
的地址设置为了restore
,所以switch
的ret
之后会调用restore
。restore
回使用sp
作为内核栈的地址,将内核栈中的内容作为上下文进行还原,于是这个任务就启动了。
系统调用
这一章实现了两个系统调用,sys_yield
和sys_exit
。如下是两个系统调用的具体实现:
use crate::task::suspend_current_and_run_next;
pub fn sys_yield() -> isize {
suspend_current_and_run_next();
0
}
pub fn sys_exit(exit_code: i32) -> !{
println!("[kernel] Application exited with code {}", exit_code);
exit_current_and_run_next();
panic!("Unreachable in sys_exit!");
}
suspend_current_and_run_next()
和 exit_current_and_run_next
都调用了TaskManager
中的实现。
pub fn suspend_current_and_run_next() {
mark_current_suspend();
run_next_task();
}
pub fn exit_current_and_run_next() {
mark_current_exited();
run_next_task();
}
fn mark_current_suspended() {
TASK_MANAGER.mark_current_suspended();
}
fn mark_current_exited() {
TASK_MANAGER.mark_current_exited();
}
fn run_next_task() {
TASK_MANAGER.run_next_task();
}
impl TaskManager {
fn mark_current_suspended(&self) {
let mut inner = self.inner.borrow_mut();
let current = inner.current_task;
inner.tasks[current].task_status = TaskStatus::Ready;
}
fn mark_current_exited(&self){
let mut inner = self.inner.borrow_mut();
let current = inner.current_task;
inner.tasks[current].task_status = TaskStatus::Exited;
}
}
yield
和exit
的区别就是,yield
的任务只是将任务的状态设置为READY
,而exit
会将任务的状态设置为EXIT
导致任务不再被调度。最后这两个系统调用都会将执行权交给下一个任务。
时钟中断
这里时钟中断用于实现抢占式调度,抢占式调度可以使得应用随时被调度,而不用等待应用主动交出执行权。我们这里要求每个应用只能执行一段时间,之后内核就会强制将它切换出去。
我们使用时间片轮转调度的机制,这里主要依赖硬件提供的时间中断进行实现:
Interrupt | Exception Code | Description |
---|---|---|
1 | 1 | Supervisor software interrupt |
1 | 3 | Machine software interrupt |
1 | 5 | Supervisor timer interrupt |
1 | 7 | Machine timer interrupt |
1 | 9 | Supervisor external interrupt |
1 | 11 | Machine external interrupt |
而中断是否屏蔽会根据如下方式进行判断:
- 如果中断的特权级低于
CPU
当前的特权级,则该中断会被屏蔽; - 如果中断的特权级高于
CPU
当前特权级或者相同,则会通过sstatus
或者mstatus
进行判断。
我们在这里不考虑中断嵌套的情况,我们只考虑S
特权级的情况。
我们使用SBI
的来实现相关函数:
use riscv::register::time;
//获取mtime的计数值
pub fn get_time() -> usize {
time::read()
}
之后是设置时钟相关的部分:
const SBI_SET_TIMER: usize = 0;
pub fn set_timer(timer: usize) {
sbi_call(SBI_SET_TIMER, timer, 0, 0);
}
use crate::config::CLOCK_FREQ;
const TICKS_PER_SEC: usize = 100;
pub fn set_next_trigger() {
set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
}
这里的set_timer
或设置mtimecmp
,于是CLOCK_FREQ/TICKS_PER_SRC
就可以设置多少个mtime
可以触发一次时钟中断。
于是我们就可以编写时钟中断的处理函数:
match scause.cause() {
Trap::Interrupt(Interrupt::SupervisorTimer) => {
set_next_trigger();
suspend_current_and_run_next();
}
}
这里直接设置下一次触发时钟中断的时间,同时将当前任务设置为READY
并调度下一个任务即可。