trap初始化

​ 在主函数中清空bss段后,执行了三个函数trap::initbatch::initbatch::run_next_app。首先分析trap::init

pub fn init() {
    extern "C" {
        fn __alltraps();
    }
    
    unsafe {
        stvec::write(__alltraps as usize, TrapMode::Direct)
    }
}

​ 这段代码首先引入了在汇编代码中说明的__alltraps,这个是trap的入口,之后通过unsafe将这个值写入stvec寄存器,并且模式设置为direct,说明入口函数有且仅有一个。

batch初始化

batch模块中的init函数十分简单:

pub fn init() {
    print_app_info();
}

​ 之后我们可以看看是如何进行输出的,这个是当前模块中AppManager中的pub方法。

impl AppManager {
    pub fn print_app_info(&self){
        println!("[kernel] num_app = {}", self.num_app);
        for i in 0..self.num_app {
            println!(
            	"[kernel] app_{} [{:#x}, {:#x})",
                i,
                self.app_start[i],
                self.app_start[i+1]
            );
        }
    }
    
    //...
}

​ 这里对于AppManager的结构进行遍历,输出了各个app的地址范围。之所以没有初始化App仍旧能够输出,这里是因为使用lazy_static

lazy_static! {
    static ref APP_MANAGER: UPSafeCell<AppManager> = unsafe {
        UPSafeCell::new({
            extern "C"{
                fn _num_app();
            }
            let num_app_ptr = _num_app as usize as *const usize;
            let num_app = num_app_ptr.read_volatile();
            let mut app_start:[usize; MAX_APP_NUM+1] = [0; MAX_APP_NUM+1];
            let app_start_raw: &[usize] = core::slice::from_raw_parts[num_app_ptr.add(1), num_app+1];
            app_start[..=].copy_from_slice(app_start_raw);
            AppManager {
                num_app,
                current_app: 0,
                app_start,
            }
        })
    };
}

​ 在第一次使用时会进行初始化,这里根据地址值和长度生成一个引用的结构,因为rust中引用是一个固定长度的结构,由一个地址和一个长度构成,指向堆区。我们读出地址值作为*const usize,通过from_raw_part我们可以直接构造一个引用,最后间数据拷贝到我们的app_start中,最后对AppManager的进行初始化。

调度

​ 在main函数中,执行完前两个初始化函数后就会执行run_next_app(),它的内容如下:

pub fn run_next_app() -> !{
    let mut app_manager = APP_MANAGER.exclusive_access();
    let current_app = app_manager.get_cuurent_app();
    unsafe{
        app_manager.load_app(current_app);
    }
    app_manager.move_to_next_app();
    drop(app_manager);
    extern "C" {
        fn __restore(cx_addr: usize);
    }
    unsafe {
        __restore(KERNEL_STACK.push_context(TrapContext::app_init_context(
        	APP_BASE_ADDRESS,
            USER_STACK.get_sp(),
        )) as *const _ as usize);
    }
    panic!("Unreachable in batch::run_current_app!");
}

​ 这里调用了AppManagerexclusive_access方法,这个方法用于解决实现全局可访问的可变变量的问题。它的结构如下:

UPSafeCell

use core::cell::{RefCell, RefMut};

pub struct UPSafeCell<T> {
    inner: RefCell<T>,
}

unsafe impl<T> Sync for UPSafeCell<T> {}

impl<T> UPSafeCell<T> {
    pub unsafe fn new(value: T) -> Self {
        Self {
            inner: RefCell::new(value),
        }
    }
    
    pub fn exclusive_access(&self) -> RefMut<'_, T> {
        self.inner.borrow_mut()
    }
}

​ 我们不使用static mut这种使用方法是unsafe的,所以我们使用RefCell这个方法可以在运行时改变自己的可访问性,通过实现Sync向编译器说明,由我们保证并发时的安全。然后,我们实现exclusive_access方法,返回一个mut,如果同时有两个mut,就会发生panic,所以我们使用必须在申请使用完后,马上进行销毁。

加载运行

​ 获取到AppManager的结构体后,可以使用load_app方法将app加载到对应的位置。我们可以查看这个函数的实现

unsafe fn load_app(&self, app_id: usize) {
    if app_id >= self.num_app {
        println!("All application completed!");
        use crate::board::QEMUExit;
        crate::board::QEMU_EXIT_HANDLE.exit_success();
    }
    println!("[kernel] Loading app_{}", app_id);
    asm!("fence.i");
    core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(0);
    let app_src = core::slice::from_raw_parts(
    	self.app_start[app_id] as *const u8,
        self.app_start[app_id+1] - self.app_start[app_id],
    );
    let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len());
    app_dst.copy_from_slice(app_src);
}

​ 这里构造了两个引用进行数据拷贝,又有一个引用负责进行将程序运行的区域进行初始化归零。第一个:

core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(0);

