系统调用实现(RISC V)
Contents
说明
在操作系统上,运行在用户态的程序在需要执行更高权限的指令时,需要通过系统调用交由操作系统内核执行,比如文件操作、进程控制等,以确保用户程序可以安全的访问硬件资源
系统调用依赖于中断来实现,关于RISC-V的中断机制,可以参考这篇文章
系统调用大致流程
- 应用程序代码调用标准库中的系统调用函数,该函数是对系统调用的包装
- 调用函数准备好参数,并通过软中断切换到内核态
- CPU执行中断处理程序,通过系统调用号决定进入某个系统调用处理函数
- 系统调用处理函数完成操作
RISC-V的系统调用过程
- 通过标准库的封装,如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的后一条指令继续执行