第2篇:系统调用
在操作系统中,系统调用是从应用程序角度理解操作系统的关键。借助系统调用,我们可以从应用程序进入操作系统,而这个过程其实也很简单,就是把系统调用的参数放到寄存器中,然后程序把控制权完全交给操作系统,操作系统可以改变程序状态甚至终止程序。
我们来看看xv6中的系统调用。
查看
kernel/syscall.h
,我们可以看到xv6定义了21个系统调用,以及他们所对应的编号。// System call numbers
#define SYS_fork 1
#define SYS_exit 2
#define SYS_wait 3
#define SYS_pipe 4
#define SYS_read 5
#define SYS_kill 6
#define SYS_exec 7
#define SYS_fstat 8
#define SYS_chdir 9
#define SYS_dup 10
#define SYS_getpid 11
#define SYS_sbrk 12
#define SYS_sleep 13
#define SYS_uptime 14
#define SYS_open 15
#define SYS_write 16
#define SYS_mknod 17
#define SYS_unlink 18
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21
接下来来看看xv6执行系统调用的过程。
在xv6启动之时,在
main
函数初始化几个设备和子系统后,便通过调用userinit
函数来创建第一个进程,第一个进程执行一个用RISC-V程序集写的小型程序(在user/initcode.S
)中,它通过调用exec
系统调用重新进入内核。.globl start
start:
la a0, init
la a1, argv
li a7, SYS_exec
ecall
可以看到,
li
指令将参数SYS_exec
(在kernel/syscall.h
查看可知该值为7)存到寄存器a7
中,然后使用ecall
指令将控制权转移到操作系统内核中的一个特定地址,以执行特权级操作。通过这一系列操作之后,函数syscall
(在kernel/syscall.c
中)从陷阱帧(trapframe)中保存的a7
中检索系统调用号(p->trapframe->a7
),并用它索引到syscalls
中。void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// Use num to lookup the system call function for num, call it,
// and store its return value in p->trapframe->a0
p->trapframe->a0 = syscalls[num]();
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
接下来就可以通过函数指针数组
*syscalls[]
在kernel/syscall.c
中)定位到相应的函数,在内核中执行该系统调用的功能,返回值将记录在p->trapframe->a0
中,可以使得用户空间的系统调用返回这个值。之所以是该寄存器,是因为RISC-V上的C调用约定将返回值放在a0
中。那么接下来的一个问题是:
ecall
指令执行的过程中到底发生了什么,可以进入操作系统内核态。在这个过程中,内核中执行的第一个指令是一个由汇编语言写的函数,叫做
uservec
。uservec
充斥着大量的汇编代码不便于阅读,在这里我们不需要过度关心uservec
的细节。我们只需要知道之后在uservec
函数中,代码执行跳转到了由C语言实现的函数usertrap
。跳转到了C语言的函数,我们便能更好的理解了。在usertrap
函数中,我们执行了函数syscall
。void
usertrap(void)
{
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
// save user program counter.
p->trapframe->epc = r_sepc();
if(r_scause() == 8){
// system call
if(killed(p))
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
// an interrupt will change sepc, scause, and sstatus,
// so enable only now that we're done with those registers.
intr_on();
syscall();
这里节选了一部分
usertrap
函数,可以看到,在执行函数syscall
之前,还做了一些必要的操作,包括保证此时的系统调用是从用户态进入的,还有做一些中断相关的操作。在函数
syscall
执行相应函数返回之后,同样的,我们需要返回用户空间中。而之前的ecall
指令中断了用户空间代码的执行,恢复到用户空间,需要做一系列的事情,而usertrap
函数的最后会执行usertrapret
函数,该函数会完成这部分工作。当然,有一些工作只能由汇编代码完成,所以usertrapret
函数最后会跳转到汇编代码执行userret
函数。我们可以用一幅图来概括这个过程:
职是之故,我们可以看到在用户空间看来一条简单的
ecall
指令,其中包含了大量的软硬件操作,对于用户空间来说,这些事情都是透明的,只需要对着系统调用发出请求,就可以得到自己想要的结果。一些更详细的关于trap
的操作,可以阅读xv6 book的第四章进行了解。有了系统调用作为应用程序和操作系统之间的媒介,我们也可以通过系统调用来研究操作系统,可以通过追踪应用程序的系统调用来观察程序的执行,这便是
strace
工具的强大之处,所以在xv6的lab中也设计了一个实现trace
的lab,这也是在提示我们这一点的重要性。
Loading Comments...