最近更新于2021/08/05.
我们以xv6的一个系统调用getpid()为例,观察xv6的系统调用大致过程。
系统调用的声明位于 user.h 中,xv6的用户程序若要使用系统调用需要包括这个头文件,其中getpid()声明如下:
int getpid(void);
此函数定义于 usys.S 中,函数体通过宏SYSCALL定义:
usys.S/getpid
#define SYSCALL(name) \
.globl name; \
name: \
movl $SYS_ ## name, %eax; \
int $T_SYSCALL; \
ret
...
SYSCALL(getpid)
...
将宏SYSCALL展开后如下所示:
.globl getpid; # 声明全局变量getpid
getpid:
movl $SYS_getpid, %eax;
int $T_SYSCALL;
ret
可以看到getpid()将系统调用的类型编号SYS_getpid(定义于 syscall.h 中,其值为11)写入寄存器 %eax, 之后通过指令 int $T_SYSCALL 产生软中断(陷阱),从而调用相应的中断处理程序后返回。(T_SYSCALL定义于 traps.h 中,其值为64,通常系统调用的编号为0x80, 即128).
产生中断后,硬件会在中断描述符表(IDT, Interrupt Descriptor Table)中查找用于处理系统调用的项。在xv6中,IDT是一个数组,定义于文件 trap.c 中:
struct gatedesc idt[256];
结构体gatedesc保存了中断处理程序的相关信息,例如中断处理程序的首地址。数组idt在函数tvinit()中被初始化:
trap.c/tvinit
void
tvinit(void)
{
int i;
for(i = 0; i < 256; i++)
SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0);
SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);
initlock(&tickslock, "time");
}
宏SETGATE用户初始化结构体gatedesc. 可以看出,处理系统调用的程序首地址为vectors[T_SYSCALL],数组vectors由Perl脚本 vectors.pl 生成,执行如下命令:
$ perl vectors.pl > vectors.S
在生成的文件 vectors.S 中,可以看到数组vectors的第X项为函数vectorX的首地址。因此处理系统调用的函数应为vector64:
.globl vector64
vector64:
pushl $0
pushl $64
jmp alltraps
向栈中压入0和系统调用对应的编号64后,跳转到函数alltraps(). 函数alltraps()定义于文件 trapasm.S 中:
trapasm.S/alltraps
.globl alltraps
alltraps:
# Build trap frame.
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
# Set up data segments.
movw $(SEG_KDATA<<3), %ax
movw %ax, %ds
movw %ax, %es
# Call trap(tf), where tf=%esp
pushl %esp
call trap
addl $4, %esp
# Return falls through to trapret...
.globl trapret
trapret:
popal
popl %gs
popl %fs
popl %es
popl %ds
addl $0x8, %esp # trapno and errcode
iret
此函数首先将一些寄存器压入栈中,然后调用函数trap(), 随后从栈中恢复寄存器的值,最后以指令iret恢复到用户权限并返回。寄存器压入栈中的顺序是有讲究的,在压入这些寄存器后,在栈顶构造了一个结构体trapframe,因此可以通过向函数trap()传入栈顶寄存器%esp来引用此结构体。结构体trapframe定义于 x86.h 中:
x86.h/struct trapframe
// Layout of the trap frame built on the stack by the
// hardware and by trapasm.S, and passed to trap().
struct trapframe {
// registers as pushed by pusha
uint edi;
uint esi;
uint ebp;
uint oesp; // useless & ignored
uint ebx;
uint edx;
uint ecx;
uint eax;
// rest of trap frame
ushort gs;
ushort padding1;
ushort fs;
ushort padding2;
ushort es;
ushort padding3;
ushort ds;
ushort padding4;
uint trapno;
// below here defined by x86 hardware
uint err;
uint eip;
ushort cs;
ushort padding5;
uint eflags;
// below here only when crossing rings, such as from user to kernel
uint esp;
ushort ss;
ushort padding6;
};
函数trap()位于 trap.c 中,我们考虑与系统调用相关的那一部分:
void
trap(struct trapframe *tf)
{
if(tf->trapno == T_SYSCALL){ // 若为系统调用
if(myproc()->killed)
exit();
myproc()->tf = tf;
syscall();
if(myproc()->killed)
exit();
return;
}
...
}
此函数首先检查发起系统的进程是否已被杀死,如果已被杀死就退出;否则调用函数syscall(),调用完成后再次检查发起系统调用的进程是否被杀死,若已被杀死就退出,否则返回。
看来函数syscall()是真正用于处理系统调用的函数,它位于syscall.c 中:
void
syscall(void)
{
int num;
struct proc *curproc = myproc();
num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
curproc->tf->eax = syscalls[num]();
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}
还记得吗?一开始我们就将系统调用的编号保存到寄存器%eax中,现在我们要通过查看%eax的值确定发起系统调用的类型,在这里%eax中保存的值为11(SYS_getpid), 因此我们调用函数syscalls[11](), 即函数sys_getpid.(数组syscalls就定义在函数syscall的上面)。函数sys_getpid位于 sysproc.c 中,它做的事情也很简单:返回当前进程的pid:
int
sys_getpid(void)
{
return myproc()->pid;
}
以上就是xv6中系统调用getpid()的大致执行流程。