​ 将这个区域置零,由于我们的测试程序都不大,可以通过一个APP_SIZE_LIMIT直接写死。

let app_src = core::slice::from_raw_parts(
    	self.app_start[app_id] as *const u8,
        self.app_start[app_id+1] - self.app_start[app_id],
    );
let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len());
app_dst.copy_from_slice(app_src);

​ 直接程序拷贝到其所要运行的地址0x80400000

​ 接着开始运行函数如下:

unsafe {
    __restore(KERNEL_STACK.push_context(TrapContext::app_init_context(
        	APP_BASE_ADDRESS,
            USER_STACK.get_sp(),
        )) as *const _ as usize);
}

​ 首先这里需要创建一个内核栈的上下文。

pub fn push_context(&self, cx: TrapContext) -> &'static mut TrapContext {
    let cx_ptr: *mut TrapContext = (self.get_sp() - core::mem::size_of::<TrapContext>()) as *mut TrapContext;
    unsafe {
        *cx_ptr = cx;
    }
    unsafe { cx_ptr.as_mut().unwrap() }
}

​ 这里在内核栈的空间上申请了一个上下文大小的空间,将这个空间赋值为cx即我们传入的结构体,之后将这个结构体返回。

​ 而这个结构体根据上方调用处的代码,是直接初始化的一个上下文结构。根据trap模块中的代码可以直接还原其结构,其中x2寄存器中存储内核栈的地址。

​ 于是__restore作为入口,通过a0传入内核栈中的上下文地址,加载上下文中的内容到寄存器,最后交换内核栈的地址到sscratch,并将用户栈指针写入sp, 将执行流转移到用户程序。

系统调用

​ 在我们的批处理操作系统中存在一个trap_handler, 一旦用户程序进行ecall,有的trap会被委托到S模式,于是根据scause我们可以具体判断要进行哪些处理。这里在第二章中,只用考虑UserEnvCall,首先sepc+4因为,系统调用结束要执行下一条置零,之后分析具体是哪个系统调用。

pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
    let scause = scause::read();
    let stval = stval::read();
    match scause.cause(){
        Trap::Exception(Exception::UserEnvCall) => {
            cx.sepc += 4;
            cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
        }
        Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
            println!("[kernel] PageFault application, kernel killed it.");
            run_next_app();
        }
        Trap::Exception(Exception::IllegalInstruction) => {
            println!("[kernel] IllegalInstruction in application, kernel killed it.");
            run_next_app();
        }
        _ => {
         	panic!(
                "Unsupported trap {:?}, stval = {:#x}!",
                scause.cause(),
                stval
            );   
        }
    }
}

​ 具体是哪个系统系统调用的判断位于syscall模块下,代码如下:

pub fn syscall(syscall_id: usize, args:[usize; 3]) -> isize {
    match syscall_id {
        SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
        SYSCALL_EXIT => sys_exit(args[0], as i32),
        _ => panic!("Unsupported syscall_id: {}", syscall_id),
    }
}

​ 根据系统调用号,可以具体判断执行哪个系统调用,write是进行输出,并返回输出长度。而sys_exit会进行run_next_app()

​ 在运行的app中,所有的app都通过如下的模式进行,所以每个程序退出都会自动执行下一个程序。

pin extern "C" fn _start() -> !{
    clear_bss();
    exit(main());
    panic!("unreachable after sys_exit!");
}

​ 我们可以再次考虑在系统调用的情况下上下文是如何保存和恢复的。我们首先将内核栈的地址转移到sp中,用户栈的地址转移到sscratch中,接着在内核栈中存储app的上下文。于是通过a0做作为上下文指针就可以将整个结构体传递给trap_handler。接着考虑如何进行恢复,返回时a0寄存器中的值是上下文在核内栈中的地址,根据这个上下文进行恢复即可。最后一步再次交换内核栈和用户栈的指针,就可恢复到用户栈。

__alltraps:
	csrrw sp, sscratch, sp
	sd x1, 1*8(sp)
	sd x3, 3*8(sp)
	.set n, 5
	.rept 27
		SAVE_GP %n
		.set n, n+1
	.endr
	csrr t0, sstatus
	csrr t1, sepc
	sd t0, 32*8(sp)
	sd t1, 33*8(sp)
	csrr t2, sscratch
	sd t2, 2*8(sp)
	mv a0, sp
	call trap_handler
	
__restore:
	mv sp, a0
	ld t0, 32*8(sp)
	ld t1, 33*8(sp)
	ld t2, 2*8(sp)
	csrw sstatus, t0
	csrw sepc, t1
	csrw sscratch, t2
	ld x1, 1*8(sp)
	ld x3, 3*8(sp)
	.set n, 5
	.rept 27
		LOAD_GP %n
		.set n, n+1
	.endr
	addi sp, sp, 34*8
	csrw sp, sscratch, sp
	sret