系统调用接口

在编写用户程序时,最简单的调试方式就是输出信息。但是,用户程序如何才能输出信息呢?答案是通过系统调用。

系统调用是操作系统内核提供给用户程序的一组标准接口。通过系统调用,用户程序可以请求内核提供各种服务,例如文件读写、进程管理、网络通信等。系统调用是用户程序与内核之间通信的桥梁。

系统调用这一节中,我们已经讨论了系统调用的实现原理。在这一节中,我们将讨论用户程序如何调用系统调用。

实现接口

SYSCALL_WRITE 为例,我们来看看如何在用户态实现系统调用接口。当程序运行在用户态 (U mode) 时,可以通过 ecall 指令触发系统调用。因此,用户态的系统调用接口本质上就是通过 ecall 指令进入内核态,由内核处理系统调用请求。 ecall 指令会触发一个特殊的异常 (exception),称为环境调用 (environment call)。内核的异常处理程序可以识别这种异常,进一步根据 a7 寄存器的值确定具体的系统调用类型,并调用相应的处理函数。而在用户态,我们只需要将系统调用号放在 a7 寄存器,其他参数依次放在 a0~a6 寄存器中,然后执行 ecall 指令即可。下面是一个通用的系统调用接口实现:

fn syscall(id: usize, args: [usize; 7]) -> isize {
    let mut ret: isize;
    unsafe {
        asm!(
            "ecall",
            inlateout("a0") args[0] => ret,
            in("a1") args[1],
            in("a2") args[2],
            in("a3") args[3],
            in("a4") args[4],
            in("a5") args[5],
            in("a6") args[6],
            in("a7") id,
        );
    }
    ret
}

其中,id 表示系统调用号,需要与内核定义一致。args 是一个最多包含 7 个参数的数组,传递给内核的系统调用处理函数。

有了这个通用接口,我们可以进一步封装出更易用的系统调用函数。以 SYSCALL_WRITE 为例:

pub fn sys_write(fd: usize, buffer: &[u8]) -> isize {
    syscall(
        SYSCALL_WRITE,
        [fd, buffer.as_ptr() as usize, buffer.len(), 0, 0, 0, 0],
    )
}

类似的,SYSCALL_READ 系统调用的封装如下:

pub fn sys_read(fd: usize, buffer: &mut [u8]) -> isize {
    syscall(
        SYSCALL_READ,
        [fd, buffer.as_mut_ptr() as usize, buffer.len(), 0, 0, 0, 0],
    )
}

用户库

基于 sys_writesys_read 等系统调用封装函数,我们可以进一步实现一些常用的用户库函数,使得用户程序更方便地使用系统服务。例如,实现 putchargetchar 函数:

const STDIN: usize = 0;
const STDOUT: usize = 1;

pub extern "C" fn putchar(c: u8) {
    sys_write(STDOUT, &[c]);
}

pub extern "C" fn getchar() -> u8 {
    let mut buf = [0u8; 1];
    sys_read(STDIN, &mut buf);
    buf[0]
}

这样,用户程序就可以直接调用 putchargetchar 来输出/输入单个字符,而无需关心底层的系统调用细节。

此外,通常用户库也会实现一些更高级的接口,如 println! 宏、read_line 函数等,以提供更方便的功能。