系统调用
系统调用,是用户态(U mode)的程序获取内核态(S mode)服务的方法,所以需要在用户态和内核态都加入对应的支持和处理。我们也可以认为用户态只是提供一个调用的接口,真正的处理都在内核态进行。
系统调用转发
首先我们在头文件里定义一些系统调用的编号。
// libs/unistd.h
#ifndef __LIBS_UNISTD_H__
#define __LIBS_UNISTD_H__
#define T_SYSCALL 0x80
/* syscall number */
#define SYS_exit 1
#define SYS_fork 2
#define SYS_wait 3
#define SYS_exec 4
#define SYS_clone 5
#define SYS_yield 10
#define SYS_sleep 11
#define SYS_kill 12
#define SYS_gettime 17
#define SYS_getpid 18
#define SYS_brk 19
#define SYS_mmap 20
#define SYS_munmap 21
#define SYS_shmem 22
#define SYS_putc 30
#define SYS_pgdir 31
/* SYS_fork flags */
#define CLONE_VM 0x00000100 // set if VM shared between processes
#define CLONE_THREAD 0x00000200 // thread group
#endif /* !__LIBS_UNISTD_H__ */
我们注意在用户态进行系统调用的核心操作是,通过内联汇编进行ecall
环境调用。这将产生一个trap, 进入S mode进行异常处理。
// user/libs/syscall.c
#include <defs.h>
#include <unistd.h>
#include <stdarg.h>
#include <syscall.h>
#define MAX_ARGS 5
static inline int syscall(int num, ...) {
//va_list, va_start, va_arg都是C语言处理参数个数不定的函数的宏
//在stdarg.h里定义
va_list ap; //ap: 参数列表(此时未初始化)
va_start(ap, num); //初始化参数列表, 从num开始
//First, va_start initializes the list of variable arguments as a va_list.
uint64_t a[MAX_ARGS];
int i, ret;
for (i = 0; i < MAX_ARGS; i ++) { //把参数依次取出
/*Subsequent executions of va_arg yield the values of the additional arguments
in the same order as passed to the function.*/
a[i] = va_arg(ap, uint64_t);
}
va_end(ap); //Finally, va_end shall be executed before the function returns.
asm volatile (
"ld a0, %1\n"
"ld a1, %2\n"
"ld a2, %3\n"
"ld a3, %4\n"
"ld a4, %5\n"
"ld a5, %6\n"
"ecall\n"
"sd a0, %0"
: "=m" (ret)
: "m"(num), "m"(a[0]), "m"(a[1]), "m"(a[2]), "m"(a[3]), "m"(a[4])
:"memory");
//num存到a0寄存器, a[0]存到a1寄存器
//ecall的返回值存到ret
return ret;
}
int sys_exit(int error_code) { return syscall(SYS_exit, error_code); }
int sys_fork(void) { return syscall(SYS_fork); }
int sys_wait(int pid, int *store) { return syscall(SYS_wait, pid, store); }
int sys_yield(void) { return syscall(SYS_yield);}
int sys_kill(int pid) { return syscall(SYS_kill, pid); }
int sys_getpid(void) { return syscall(SYS_getpid); }
int sys_putc(int c) { return syscall(SYS_putc, c); }
我们下面看看trap.c是如何转发这个系统调用的。
// kern/trap/trap.c
void exception_handler(struct trapframe *tf) {
int ret;
switch (tf->cause) { //通过中断帧里 scause寄存器的数值,判断出当前是来自USER_ECALL的异常
case CAUSE_USER_ECALL:
//cprintf("Environment call from U-mode\n");
tf->epc += 4;
//sepc寄存器是产生异常的指令的位置,在异常处理结束后,会回到sepc的位置继续执行
//对于ecall, 我们希望sepc寄存器要指向产生异常的指令(ecall)的下一条指令
//否则就会回到ecall执行再执行一次ecall, 无限循环
syscall();// 进行系统调用处理
break;
/*other cases .... */
}
}
// kern/syscall/syscall.c
#include <unistd.h>
#include <proc.h>
#include <syscall.h>
#include <trap.h>
#include <stdio.h>
#include <pmm.h>
#include <assert.h>
//这里把系统调用进一步转发给proc.c的do_exit(), do_fork()等函数
static int sys_exit(uint64_t arg[]) {
int error_code = (int)arg[0];
return do_exit(error_code);
}
static int sys_fork(uint64_t arg[]) {
struct trapframe *tf = current->tf;
uintptr_t stack = tf->gpr.sp;
return do_fork(0, stack, tf);
}
static int sys_wait(uint64_t arg[]) {
int pid = (int)arg[0];
int *store = (int *)arg[1];
return do_wait(pid, store);
}
static int sys_exec(uint64_t arg[]) {
const char *name = (const char *)arg[0];
size_t len = (size_t)arg[1];
unsigned char *binary = (unsigned char *)arg[2];
size_t size = (size_t)arg[3];
//用户态调用的exec(), 归根结底是do_execve()
return do_execve(name, len, binary, size);
}
static int sys_yield(uint64_t arg[]) {
return do_yield();
}
static int sys_kill(uint64_t arg[]) {
int pid = (int)arg[0];
return do_kill(pid);
}
static int sys_getpid(uint64_t arg[]) {
return current->pid;
}
static int sys_putc(uint64_t arg[]) {
int c = (int)arg[0];
cputchar(c);
return 0;
}
//这里定义了函数指针的数组syscalls, 把每个系统调用编号的下标上初始化为对应的函数指针
static int (*syscalls[])(uint64_t arg[]) = {
[SYS_exit] sys_exit,
[SYS_fork] sys_fork,
[SYS_wait] sys_wait,
[SYS_exec] sys_exec,
[SYS_yield] sys_yield,
[SYS_kill] sys_kill,
[SYS_getpid] sys_getpid,
[SYS_putc] sys_putc,
};
#define NUM_SYSCALLS ((sizeof(syscalls)) / (sizeof(syscalls[0])))
void syscall(void) {
struct trapframe *tf = current->tf;
uint64_t arg[5];
int num = tf->gpr.a0;//a0寄存器保存了系统调用编号
if (num >= 0 && num < NUM_SYSCALLS) {//防止syscalls[num]下标越界
if (syscalls[num] != NULL) {
arg[0] = tf->gpr.a1;
arg[1] = tf->gpr.a2;
arg[2] = tf->gpr.a3;
arg[3] = tf->gpr.a4;
arg[4] = tf->gpr.a5;
tf->gpr.a0 = syscalls[num](arg);
//把寄存器里的参数取出来,转发给系统调用编号对应的函数进行处理
return ;
}
}
//如果执行到这里,说明传入的系统调用编号还没有被实现,就崩掉了。
print_trapframe(tf);
panic("undefined syscall %d, pid = %d, name = %s.\n",
num, current->pid, current->name);
}
这样我们就完成了系统调用的转发。接下来就是在do_exit(), do_execve()
等函数中进行具体处理了。
do_execve()
do_execve()
我们看看do_execve()
函数
// kern/mm/vmm.c
bool user_mem_check(struct mm_struct *mm, uintptr_t addr, size_t len, bool write) {
//检查从addr开始长为len的一段内存能否被用户态程序访问
if (mm != NULL) {
if (!USER_ACCESS(addr, addr + len)) {
return 0;
}
struct vma_struct *vma;
uintptr_t start = addr, end = addr + len;
while (start < end) {
if ((vma = find_vma(mm, start)) == NULL || start < vma->vm_start) {
return 0;
}
if (!(vma->vm_flags & ((write) ? VM_WRITE : VM_READ))) {
return 0;
}
if (write && (vma->vm_flags & VM_STACK)) {
if (start < vma->vm_start + PGSIZE) { //check stack start & size
return 0;
}
}
start = vma->vm_end;
}
return 1;
}
return KERN_ACCESS(addr, addr + len);
}
// kern/process/proc.c
// do_execve - call exit_mmap(mm)&put_pgdir(mm) to reclaim memory space of current process
// - call load_icode to setup new memory space accroding binary prog.
int do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
struct mm_struct *mm = current->mm;
if (!user_mem_check(mm, (uintptr_t)name, len, 0)) { //检查name的内存空间能否被访问
return -E_INVAL;
}
if (len > PROC_NAME_LEN) { //进程名字的长度有上限 PROC_NAME_LEN,在proc.h定义
len = PROC_NAME_LEN;
}
char local_name[PROC_NAME_LEN + 1];
memset(local_name, 0, sizeof(local_name));
memcpy(local_name, name, len);
if (mm != NULL) {
cputs("mm != NULL");
lcr3(boot_cr3);
if (mm_count_dec(mm) == 0) {
exit_mmap(mm);
put_pgdir(mm);
mm_destroy(mm);//把进程当前占用的内存释放,之后重新分配内存
}
current->mm = NULL;
}
//把新的程序加载到当前进程里的工作都在load_icode()函数里完成
int ret;
if ((ret = load_icode(binary, size)) != 0) {
goto execve_exit;//返回不为0,则加载失败
}
set_proc_name(current, local_name);
//如果set_proc_name的实现不变, 为什么不能直接set_proc_name(current, name)?
return 0;
execve_exit:
do_exit(ret);
panic("already exit: %e.\n", ret);
}
kernel_execve()
kernel_execve()
那么我们如何实现kernel_execve()
函数?
能否直接调用do_execve()
?
// kern/process/proc.c
static int kernel_execve(const char *name, unsigned char *binary, size_t size) {
int64_t ret=0, len = strlen(name);
ret = do_execve(name, len, binary, size);
cprintf("ret = %d\n", ret);
return ret;
}
很不幸。这么做行不通。do_execve()
load_icode()
里面只是构建了用户程序运行的上下文,但是并没有完成切换。上下文切换实际上要借助中断处理的返回来完成。直接调用do_execve()
是无法完成上下文切换的。如果是在用户态调用exec()
, 系统调用的ecall
产生的中断返回时, 就可以完成上下文切换。
由于目前我们在S mode下,所以不能通过ecall
来产生中断。我们这里采取一个取巧的办法,用ebreak
产生断点中断进行处理,通过设置a7
寄存器的值为10说明这不是一个普通的断点中断,而是要转发到syscall()
, 这样用一个不是特别优雅的方式,实现了在内核态使用系统调用。
// kern/process/proc.c
// kernel_execve - do SYS_exec syscall to exec a user program called by user_main kernel_thread
static int kernel_execve(const char *name, unsigned char *binary, size_t size) {
int64_t ret=0, len = strlen(name);
asm volatile(
"li a0, %1\n"
"lw a1, %2\n"
"lw a2, %3\n"
"lw a3, %4\n"
"lw a4, %5\n"
"li a7, 10\n"
"ebreak\n"
"sw a0, %0\n"
: "=m"(ret)
: "i"(SYS_exec), "m"(name), "m"(len), "m"(binary), "m"(size)
: "memory"); //这里内联汇编的格式,和用户态调用ecall的格式类似,只是ecall换成了ebreak
cprintf("ret = %d\n", ret);
return ret;
}
// kern/trap/trap.c
void exception_handler(struct trapframe *tf) {
int ret;
switch (tf->cause) {
case CAUSE_BREAKPOINT:
cprintf("Breakpoint\n");
if(tf->gpr.a7 == 10){
tf->epc += 4; //注意返回时要执行ebreak的下一条指令
syscall();
}
break;
/* other cases ... */
}
}
注意我们需要让CPU进入U mode执行do_execve()
加载的用户程序。进行系统调用sys_exec
之后,我们在trap返回的时候调用了sret
指令,这时只要sstatus
寄存器的SPP
二进制位为0,就会切换到U mode,但SPP
存储的是“进入trap之前来自什么特权级”,也就是说我们这里ebreak之后SPP
的数值为1,sret之后会回到S mode在内核态执行用户程序。所以load_icode()
函数在构造新进程的时候,会把SSTATUS_SPP
设置为0,使得sret
的时候能回到U mode。
最后更新于