系统调用实现(RISC V)

在操作系统上,运行在用户态的程序在需要执行更高权限的指令时,需要通过系统调用交由操作系统内核执行,比如文件操作、进程控制等,以确保用户程序可以安全的访问硬件资源

系统调用依赖于中断来实现,关于RISC-V的中断机制,可以参考这篇文章

  • 应用程序代码调用标准库中的系统调用函数,该函数是对系统调用的包装
  • 调用函数准备好参数,并通过软中断切换到内核态
  • CPU执行中断处理程序,通过系统调用号决定进入某个系统调用处理函数
  • 系统调用处理函数完成操作
  • 通过标准库的封装,如write(),进入sys_write
  • sys_write()调用标准库的syscall()函数
  • syscall()函数通过ecall触发自陷异常,并将系统调用号和参数等存入寄存器中
  • syscall()通过ecall进入内核态(RISC-V下为S模式)之后,通过stvec中保存的自陷处理内核程序保存现场并调用trap_handler
  • trap_handler设置各种CSR的状态,并查找寄存器中的系统调用id,通过内核的syscall()进入具体的系统调用处理函数

具体实现来自rCore项目,我们根据调用过程逐步分析代码

//src/lib.rs
pub fn write(fd: usize, buf: &[u8]) -> isize {
    sys_write(fd, buf)
}
pub fn exit(exit_code: i32) -> isize {
    sys_exit(exit_code)
}
//src/syscall.rs
pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
    syscall(SYSCALL_WRITE, [fd, buffer.as_ptr() as usize, buffer.len()])
}

pub fn sys_exit(exit_code: i32) -> isize {
    syscall(SYSCALL_EXIT, [exit_code as usize, 0, 0])
}

用户通过标准库的接口执行syscall

//src/syscall.rs
fn syscall(id: usize, args: [usize; 3]) -> isize {
    let mut ret: isize;
    unsafe {
        asm!(
            "ecall",
            inlateout("x10") args[0] => ret,
            in("x11") args[1],
            in("x12") args[2],
            in("x17") id
        );
    }
    ret
}

标准库的syscall函数通过汇编保存返回地址,系统调用号和参数,并执行ecall切换到S模式

.altmacro
.macro SAVE_GP n
    sd x\n, \n*8(sp)
.endm
.macro LOAD_GP n
    ld x\n, \n*8(sp)
.endm
    .section .text
    .globl __alltraps
    .globl __restore
    .align 2
__alltraps:
    csrrw sp, sscratch, sp
    # now sp->kernel stack, sscratch->user stack
    # allocate a TrapContext on kernel stack
    addi sp, sp, -34*8
    # save general-purpose registers
    sd x1, 1*8(sp) # skip sp(x2), we will save it later
    sd x3, 3*8(sp)
    # skip tp(x4), application does not use it
    # save x5~x31
    .set n, 5
    .rept 27
        SAVE_GP %n
        .set n, n+1
    .endr
    # we can use t0/t1/t2 freely, because they were saved on kernel stack
    csrr t0, sstatus
    csrr t1, sepc
    sd t0, 32*8(sp)
    sd t1, 33*8(sp)
    # read user stack from sscratch and save it on the kernel stack
    csrr t2, sscratch
    sd t2, 2*8(sp)
    # set input argument of trap_handler(cx: &mut TrapContext)
    mv a0, sp
    call trap_handler

__restore:
    # case1: start running app by __restore
    # case2: back to U after handling trap
    mv sp, a0
    # now sp->kernel stack(after allocated), sscratch->user stack
    # restore sstatus/sepc
    ld t0, 32*8(sp)
    ld t1, 33*8(sp)
    ld t2, 2*8(sp)
    csrw sstatus, t0
    csrw sepc, t1
    csrw sscratch, t2
    # restore general-purpuse registers except sp/tp
    ld x1, 1*8(sp)
    ld x3, 3*8(sp)
    .set n, 5
    .rept 27
        LOAD_GP %n
        .set n, n+1
    .endr
    # release TrapContext on kernel stack
    addi sp, sp, 34*8
    # now sp->kernel stack, sscratch->user stack
    csrrw sp, sscratch, sp
    sret

当自陷发生时,__alltraps交换内核和用户程序的sp,在内核栈上新建一个TrapContent结构,保存通用寄存器和一些特殊寄存器的值并跳转到trap::trap_handler()

//src/trap/mod.rs
/// initialize CSR `stvec` as the entry of `__alltraps`
pub fn init() {
    extern "C" {
        fn __alltraps();
    }
    unsafe {
        stvec::write(__alltraps as usize, TrapMode::Direct);
    }
}

通过trap::init()设置stvec,将用户模式调用ecall时的自陷处理程序设置为__alltraps,即上面的代码

//src/trap/mod.rs
#[no_mangle]
/// handle an interrupt, exception, or system call from user space
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
    let scause = scause::read(); // get trap cause
    let stval = stval::read(); // get extra value
    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 in 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
            );
        }
    }
    cx
}

pub use context::TrapContext;

trap::trap_handler()

设置scause和stval寄存器,通过scause判断异常/中断类型,并通过用户态保存在x17的系统调用号调用内核的syscall函数进入某个具体的系统调用(如下)

//src/syscall/mod.rs
const SYSCALL_WRITE: usize = 64;
const SYSCALL_EXIT: usize = 93;


/// handle syscall exception with `syscall_id` and other arguments
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),
    }
}

进入具体实现后,一次内核调用便就此结束

const FD_STDOUT: usize = 1;

/// write buf of length `len`  to a file with `fd`
pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
    match fd {
        FD_STDOUT => {
            let slice = unsafe { core::slice::from_raw_parts(buf, len) };
            let str = core::str::from_utf8(slice).unwrap();
            print!("{}", str);
            len as isize
        }
        _ => {
            panic!("Unsupported fd in sys_write!");
        }
    }
}

此时内核将通过__restore恢复现场,并通过sret回到用户态

注意到trap::trap_handler()中的cx.sepc += 4;,此时系统将会回到用户程序ecall的后一条指令继续执行

Related Content